diff --git a/github4s/src/main/scala/github4s/Decoders.scala b/github4s/src/main/scala/github4s/Decoders.scala index eb9b6c545..97649a00a 100644 --- a/github4s/src/main/scala/github4s/Decoders.scala +++ b/github4s/src/main/scala/github4s/Decoders.scala @@ -272,6 +272,8 @@ object Decoders { implicit val decoderAuthorization: Decoder[Authorization] = deriveDecoder[Authorization] implicit val decoderOAuthToken: Decoder[OAuthToken] = deriveDecoder[OAuthToken] implicit val decoderRelease: Decoder[Release] = deriveDecoder[Release] + implicit val decoderUserRepoPermission: Decoder[UserRepoPermission] = + deriveDecoder[UserRepoPermission] implicit val decodeStargazer: Decoder[Stargazer] = decoderUser diff --git a/github4s/src/main/scala/github4s/algebras/Repositories.scala b/github4s/src/main/scala/github4s/algebras/Repositories.scala index e106399ab..a8b6998f1 100644 --- a/github4s/src/main/scala/github4s/algebras/Repositories.scala +++ b/github4s/src/main/scala/github4s/algebras/Repositories.scala @@ -271,6 +271,22 @@ trait Repositories[F[_]] { headers: Map[String, String] = Map() ): F[GHResponse[List[User]]] + /** + * Get the repository permission of a collaborator + * + * @param owner of the repo + * @param repo name of the repo + * @param username Github username + * @param headers optional user headers to include in the request + * @return a GHResponse with UserRepoPermission + */ + def getRepoPermissionForUser( + owner: String, + repo: String, + username: String, + headers: Map[String, String] = Map() + ): F[GHResponse[UserRepoPermission]] + /** * Get a single release * diff --git a/github4s/src/main/scala/github4s/domain/Repository.scala b/github4s/src/main/scala/github4s/domain/Repository.scala index adce3ee8a..406e4187e 100644 --- a/github4s/src/main/scala/github4s/domain/Repository.scala +++ b/github4s/src/main/scala/github4s/domain/Repository.scala @@ -202,6 +202,9 @@ final case class Committer( name: String, email: String ) + +final case class UserRepoPermission(permission: String, user: User) + object RepoUrlKeys { val forks_url = "forks_url" diff --git a/github4s/src/main/scala/github4s/interpreters/RepositoriesInterpreter.scala b/github4s/src/main/scala/github4s/interpreters/RepositoriesInterpreter.scala index e85e6d0fa..563fbd033 100644 --- a/github4s/src/main/scala/github4s/interpreters/RepositoriesInterpreter.scala +++ b/github4s/src/main/scala/github4s/interpreters/RepositoriesInterpreter.scala @@ -217,6 +217,20 @@ class RepositoriesInterpreter[F[_]](implicit client: HttpClient[F], accessToken: pagination ) + override def getRepoPermissionForUser( + owner: String, + repo: String, + username: String, + headers: Map[String, String] + ): F[GHResponse[UserRepoPermission]] = + client + .get[UserRepoPermission]( + accessToken, + s"repos/$owner/$repo/collaborators/$username/permission", + headers, + Map.empty + ) + override def latestRelease( owner: String, repo: String, diff --git a/github4s/src/test/scala/github4s/integration/ReposSpec.scala b/github4s/src/test/scala/github4s/integration/ReposSpec.scala index d296cd5f9..b935af9f3 100644 --- a/github4s/src/test/scala/github4s/integration/ReposSpec.scala +++ b/github4s/src/test/scala/github4s/integration/ReposSpec.scala @@ -313,6 +313,40 @@ trait ReposSpec extends BaseIntegrationSpec { response.statusCode shouldBe notFoundStatusCode } + "Repos >> GetRepoPermissionForUser" should "return user repo permission" taggedAs Integration in { + val response = clientResource + .use { client => + Github[IO](client, accessToken).repos + .getRepoPermissionForUser( + validRepoOwner, + validRepoName, + validUsername, + headers = headerUserAgent + ) + } + .unsafeRunSync() + + testIsRight[UserRepoPermission](response, r => r.user.login shouldBe validUsername) + response.statusCode shouldBe okStatusCode + } + + it should "return error when invalid username is passed" taggedAs Integration in { + val response = clientResource + .use { client => + Github[IO](client, accessToken).repos + .getRepoPermissionForUser( + validRepoOwner, + validRepoName, + invalidUsername, + headers = headerUserAgent + ) + } + .unsafeRunSync() + + testIsLeft[NotFoundError, UserRepoPermission](response) + response.statusCode shouldBe notFoundStatusCode + } + "Repos >> GetStatus" should "return a combined status" taggedAs Integration in { val response = clientResource .use { client => diff --git a/github4s/src/test/scala/github4s/unit/DecodersSpec.scala b/github4s/src/test/scala/github4s/unit/DecodersSpec.scala index c2f5e9334..f3215bc76 100644 --- a/github4s/src/test/scala/github4s/unit/DecodersSpec.scala +++ b/github4s/src/test/scala/github4s/unit/DecodersSpec.scala @@ -71,6 +71,10 @@ class DecodersSpec extends AnyFlatSpec with Matchers with FakeResponses { decode[NonEmptyList[Int]]("[1,2,3]") shouldBe Right(NonEmptyList.of(1, 2, 3)) } + "UserRepoPermission decoder" should "return a UserRepoPermission" in { + decode[UserRepoPermission](getUserRepoPermissionResponse).isRight shouldBe true + } + case class Foo(a: Int) it should "return a valid NonEmptyList for a valid JSON" in { decode[NonEmptyList[Foo]]("""{"a": 1}""") shouldBe Right(NonEmptyList(Foo(1), Nil)) diff --git a/github4s/src/test/scala/github4s/unit/IssuesSpec.scala b/github4s/src/test/scala/github4s/unit/IssuesSpec.scala index cdb62f5b0..62a76da1a 100644 --- a/github4s/src/test/scala/github4s/unit/IssuesSpec.scala +++ b/github4s/src/test/scala/github4s/unit/IssuesSpec.scala @@ -197,7 +197,7 @@ class IssuesSpec extends BaseSpec { "Issue.DeleteComment" should "call to httpClient.delete with the right parameters" in { val response: IO[GHResponse[Unit]] = - IO(GHResponse(().asRight, deletedStatusCode, Map.empty)) + IO(GHResponse(().asRight, noContentStatusCode, Map.empty)) implicit val httpClientMock = httpClientMockDelete( url = s"repos/$validRepoOwner/$validRepoName/issues/comments/$validCommentId", @@ -393,7 +393,7 @@ class IssuesSpec extends BaseSpec { "Issue.DeleteMilestone" should "call to httpClient.delete with the right parameters" in { val response: IO[GHResponse[Unit]] = - IO(GHResponse(().asRight, deletedStatusCode, Map.empty)) + IO(GHResponse(().asRight, noContentStatusCode, Map.empty)) implicit val httpClientMock = httpClientMockDelete( url = s"repos/$validRepoOwner/$validRepoName/milestones/$validMilestoneNumber", diff --git a/github4s/src/test/scala/github4s/unit/ReposSpec.scala b/github4s/src/test/scala/github4s/unit/ReposSpec.scala index 25bb9750b..20ef19266 100644 --- a/github4s/src/test/scala/github4s/unit/ReposSpec.scala +++ b/github4s/src/test/scala/github4s/unit/ReposSpec.scala @@ -16,14 +16,14 @@ package github4s.unit -import cats.effect.IO import cats.data.NonEmptyList +import cats.effect.IO import cats.syntax.either._ +import com.github.marklister.base64.Base64._ import github4s.GHResponse import github4s.domain._ import github4s.interpreters.RepositoriesInterpreter import github4s.utils.BaseSpec -import com.github.marklister.base64.Base64._ class ReposSpec extends BaseSpec { @@ -342,6 +342,25 @@ class ReposSpec extends BaseSpec { repos.listCollaborators(validRepoOwner, validRepoName, headers = headerUserAgent) } + "Repos.getRepoPermissionForUser" should "call to httpClient.get with the right parameters" in { + val response: IO[GHResponse[UserRepoPermission]] = + IO(GHResponse(userRepoPermission.asRight, okStatusCode, Map.empty)) + + implicit val httpClientMock = httpClientMockGet[UserRepoPermission]( + url = s"repos/$validRepoOwner/$validRepoName/collaborators/$validUsername/permission", + response = response + ) + + val repos = new RepositoriesInterpreter[IO] + + repos.getRepoPermissionForUser( + validRepoOwner, + validRepoName, + validUsername, + headers = headerUserAgent + ) + } + "Repos.getCombinedStatus" should "call httpClient.get with the right parameters" in { val response: IO[GHResponse[CombinedStatus]] = IO(GHResponse(combinedStatus.asRight, okStatusCode, Map.empty)) diff --git a/github4s/src/test/scala/github4s/utils/FakeResponses.scala b/github4s/src/test/scala/github4s/utils/FakeResponses.scala index 700aa1482..2af66cbd8 100644 --- a/github4s/src/test/scala/github4s/utils/FakeResponses.scala +++ b/github4s/src/test/scala/github4s/utils/FakeResponses.scala @@ -514,4 +514,31 @@ trait FakeResponses { |} """.stripMargin + val getUserRepoPermissionResponse = + s""" + |{ + | "permission": "admin", + | "user": { + | "login": "octocat", + | "id": 1, + | "node_id": "MDQ6VXNlcjE=", + | "avatar_url": "https://github.com/images/error/octocat_happy.gif", + | "gravatar_id": "", + | "url": "https://api.github.com/users/octocat", + | "html_url": "https://github.com/octocat", + | "followers_url": "https://api.github.com/users/octocat/followers", + | "following_url": "https://api.github.com/users/octocat/following{/other_user}", + | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + | "organizations_url": "https://api.github.com/users/octocat/orgs", + | "repos_url": "https://api.github.com/users/octocat/repos", + | "events_url": "https://api.github.com/users/octocat/events{/privacy}", + | "received_events_url": "https://api.github.com/users/octocat/received_events", + | "type": "User", + | "site_admin": false + | } + |} + |""".stripMargin + } diff --git a/github4s/src/test/scala/github4s/utils/TestData.scala b/github4s/src/test/scala/github4s/utils/TestData.scala index 14a6412f7..9cf17f887 100644 --- a/github4s/src/test/scala/github4s/utils/TestData.scala +++ b/github4s/src/test/scala/github4s/utils/TestData.scala @@ -33,8 +33,9 @@ trait TestData { val invalidUsername = "GHInvalidUserName" val invalidPassword = "invalidPassword" - val githubApiUrl = "http://api.github.com" - val user = User(1, validUsername, githubApiUrl, githubApiUrl) + val githubApiUrl = "http://api.github.com" + val user = User(1, validUsername, githubApiUrl, githubApiUrl) + val userRepoPermission: UserRepoPermission = UserRepoPermission("admin", user) def validBasicAuth = s"Basic ${s"$validUsername:".getBytes.toBase64}" @@ -70,7 +71,7 @@ trait TestData { val okStatusCode = 200 val createdStatusCode = 201 - val deletedStatusCode = 204 + val noContentStatusCode = 204 val unauthorizedStatusCode = 401 val notFoundStatusCode = 404 diff --git a/microsite/docs/repository.md b/microsite/docs/repository.md index bdcdb8dda..3b50a34fa 100644 --- a/microsite/docs/repository.md +++ b/microsite/docs/repository.md @@ -15,6 +15,7 @@ with Github4s, you can interact with: - [List user repositories](#list-user-repositories) - [List contributors](#list-contributors) - [List collaborators](#list-collaborators) + - [Get repository permissions for a user](#get-repository-permissions-for-a-user) - [Commits](#commits) - [List commits on a repository](#list-commits-on-a-repository) - [Contents](#contents) @@ -181,6 +182,26 @@ The `result` on the right is the corresponding [List[User]][user-scala]. See [the API doc](https://developer.github.com/v3/repos/collaborators/#list-collaborators) for full reference. +### Get repository permissions for a user + +Checks the repository permission of a collaborator. + +The possible repository permissions are `admin`, `write`, `read`, and `none`. + +```scala mdoc:compile-only +val userRepoPermission = gh.repos.getRepoPermissionForUser("47degrees", "github4s", "rafaparadela") +val response = userRepoPermission.unsafeRunSync() +response.result match { + case Left(e) => println(s"Something went wrong: ${e.getMessage}") + case Right(r) => println(r) +} +``` + +The `result` on the right is the corresponding [UserRepoPermission][repository-scala]. + +See [the API doc](https://developer.github.com/v3/repos/collaborators/#get-repository-permissions-for-a-user) for full +reference. + ## Commits ### List commits on a repository