Skip to content

Commit df9ec5e

Browse files
authored
Support getting repository perms for a user (#528)
1 parent 01206f8 commit df9ec5e

11 files changed

Lines changed: 148 additions & 7 deletions

File tree

github4s/src/main/scala/github4s/Decoders.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,8 @@ object Decoders {
272272
implicit val decoderAuthorization: Decoder[Authorization] = deriveDecoder[Authorization]
273273
implicit val decoderOAuthToken: Decoder[OAuthToken] = deriveDecoder[OAuthToken]
274274
implicit val decoderRelease: Decoder[Release] = deriveDecoder[Release]
275+
implicit val decoderUserRepoPermission: Decoder[UserRepoPermission] =
276+
deriveDecoder[UserRepoPermission]
275277

276278
implicit val decodeStargazer: Decoder[Stargazer] =
277279
decoderUser

github4s/src/main/scala/github4s/algebras/Repositories.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,22 @@ trait Repositories[F[_]] {
271271
headers: Map[String, String] = Map()
272272
): F[GHResponse[List[User]]]
273273

274+
/**
275+
* Get the repository permission of a collaborator
276+
*
277+
* @param owner of the repo
278+
* @param repo name of the repo
279+
* @param username Github username
280+
* @param headers optional user headers to include in the request
281+
* @return a GHResponse with UserRepoPermission
282+
*/
283+
def getRepoPermissionForUser(
284+
owner: String,
285+
repo: String,
286+
username: String,
287+
headers: Map[String, String] = Map()
288+
): F[GHResponse[UserRepoPermission]]
289+
274290
/**
275291
* Get a single release
276292
*

github4s/src/main/scala/github4s/domain/Repository.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ final case class Committer(
202202
name: String,
203203
email: String
204204
)
205+
206+
final case class UserRepoPermission(permission: String, user: User)
207+
205208
object RepoUrlKeys {
206209

207210
val forks_url = "forks_url"

github4s/src/main/scala/github4s/interpreters/RepositoriesInterpreter.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,20 @@ class RepositoriesInterpreter[F[_]](implicit client: HttpClient[F], accessToken:
217217
pagination
218218
)
219219

220+
override def getRepoPermissionForUser(
221+
owner: String,
222+
repo: String,
223+
username: String,
224+
headers: Map[String, String]
225+
): F[GHResponse[UserRepoPermission]] =
226+
client
227+
.get[UserRepoPermission](
228+
accessToken,
229+
s"repos/$owner/$repo/collaborators/$username/permission",
230+
headers,
231+
Map.empty
232+
)
233+
220234
override def latestRelease(
221235
owner: String,
222236
repo: String,

github4s/src/test/scala/github4s/integration/ReposSpec.scala

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,40 @@ trait ReposSpec extends BaseIntegrationSpec {
313313
response.statusCode shouldBe notFoundStatusCode
314314
}
315315

316+
"Repos >> GetRepoPermissionForUser" should "return user repo permission" taggedAs Integration in {
317+
val response = clientResource
318+
.use { client =>
319+
Github[IO](client, accessToken).repos
320+
.getRepoPermissionForUser(
321+
validRepoOwner,
322+
validRepoName,
323+
validUsername,
324+
headers = headerUserAgent
325+
)
326+
}
327+
.unsafeRunSync()
328+
329+
testIsRight[UserRepoPermission](response, r => r.user.login shouldBe validUsername)
330+
response.statusCode shouldBe okStatusCode
331+
}
332+
333+
it should "return error when invalid username is passed" taggedAs Integration in {
334+
val response = clientResource
335+
.use { client =>
336+
Github[IO](client, accessToken).repos
337+
.getRepoPermissionForUser(
338+
validRepoOwner,
339+
validRepoName,
340+
invalidUsername,
341+
headers = headerUserAgent
342+
)
343+
}
344+
.unsafeRunSync()
345+
346+
testIsLeft[NotFoundError, UserRepoPermission](response)
347+
response.statusCode shouldBe notFoundStatusCode
348+
}
349+
316350
"Repos >> GetStatus" should "return a combined status" taggedAs Integration in {
317351
val response = clientResource
318352
.use { client =>

github4s/src/test/scala/github4s/unit/DecodersSpec.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ class DecodersSpec extends AnyFlatSpec with Matchers with FakeResponses {
7171
decode[NonEmptyList[Int]]("[1,2,3]") shouldBe Right(NonEmptyList.of(1, 2, 3))
7272
}
7373

74+
"UserRepoPermission decoder" should "return a UserRepoPermission" in {
75+
decode[UserRepoPermission](getUserRepoPermissionResponse).isRight shouldBe true
76+
}
77+
7478
case class Foo(a: Int)
7579
it should "return a valid NonEmptyList for a valid JSON" in {
7680
decode[NonEmptyList[Foo]]("""{"a": 1}""") shouldBe Right(NonEmptyList(Foo(1), Nil))

github4s/src/test/scala/github4s/unit/IssuesSpec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ class IssuesSpec extends BaseSpec {
197197
"Issue.DeleteComment" should "call to httpClient.delete with the right parameters" in {
198198

199199
val response: IO[GHResponse[Unit]] =
200-
IO(GHResponse(().asRight, deletedStatusCode, Map.empty))
200+
IO(GHResponse(().asRight, noContentStatusCode, Map.empty))
201201

202202
implicit val httpClientMock = httpClientMockDelete(
203203
url = s"repos/$validRepoOwner/$validRepoName/issues/comments/$validCommentId",
@@ -393,7 +393,7 @@ class IssuesSpec extends BaseSpec {
393393
"Issue.DeleteMilestone" should "call to httpClient.delete with the right parameters" in {
394394

395395
val response: IO[GHResponse[Unit]] =
396-
IO(GHResponse(().asRight, deletedStatusCode, Map.empty))
396+
IO(GHResponse(().asRight, noContentStatusCode, Map.empty))
397397

398398
implicit val httpClientMock = httpClientMockDelete(
399399
url = s"repos/$validRepoOwner/$validRepoName/milestones/$validMilestoneNumber",

github4s/src/test/scala/github4s/unit/ReposSpec.scala

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616

1717
package github4s.unit
1818

19-
import cats.effect.IO
2019
import cats.data.NonEmptyList
20+
import cats.effect.IO
2121
import cats.syntax.either._
22+
import com.github.marklister.base64.Base64._
2223
import github4s.GHResponse
2324
import github4s.domain._
2425
import github4s.interpreters.RepositoriesInterpreter
2526
import github4s.utils.BaseSpec
26-
import com.github.marklister.base64.Base64._
2727

2828
class ReposSpec extends BaseSpec {
2929

@@ -342,6 +342,25 @@ class ReposSpec extends BaseSpec {
342342
repos.listCollaborators(validRepoOwner, validRepoName, headers = headerUserAgent)
343343
}
344344

345+
"Repos.getRepoPermissionForUser" should "call to httpClient.get with the right parameters" in {
346+
val response: IO[GHResponse[UserRepoPermission]] =
347+
IO(GHResponse(userRepoPermission.asRight, okStatusCode, Map.empty))
348+
349+
implicit val httpClientMock = httpClientMockGet[UserRepoPermission](
350+
url = s"repos/$validRepoOwner/$validRepoName/collaborators/$validUsername/permission",
351+
response = response
352+
)
353+
354+
val repos = new RepositoriesInterpreter[IO]
355+
356+
repos.getRepoPermissionForUser(
357+
validRepoOwner,
358+
validRepoName,
359+
validUsername,
360+
headers = headerUserAgent
361+
)
362+
}
363+
345364
"Repos.getCombinedStatus" should "call httpClient.get with the right parameters" in {
346365
val response: IO[GHResponse[CombinedStatus]] =
347366
IO(GHResponse(combinedStatus.asRight, okStatusCode, Map.empty))

github4s/src/test/scala/github4s/utils/FakeResponses.scala

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,4 +514,31 @@ trait FakeResponses {
514514
|}
515515
""".stripMargin
516516

517+
val getUserRepoPermissionResponse =
518+
s"""
519+
|{
520+
| "permission": "admin",
521+
| "user": {
522+
| "login": "octocat",
523+
| "id": 1,
524+
| "node_id": "MDQ6VXNlcjE=",
525+
| "avatar_url": "https://github.com/images/error/octocat_happy.gif",
526+
| "gravatar_id": "",
527+
| "url": "https://api.github.com/users/octocat",
528+
| "html_url": "https://github.com/octocat",
529+
| "followers_url": "https://api.github.com/users/octocat/followers",
530+
| "following_url": "https://api.github.com/users/octocat/following{/other_user}",
531+
| "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
532+
| "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
533+
| "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
534+
| "organizations_url": "https://api.github.com/users/octocat/orgs",
535+
| "repos_url": "https://api.github.com/users/octocat/repos",
536+
| "events_url": "https://api.github.com/users/octocat/events{/privacy}",
537+
| "received_events_url": "https://api.github.com/users/octocat/received_events",
538+
| "type": "User",
539+
| "site_admin": false
540+
| }
541+
|}
542+
|""".stripMargin
543+
517544
}

github4s/src/test/scala/github4s/utils/TestData.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ trait TestData {
3333
val invalidUsername = "GHInvalidUserName"
3434
val invalidPassword = "invalidPassword"
3535

36-
val githubApiUrl = "http://api.github.com"
37-
val user = User(1, validUsername, githubApiUrl, githubApiUrl)
36+
val githubApiUrl = "http://api.github.com"
37+
val user = User(1, validUsername, githubApiUrl, githubApiUrl)
38+
val userRepoPermission: UserRepoPermission = UserRepoPermission("admin", user)
3839

3940
def validBasicAuth = s"Basic ${s"$validUsername:".getBytes.toBase64}"
4041

@@ -70,7 +71,7 @@ trait TestData {
7071

7172
val okStatusCode = 200
7273
val createdStatusCode = 201
73-
val deletedStatusCode = 204
74+
val noContentStatusCode = 204
7475
val unauthorizedStatusCode = 401
7576
val notFoundStatusCode = 404
7677

0 commit comments

Comments
 (0)