Skip to content

Commit eabba6c

Browse files
committed
[WIP] #3 - issueAPI
Added the last missing method listRepositoryIssue. Implemented tests for create, get, and edit issue, but not done tests for listing and filtering issues. listRepositoryIssue method works but untested.
1 parent d24f21d commit eabba6c

File tree

4 files changed

+248
-27
lines changed

4 files changed

+248
-27
lines changed

src/main/scala/codecheck/github/models/Issue.scala

+60-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import org.json4s.JValue
44
import org.json4s.JString
55
import org.json4s.JNothing
66
import org.json4s.JNull
7+
import org.json4s.JInt
78
import org.json4s.JArray
89
import org.json4s.JsonDSL._
910
import org.joda.time.DateTime
@@ -54,6 +55,26 @@ object IssueSort {
5455
def fromString(str: String) = values.filter(_.name == str).head
5556
}
5657

58+
sealed abstract class MilestoneSearchOption {
59+
60+
}
61+
62+
object MilestoneSearchOption {
63+
case object all extends MilestoneSearchOption {
64+
val value = JString("*")
65+
}
66+
67+
case object none extends MilestoneSearchOption {
68+
val value = JString("none")
69+
}
70+
71+
case class Specified(number: Int) extends MilestoneSearchOption {
72+
val value = JInt(number)
73+
}
74+
75+
def apply(number: Int) = Specified(number)
76+
}
77+
5778
case class IssueListOption(
5879
filter: IssueFilter = IssueFilter.assigned,
5980
state: IssueState = IssueState.open,
@@ -67,7 +88,27 @@ case class IssueListOption(
6788
since.map("&since=" + _.toString("yyyy-MM-dd'T'HH:mm:ssZ"))
6889
}
6990

70-
/*case*/ class IssueListOption4Repository extends ToDo
91+
case class IssueListOption4Repository(
92+
milestone: Option[MilestoneSearchOption] = None,
93+
state: IssueState = IssueState.open,
94+
assignee: Option[String] = None,
95+
creator: Option[String] = None,
96+
mentioned: Option[String] = None,
97+
labels: Seq[String] = Nil,
98+
sort: IssueSort = IssueSort.created,
99+
direction: SortDirection = SortDirection.desc,
100+
since: Option[DateTime] = None
101+
) {
102+
def q: String = "?" + (if (!milestone.isEmpty) milestone map (t => s"milestone=$t&")) +
103+
s"state=$state" +
104+
(if (!assignee.isEmpty) assignee map (t => s"&assignee=$t")) +
105+
(if (!creator.isEmpty) creator map (t => s"&creator=$t")) +
106+
(if (!mentioned.isEmpty) mentioned map (t => s"&mentioned=$t")) +
107+
(if (!labels.isEmpty) "&labels=" + labels.mkString(",")) +
108+
s"&sort=$sort" +
109+
s"&direction=$direction" +
110+
(if (!since.isEmpty) since map ("&since=" + _.toString("yyyy-MM-dd'T'HH:mm:ssZ")))
111+
}
71112

72113
case class IssueInput(
73114
title: Option[String] = None,
@@ -96,23 +137,36 @@ object IssueInput {
96137
def apply(title: String, body: Option[String], assignee: Option[String], milestone: Option[Int], labels: Seq[String]): IssueInput =
97138
IssueInput(Some(title), body, assignee, milestone, labels, None)
98139
}
140+
99141
case class Issue(value: JValue) extends AbstractJson(value) {
142+
def url = get("url")
143+
def labels_url = get("labels_url")
144+
def comments_url = get("comments_url")
145+
def events_url = get("events_url")
146+
def html_url = get("html_url")
147+
def id = get("id").toLong
100148
def number = get("number").toLong
101149
def title = get("title")
102-
def body = opt("body")
103-
104-
lazy val assignee = objectOpt("assignee")(v => User(v))
105-
lazy val milestone = objectOpt("milestone")(v => Milestone(v))
106150

107151
lazy val user = new User(value \ "user")
108152
lazy val labels = (value \ "labels") match {
109153
case JArray(arr) => arr.map(new Label(_))
110154
case _ => Nil
111155
}
112-
lazy val repository = new Repository(value \ "repository")
156+
157+
def state = get("state")
158+
def locked = boolean("locked")
159+
160+
lazy val assignee = objectOpt("assignee")(v => User(v))
161+
lazy val milestone = objectOpt("milestone")(v => Milestone(v))
113162

114163
def comments = get("comments").toInt
115164
def created_at = getDate("created_at")
116165
def updated_at = getDate("updated_at")
117166
def closed_at = dateOpt("closed_at")
167+
def body = opt("body")
168+
169+
lazy val closed_by = objectOpt("closed_by")(v => User(v))
170+
171+
lazy val repository = new Repository(value \ "repository")
118172
}

src/main/scala/codecheck/github/operations/IssueOp.scala

+7-8
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,32 @@ import codecheck.github.models.Issue
1414
import codecheck.github.models.IssueListOption
1515
import codecheck.github.models.IssueListOption4Repository
1616

17-
import codecheck.github.utils.ToDo
18-
1917
trait IssueOp {
2018
self: GitHubAPI =>
2119

2220
private def doList(path: String): Future[List[Issue]] = {
23-
exec("GET", path).map(
21+
exec("GET", path).map(
2422
_.body match {
2523
case JArray(arr) => arr.map(v => Issue(v))
2624
case _ => throw new IllegalStateException()
2725
}
2826
)
2927
}
3028

31-
def listAllIssues(option: IssueListOption = IssueListOption()): Future[List[Issue]] =
29+
def listAllIssues(option: IssueListOption = IssueListOption()): Future[List[Issue]] =
3230
doList("/issues" + option.q)
3331

34-
def listUserIssues(option: IssueListOption = IssueListOption()): Future[List[Issue]] =
32+
def listUserIssues(option: IssueListOption = IssueListOption()): Future[List[Issue]] =
3533
doList("/user/issues" + option.q)
3634

3735
def listOrgIssues(org: String, option: IssueListOption = IssueListOption()): Future[List[Issue]] =
3836
doList(s"/orgs/$org/issues" + option.q)
3937

40-
def listRepositoryIssues(owner: String, repo: String, option: IssueListOption4Repository): Future[List[Issue]] = ToDo[Future[List[Issue]]]
38+
def listRepositoryIssues(owner: String, repo: String, option: IssueListOption4Repository = IssueListOption4Repository()): Future[List[Issue]] =
39+
doList(s"/repos/$owner/$repo/issues" + option.q)
4140

42-
def getIssue(owner: String, repo: String, number: Long): Future[Option[Issue]] =
43-
exec("GET", s"/repos/$owner/$repo/issues/$number", fail404=false).map(res =>
41+
def getIssue(owner: String, repo: String, number: Long): Future[Option[Issue]] =
42+
exec("GET", s"/repos/$owner/$repo/issues/$number", fail404=false).map(res =>
4443
res.statusCode match {
4544
case 404 => None
4645
case 200 => Some(Issue(res.body))

src/test/scala/IssueOpSpec.scala

+163-7
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,176 @@
1+
12
import org.scalatest.FunSpec
23
import scala.concurrent.Await
4+
import org.joda.time.DateTime
5+
import org.joda.time.DateTimeZone
6+
7+
import codecheck.github.models.IssueListOption
8+
import codecheck.github.models.IssueFilter
9+
import codecheck.github.models.IssueListOption4Repository
10+
import codecheck.github.models.IssueState
11+
import codecheck.github.models.Issue
12+
import codecheck.github.models.IssueInput
313

414
class IssueOpSpec extends FunSpec with Constants {
515

616
val number = 1
17+
var nUser: Long = 0
18+
var nOrg: Long = 0
19+
var createdUser: DateTime = DateTime.now
20+
var createdOrg: DateTime = DateTime.now
721

8-
describe("assign operations") {
9-
it("assign should succeed") {
10-
val result = Await.result(api.assign(organization, repo, number, user), TIMEOUT)
11-
showResponse(result)
12-
assert(result.get("assignee.login") == user)
22+
describe("createIssue(owner, repo, input)") {
23+
val input = IssueInput(Some("test issue"), Some("testing"), Some(user), Some(1), Seq("question"))
24+
25+
it("should create issue for user's own repo.") {
26+
val result = Await.result(api.createIssue(user, userRepo, input), TIMEOUT)
27+
//showResponse(result)
28+
nUser = result.number
29+
assert(result.url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser)
30+
assert(result.labels_url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser + "/labels{/name}")
31+
assert(result.comments_url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser + "/comments")
32+
assert(result.events_url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser + "/events")
33+
assert(result.html_url == "https://github.com/" + user + "/" + userRepo + "/issues/" + nUser)
34+
assert(result.title == "test issue")
35+
assert(result.user.login == user)
36+
assert(result.labels.head.name == "question")
37+
assert(result.state == "open")
38+
assert(result.locked == false)
39+
assert(result.assignee.get.login == user)
40+
assert(result.milestone.get.number == 1)
41+
assert(result.comments == 0)
42+
assert(result.created_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000)
43+
assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000)
44+
createdUser = result.created_at
45+
assert(result.closed_at.isEmpty)
46+
assert(result.body.get == "testing")
47+
assert(result.closed_by.isEmpty)
48+
}
49+
50+
it("should create issue for organization's repo.") {
51+
val result = Await.result(api.createIssue(organization, repo, input), TIMEOUT)
52+
nOrg = result.number
53+
assert(result.url == "https://api.github.com/repos/" + organization + "/" + repo + "/issues/" + nOrg)
54+
assert(result.labels_url == "https://api.github.com/repos/" + organization + "/" + repo + "/issues/" + nOrg + "/labels{/name}")
55+
assert(result.comments_url == "https://api.github.com/repos/" + organization + "/" + repo + "/issues/" + nOrg + "/comments")
56+
assert(result.events_url == "https://api.github.com/repos/" + organization + "/" + repo + "/issues/" + nOrg + "/events")
57+
assert(result.html_url == "https://github.com/" + organization + "/" + repo + "/issues/" + nOrg)
58+
assert(result.title == "test issue")
59+
assert(result.user.login == user)
60+
assert(result.labels.head.name == "question")
61+
assert(result.state == "open")
62+
assert(result.locked == false)
63+
assert(result.assignee.get.login == user)
64+
assert(result.milestone.get.number == 1)
65+
assert(result.comments == 0)
66+
assert(result.created_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000)
67+
assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000)
68+
createdOrg = result.created_at
69+
assert(result.closed_at.isEmpty)
70+
assert(result.body.get == "testing")
71+
assert(result.closed_by.isEmpty)
1372
}
73+
}
74+
75+
describe("getIssue(owner, repo, number)") {
76+
it("should return issue from user's own repo.") {
77+
val result = Await.result(api.getIssue(user, userRepo, nUser), TIMEOUT)
78+
assert(result.get.title == "test issue")
79+
}
80+
81+
it("should return issue from organization's repo.") {
82+
val result = Await.result(api.getIssue(organization, repo, nOrg), TIMEOUT)
83+
assert(result.get.title == "test issue")
84+
}
85+
}
1486

15-
it("unassign should succeed") {
16-
val result = Await.result(api.unassign(organization, repo, number), TIMEOUT)
87+
describe("unassign(owner, repo, number)") {
88+
it("should succeed with valid inputs on issues in user's own repo.") {
89+
val result = Await.result(api.unassign(user, userRepo, nUser), TIMEOUT)
1790
assert(result.opt("assignee").isEmpty)
1891
}
92+
93+
it("should succeed with valid inputs on issues in organization's repo.") {
94+
val result = Await.result(api.unassign(organization, repo, nOrg), TIMEOUT)
95+
assert(result.opt("assignee").isEmpty)
96+
}
97+
}
98+
99+
describe("assign(owner, repo, number, assignee)") {
100+
it("should succeed with valid inputs on issues in user's own repo.") {
101+
val result = Await.result(api.assign(user, userRepo, nUser, user), TIMEOUT)
102+
assert(result.get("assignee.login") == user)
103+
}
104+
105+
it("should succeed with valid inputs on issues in organization's repo.") {
106+
val result = Await.result(api.assign(organization, repo, nOrg, user), TIMEOUT)
107+
assert(result.get("assignee.login") == user)
108+
}
109+
}
110+
111+
describe("listAllIssues(option)") {
112+
it("shold return at least one issue.") {
113+
val result = Await.result(api.listAllIssues(), TIMEOUT)
114+
assert(result.length > 0)
115+
}
116+
117+
it("shold return only one issue.") {
118+
val option = IssueListOption(IssueFilter.all, IssueState.open, since=Some(createdUser))
119+
val result = Await.result(api.listAllIssues(option), TIMEOUT)
120+
assert(result.length == 1)
121+
assert(result.head.title == "test issue")
122+
}
123+
}
124+
125+
describe("listUserIssues(option)") {
126+
it("shold return at least one issue.") {
127+
val result = Await.result(api.listUserIssues(), TIMEOUT)
128+
assert(result.length > 0)
129+
}
130+
}
131+
132+
describe("listOrgIssues(org, option)") {
133+
it("should return at least one issue.") {
134+
val result = Await.result(api.listOrgIssues(organization), TIMEOUT)
135+
assert(result.length > 0)
136+
}
137+
}
138+
139+
describe("listRepositoryIssues(owner, repo, option)") {
140+
it("should return at least one issue.") {
141+
val result = Await.result(api.listRepositoryIssues(organization, repo), TIMEOUT)
142+
assert(result.length > 0)
143+
}
144+
}
145+
146+
describe("listRepositoryIssues") {
147+
it("is just testing.") {
148+
val input = new IssueListOption4Repository(state=IssueState.all)
149+
val result = Await.result(api.listRepositoryIssues(organization, repo, input), TIMEOUT)
150+
}
151+
}
152+
153+
describe("editIssue(owner, repo, number, input)") {
154+
val input = IssueInput(Some("test issue edited"), Some("testing again"), Some(user), Some(2), Seq("question", "bug"), Some(IssueState.closed))
155+
156+
it("should edit the issue in user's own repo.") {
157+
val result = Await.result(api.editIssue(user, userRepo, nUser, input), TIMEOUT)
158+
assert(result.title == "test issue edited")
159+
assert(result.body.get == "testing again")
160+
assert(result.milestone.get.number == 2)
161+
assert(result.labels.head.name == "bug")
162+
assert(result.state == "closed")
163+
assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000)
164+
}
165+
166+
it("should edit the issue in organization's repo.") {
167+
val result = Await.result(api.editIssue(organization, repo, nOrg, input), TIMEOUT)
168+
assert(result.title == "test issue edited")
169+
assert(result.body.get == "testing again")
170+
assert(result.milestone.get.number == 2)
171+
assert(result.labels.head.name == "bug")
172+
assert(result.state == "closed")
173+
assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000)
174+
}
19175
}
20176
}

src/test/scala/MilestoneOpSpec.scala

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import org.scalatest.path.FunSpec
2+
import org.scalatest.BeforeAndAfter
23
import codecheck.github.exceptions.NotFoundException
34
import codecheck.github.models.Milestone
45
import codecheck.github.models.MilestoneInput
@@ -11,10 +12,21 @@ import scala.concurrent.Await
1112
import scala.concurrent.ExecutionContext.Implicits.global
1213
import org.joda.time.DateTime
1314

14-
class MilestoneOpSpec extends FunSpec
15-
with Constants
16-
{
15+
class MilestoneOpSpec extends FunSpec with Constants {
16+
/*
17+
before { }
1718
19+
after {
20+
val input = new MilestoneInput(Some("test milestone"))
21+
val input2 = new MilestoneInput(Some("test milestone 2"))
22+
23+
Await.result(api.createMilestone(user, userRepo, input), TIMEOUT)
24+
Await.result(api.createMilestone(user, userRepo, input2), TIMEOUT)
25+
26+
Await.result(api.createMilestone(organization, repo, input), TIMEOUT)
27+
Await.result(api.createMilestone(organization, repo, input2), TIMEOUT)
28+
}
29+
*/
1830
private def removeAll = {
1931
val list = Await.result(api.listMilestones(organization, repo, MilestoneListOption(state=MilestoneState.all)), TIMEOUT)
2032
list.foreach { m =>
@@ -67,7 +79,7 @@ class MilestoneOpSpec extends FunSpec
6779
val input = MilestoneInput(gName, gDescription, d1)
6880
val ex = Await.result(api.createMilestone(organization, repoInvalid, input).failed, TIMEOUT)
6981
ex match {
70-
case e: NotFoundException =>
82+
case e: NotFoundException =>
7183
case _ => fail
7284
}
7385
}
@@ -168,7 +180,7 @@ class MilestoneOpSpec extends FunSpec
168180
it("with wrong reponame should fail") {
169181
val ex = Await.result(api.listMilestones(organization, repoInvalid).failed, TIMEOUT)
170182
ex match {
171-
case e: NotFoundException =>
183+
case e: NotFoundException =>
172184
case _ => fail
173185
}
174186
}
@@ -189,7 +201,7 @@ class MilestoneOpSpec extends FunSpec
189201

190202
val ex = Await.result(api.removeMilestone(organization, repo, m1.number).failed, TIMEOUT)
191203
ex match {
192-
case e: NotFoundException =>
204+
case e: NotFoundException =>
193205
case _ => fail
194206
}
195207
}

0 commit comments

Comments
 (0)