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

Get a single issue #191

Merged
merged 3 commits into from
Jan 23, 2018
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion docs/src/main/tut/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ See [the API doc](https://developer.github.com/v3/issues/#edit-an-issue) for ful

### List issues

You can also list issues for a repository through `listIssues`; it take as arguments:
You can also list issues for a repository through `listIssues`; it takes as arguments:

- the repository coordinates (`owner` and `name` of the repository).

Expand All @@ -111,6 +111,29 @@ contain pull requests as Github considers pull requests as issues.
See [the API doc](https://developer.github.com/v3/issues/#list-issues-for-a-repository)
for full reference.

### Get a single issue

You can also get a single issue of a repository through `getIssue`; it takes as arguments:

- the repository coordinates (`owner` and `name` of the repository).
- `number`: The issue number.

To get a single issue from a repository:

```tut:silent
val issue = Github(accessToken).issues.getIssue("47deg", "github4s", 123)

issue.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 [Issue][issue-scala]. Note that it will
return pull requests as Github considers pull requests as issues.

See [the API doc](https://developer.github.com/v3/issues/#get-a-single-issue)
for full reference.

### Search issues

Expand Down
20 changes: 20 additions & 0 deletions github4s/jvm/src/test/scala/github4s/unit/ApiSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,26 @@ class ApiSpec
response should be('left)
}

"Issues >> Get" should "return the expected issue when a valid owner/repo is provided" in {
val response =
issues.get(accessToken, headerUserAgent, validRepoOwner, validRepoName, validIssueNumber)
response should be('right)

response.toOption map { r ⇒
r.statusCode shouldBe okStatusCode
}
}
it should "return an error if an invalid issue number is provided" in {
val response =
issues.get(accessToken, headerUserAgent, validRepoOwner, validRepoName, invalidIssueNumber)
response should be('left)
}
it should "return an error if no tokens are provided" in {
val response =
issues.get(None, headerUserAgent, validRepoOwner, validRepoName, validIssueNumber)
response should be('left)
}

"Issues >> Create" should "return the created issue if valid data is provided" in {
val response = issues.create(
accessToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,23 @@ trait MockGithubApiServer extends MockServerService with FakeResponses with Test
.withHeader("Authorization", tokenHeader))
.respond(response.withStatusCode(notFoundStatusCode).withBody(notFoundResponse))

//Issues >> get
mockServer
.when(
request
.withMethod("GET")
.withPath(s"/repos/$validRepoOwner/$validRepoName/issues/$validIssueNumber")
.withHeader("Authorization", tokenHeader))
.respond(response.withStatusCode(okStatusCode).withBody(getIssueValidResponse))

mockServer
.when(
request
.withMethod("GET")
.withPath(s"/repos/$validRepoOwner/$validRepoName/issues/$invalidIssueNumber")
.withHeader("Authorization", tokenHeader))
.respond(response.withStatusCode(notFoundStatusCode).withBody(notFoundResponse))

//Issues >> create
mockServer
.when(
Expand Down
9 changes: 8 additions & 1 deletion github4s/shared/src/main/scala/github4s/GithubAPIs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@ class GHIssues(accessToken: Option[String] = None)(implicit O: IssueOps[GitHub4s
): GHIO[GHResponse[List[Issue]]] =
O.listIssues(owner, repo, accessToken)

def getIssue(
owner: String,
repo: String,
number: Int
): GHIO[GHResponse[Issue]] =
O.getIssue(owner, repo, number, accessToken)

def searchIssues(
query: String,
searchParams: List[SearchParam]
Expand Down Expand Up @@ -365,4 +372,4 @@ class GHOrganizations(accessToken: Option[String] = None)(implicit O: Organizati
pagination: Option[Pagination] = None
): GHIO[GHResponse[List[User]]] =
O.listMembers(org, filter, role, pagination, accessToken)
}
}
25 changes: 25 additions & 0 deletions github4s/shared/src/main/scala/github4s/api/Issues.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,31 @@ class Issues[C, M[_]](
repo: String): M[GHResponse[List[Issue]]] =
httpClient.get[List[Issue]](accessToken, s"repos/$owner/$repo/issues", headers)

/**
* Get a single issue of a repository
*
* Note: In the past, pull requests and issues were more closely aligned than they are now.
* As far as the API is concerned, every pull request is an issue, but not every issue is a
* pull request.
*
* This endpoint may also return pull requests in the response. If an issue is a pull request,
* the object will include a `pull_request` key.
*
* @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
* @return a GHResponse with the issue list.
*/
def get(
accessToken: Option[String] = None,
headers: Map[String, String] = Map(),
owner: String,
repo: String,
number: Int): M[GHResponse[Issue]] =
httpClient.get[Issue](accessToken, s"repos/$owner/$repo/issues/$number", headers)

/**
* Search for issues
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ final case class ListIssues(
accessToken: Option[String] = None
) extends IssueOp[GHResponse[List[Issue]]]

final case class GetIssue(
owner: String,
repo: String,
number: Int,
accessToken: Option[String] = None
) extends IssueOp[GHResponse[Issue]]

final case class SearchIssues(
query: String,
searchParams: List[SearchParam],
Expand Down Expand Up @@ -98,6 +105,14 @@ class IssueOps[F[_]](implicit I: InjectK[IssueOp, F]) {
): Free[F, GHResponse[List[Issue]]] =
Free.inject[IssueOp, F](ListIssues(owner, repo, accessToken))

def getIssue(
owner: String,
repo: String,
number: Int,
accessToken: Option[String] = None
): Free[F, GHResponse[Issue]] =
Free.inject[IssueOp, F](GetIssue(owner, repo, number, accessToken))

def searchIssues(
query: String,
searchParams: List[SearchParam],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ class Interpreters[M[_], C](
fa match {
case ListIssues(owner, repo, accessToken) ⇒
issues.list(accessToken, headers, owner, repo)
case GetIssue(owner, repo, number, accessToken) ⇒
issues.get(accessToken, headers, owner, repo, number)
case SearchIssues(query, searchParams, accessToken) ⇒
issues.search(accessToken, headers, query, searchParams)
case CreateIssue(owner, repo, title, body, milestone, labels, assignees, accessToken) ⇒
Expand Down
12 changes: 12 additions & 0 deletions github4s/shared/src/test/scala/github4s/unit/GHIssuesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ class GHIssuesSpec extends BaseSpec {
ghIssues.listIssues(validRepoOwner, validRepoName)
}

"GHIssues.getIssue" should "call to IssuesOps with the right parameters" in {
val response: Free[GitHub4s, GHResponse[Issue]] =
Free.pure(Right(GHResult(issue, okStatusCode, Map.empty)))

val issuesOps = mock[IssueOpsTest]
(issuesOps.getIssue _)
.expects(validRepoOwner, validRepoName, validIssueNumber, sampleToken)
.returns(response)
val ghIssues = new GHIssues(sampleToken)(issuesOps)
ghIssues.getIssue(validRepoOwner, validRepoName, validIssueNumber)
}

"GHIssues.searchIssues" should "call to IssuesOps with the right parameters" in {
val response: Free[GitHub4s, GHResponse[SearchIssuesResult]] =
Free.pure(Right(GHResult(searchIssuesResult, okStatusCode, Map.empty)))
Expand Down
15 changes: 15 additions & 0 deletions github4s/shared/src/test/scala/github4s/unit/IssuesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ class IssuesSpec extends BaseSpec {
issues.list(sampleToken, headerUserAgent, validRepoOwner, validRepoName)
}

"Issues.get" should "call httpClient.get with the right parameters" in {
val response: GHResponse[Issue] =
Right(GHResult(issue, okStatusCode, Map.empty))

val httpClientMock = httpClientMockGet[Issue](
url = s"repos/$validRepoOwner/$validRepoName/issues/$validIssueNumber",
response = response
)

val issues = new Issues[String, Id] {
override val httpClient: HttpClient[String, Id] = httpClientMock
}
issues.get(sampleToken, headerUserAgent, validRepoOwner, validRepoName, validIssueNumber)
}

"Issues.search" should "call htppClient.get with the right parameters" in {
val response: GHResponse[SearchIssuesResult] =
Right(GHResult(searchIssuesResult, okStatusCode, Map.empty))
Expand Down
119 changes: 119 additions & 0 deletions github4s/shared/src/test/scala/github4s/utils/FakeResponses.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1791,6 +1791,125 @@ trait FakeResponses {
| }
|]""".stripMargin

val getIssueValidResponse =
"""
|{
| "url": "https://api.github.com/repos/47deg/github4s/issues/48",
| "repository_url": "https://api.github.com/repos/47deg/github4s",
| "labels_url": "https://api.github.com/repos/47deg/github4s/issues/48/labels{/name}",
| "comments_url": "https://api.github.com/repos/47deg/github4s/issues/48/comments",
| "events_url": "https://api.github.com/repos/47deg/github4s/issues/48/events",
| "html_url": "https://github.com/47deg/github4s/issues/48",
| "id": 198471647,
| "number": 48,
| "title": "Sample Title",
| "user": {
| "login": "fedefernandez",
| "id": 720923,
| "avatar_url": "https://avatars0.githubusercontent.com/u/720923?v=4",
| "gravatar_id": "",
| "url": "https://api.github.com/users/fedefernandez",
| "html_url": "https://github.com/fedefernandez",
| "followers_url": "https://api.github.com/users/fedefernandez/followers",
| "following_url": "https://api.github.com/users/fedefernandez/following{/other_user}",
| "gists_url": "https://api.github.com/users/fedefernandez/gists{/gist_id}",
| "starred_url": "https://api.github.com/users/fedefernandez/starred{/owner}{/repo}",
| "subscriptions_url": "https://api.github.com/users/fedefernandez/subscriptions",
| "organizations_url": "https://api.github.com/users/fedefernandez/orgs",
| "repos_url": "https://api.github.com/users/fedefernandez/repos",
| "events_url": "https://api.github.com/users/fedefernandez/events{/privacy}",
| "received_events_url": "https://api.github.com/users/fedefernandez/received_events",
| "type": "User",
| "site_admin": false
| },
| "labels": [
| {
| "id": 337667889,
| "url": "https://api.github.com/repos/47deg/github4s/labels/bug",
| "name": "bug",
| "color": "fc2929",
| "default": true
| },
| {
| "id": 511000525,
| "url": "https://api.github.com/repos/47deg/github4s/labels/code%20review",
| "name": "code review",
| "color": "ededed",
| "default": false
| }
| ],
| "state": "closed",
| "locked": false,
| "assignee": {
| "login": "rafaparadela",
| "id": 315070,
| "avatar_url": "https://avatars3.githubusercontent.com/u/315070?v=4",
| "gravatar_id": "",
| "url": "https://api.github.com/users/rafaparadela",
| "html_url": "https://github.com/rafaparadela",
| "followers_url": "https://api.github.com/users/rafaparadela/followers",
| "following_url": "https://api.github.com/users/rafaparadela/following{/other_user}",
| "gists_url": "https://api.github.com/users/rafaparadela/gists{/gist_id}",
| "starred_url": "https://api.github.com/users/rafaparadela/starred{/owner}{/repo}",
| "subscriptions_url": "https://api.github.com/users/rafaparadela/subscriptions",
| "organizations_url": "https://api.github.com/users/rafaparadela/orgs",
| "repos_url": "https://api.github.com/users/rafaparadela/repos",
| "events_url": "https://api.github.com/users/rafaparadela/events{/privacy}",
| "received_events_url": "https://api.github.com/users/rafaparadela/received_events",
| "type": "User",
| "site_admin": false
| },
| "assignees": [
| {
| "login": "rafaparadela",
| "id": 315070,
| "avatar_url": "https://avatars3.githubusercontent.com/u/315070?v=4",
| "gravatar_id": "",
| "url": "https://api.github.com/users/rafaparadela",
| "html_url": "https://github.com/rafaparadela",
| "followers_url": "https://api.github.com/users/rafaparadela/followers",
| "following_url": "https://api.github.com/users/rafaparadela/following{/other_user}",
| "gists_url": "https://api.github.com/users/rafaparadela/gists{/gist_id}",
| "starred_url": "https://api.github.com/users/rafaparadela/starred{/owner}{/repo}",
| "subscriptions_url": "https://api.github.com/users/rafaparadela/subscriptions",
| "organizations_url": "https://api.github.com/users/rafaparadela/orgs",
| "repos_url": "https://api.github.com/users/rafaparadela/repos",
| "events_url": "https://api.github.com/users/rafaparadela/events{/privacy}",
| "received_events_url": "https://api.github.com/users/rafaparadela/received_events",
| "type": "User",
| "site_admin": false
| }
| ],
| "milestone": null,
| "comments": 2,
| "created_at": "2017-01-03T13:12:55Z",
| "updated_at": "2018-01-09T18:45:33Z",
| "closed_at": "2017-01-03T13:13:30Z",
| "author_association": "CONTRIBUTOR",
| "body": "Sample Body",
| "closed_by": {
| "login": "fedefernandez",
| "id": 720923,
| "avatar_url": "https://avatars0.githubusercontent.com/u/720923?v=4",
| "gravatar_id": "",
| "url": "https://api.github.com/users/fedefernandez",
| "html_url": "https://github.com/fedefernandez",
| "followers_url": "https://api.github.com/users/fedefernandez/followers",
| "following_url": "https://api.github.com/users/fedefernandez/following{/other_user}",
| "gists_url": "https://api.github.com/users/fedefernandez/gists{/gist_id}",
| "starred_url": "https://api.github.com/users/fedefernandez/starred{/owner}{/repo}",
| "subscriptions_url": "https://api.github.com/users/fedefernandez/subscriptions",
| "organizations_url": "https://api.github.com/users/fedefernandez/orgs",
| "repos_url": "https://api.github.com/users/fedefernandez/repos",
| "events_url": "https://api.github.com/users/fedefernandez/events{/privacy}",
| "received_events_url": "https://api.github.com/users/fedefernandez/received_events",
| "type": "User",
| "site_admin": false
| }
|}
""".stripMargin


val listReviewsValidResponse =
"""
|[
Expand Down