Skip to content

Commit f2c07d3

Browse files
committed
Merge pull request #44 from code-check/3-issueAPI
3 issue api
2 parents ed42a60 + 9f33cc8 commit f2c07d3

File tree

5 files changed

+281
-30
lines changed

5 files changed

+281
-30
lines changed

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

+54-8
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,18 @@ object IssueSort {
5455
def fromString(str: String) = values.filter(_.name == str).head
5556
}
5657

58+
sealed abstract class MilestoneSearchOption(val name: String) {
59+
override def toString = name
60+
}
61+
62+
object MilestoneSearchOption {
63+
case object all extends MilestoneSearchOption("*")
64+
case object none extends MilestoneSearchOption("none")
65+
case class Specified(number: Int) extends MilestoneSearchOption(number.toString())
66+
67+
def apply(number: Int) = Specified(number)
68+
}
69+
5770
case class IssueListOption(
5871
filter: IssueFilter = IssueFilter.assigned,
5972
state: IssueState = IssueState.open,
@@ -63,11 +76,31 @@ case class IssueListOption(
6376
since: Option[DateTime] = None
6477
) {
6578
def q = s"?filter=$filter&state=$state&sort=$sort&direction=$direction" +
66-
(if (labels.length == 0) "" else "&labels=" + labels.mkString(",")) +
67-
since.map("&since=" + _.toString("yyyy-MM-dd'T'HH:mm:ssZ"))
79+
(if (!labels.isEmpty) "&labels=" + labels.mkString(",") else "") +
80+
(if (!since.isEmpty) (since map ("&since=" + _.toString("yyyy-MM-dd'T'HH:mm:ss'Z'"))).get else "")
6881
}
6982

70-
/*case*/ class IssueListOption4Repository extends ToDo
83+
case class IssueListOption4Repository(
84+
milestone: Option[MilestoneSearchOption] = None,
85+
state: IssueState = IssueState.open,
86+
assignee: Option[String] = None,
87+
creator: Option[String] = None,
88+
mentioned: Option[String] = None,
89+
labels: Seq[String] = Nil,
90+
sort: IssueSort = IssueSort.created,
91+
direction: SortDirection = SortDirection.desc,
92+
since: Option[DateTime] = None
93+
) {
94+
def q = "?" + (if (!milestone.isEmpty) (milestone map (t => s"milestone=$t&")).get else "") +
95+
s"state=$state" +
96+
(if (!assignee.isEmpty) (assignee map (t => s"&assignee=$t")).get else "") +
97+
(if (!creator.isEmpty) (creator map (t => s"&creator=$t")).get else "") +
98+
(if (!mentioned.isEmpty) (mentioned map (t => s"&mentioned=$t")).get else "") +
99+
(if (!labels.isEmpty) "&labels=" + labels.mkString(",") else "") +
100+
s"&sort=$sort" +
101+
s"&direction=$direction" +
102+
(if (!since.isEmpty) (since map ("&since=" + _.toString("yyyy-MM-dd'T'HH:mm:ss'Z'"))).get else "")
103+
}
71104

72105
case class IssueInput(
73106
title: Option[String] = None,
@@ -96,23 +129,36 @@ object IssueInput {
96129
def apply(title: String, body: Option[String], assignee: Option[String], milestone: Option[Int], labels: Seq[String]): IssueInput =
97130
IssueInput(Some(title), body, assignee, milestone, labels, None)
98131
}
132+
99133
case class Issue(value: JValue) extends AbstractJson(value) {
134+
def url = get("url")
135+
def labels_url = get("labels_url")
136+
def comments_url = get("comments_url")
137+
def events_url = get("events_url")
138+
def html_url = get("html_url")
139+
def id = get("id").toLong
100140
def number = get("number").toLong
101141
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))
106142

107143
lazy val user = new User(value \ "user")
108144
lazy val labels = (value \ "labels") match {
109145
case JArray(arr) => arr.map(new Label(_))
110146
case _ => Nil
111147
}
112-
lazy val repository = new Repository(value \ "repository")
148+
149+
def state = get("state")
150+
def locked = boolean("locked")
151+
152+
lazy val assignee = objectOpt("assignee")(v => User(v))
153+
lazy val milestone = objectOpt("milestone")(v => Milestone(v))
113154

114155
def comments = get("comments").toInt
115156
def created_at = getDate("created_at")
116157
def updated_at = getDate("updated_at")
117158
def closed_at = dateOpt("closed_at")
159+
def body = opt("body")
160+
161+
lazy val closed_by = objectOpt("closed_by")(v => User(v))
162+
163+
lazy val repository = new Repository(value \ "repository")
118164
}

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

+8-8
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,33 @@ 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+
//Only listAll/User/OrgIssues return Repository object
30+
def listAllIssues(option: IssueListOption = IssueListOption()): Future[List[Issue]] =
3231
doList("/issues" + option.q)
3332

34-
def listUserIssues(option: IssueListOption = IssueListOption()): Future[List[Issue]] =
33+
def listUserIssues(option: IssueListOption = IssueListOption()): Future[List[Issue]] =
3534
doList("/user/issues" + option.q)
3635

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

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

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 =>
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 =>
4444
res.statusCode match {
4545
case 404 => None
4646
case 200 => Some(Issue(res.body))

src/test/scala/Constants.scala

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import com.ning.http.client.AsyncHttpClient
32
import codecheck.github.api.GitHubAPI
43
import scala.concurrent.duration._

src/test/scala/IssueOpSpec.scala

+214-8
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,226 @@
11
import org.scalatest.FunSpec
2+
import org.scalatest.BeforeAndAfterAll
23
import scala.concurrent.Await
4+
import org.joda.time.DateTime
5+
import org.joda.time.DateTimeZone
36

4-
class IssueOpSpec extends FunSpec with Constants {
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
13+
import codecheck.github.models.MilestoneSearchOption
14+
15+
import codecheck.github.models.MilestoneInput
16+
import codecheck.github.models.MilestoneListOption
17+
import codecheck.github.models.MilestoneState
18+
import codecheck.github.models.Milestone
19+
20+
class IssueOpSpec extends FunSpec with Constants with BeforeAndAfterAll {
521

622
val number = 1
23+
var nUser: Long = 0
24+
var nOrg: Long = 0
25+
var nTime: DateTime = DateTime.now().toDateTime(DateTimeZone.UTC)
26+
val tRepo = repo + "2"
727

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)
28+
override def beforeAll() {
29+
val userMilestones = Await.result(api.listMilestones(user, userRepo, MilestoneListOption(state=MilestoneState.all)), TIMEOUT)
30+
userMilestones.foreach { m =>
31+
Await.result(api.removeMilestone(user, userRepo, m.number), TIMEOUT)
32+
}
33+
34+
val orgMilestones = Await.result(api.listMilestones(organization, tRepo, MilestoneListOption(state=MilestoneState.all)), TIMEOUT)
35+
orgMilestones.foreach { m =>
36+
Await.result(api.removeMilestone(organization, tRepo, m.number), TIMEOUT)
37+
}
38+
39+
val nInput = new MilestoneInput(Some("test milestone"))
40+
val nInput2 = new MilestoneInput(Some("test milestone 2"))
41+
42+
Await.result(api.createMilestone(user, userRepo, nInput), TIMEOUT)
43+
Await.result(api.createMilestone(user, userRepo, nInput2), TIMEOUT)
44+
45+
Await.result(api.createMilestone(organization, tRepo, nInput), TIMEOUT)
46+
Await.result(api.createMilestone(organization, tRepo, nInput2), TIMEOUT)
47+
}
48+
49+
describe("createIssue(owner, repo, input)") {
50+
val input = IssueInput(Some("test issue"), Some("testing"), Some(user), Some(1), Seq("question"))
51+
52+
it("should create issue for user's own repo.") {
53+
val result = Await.result(api.createIssue(user, userRepo, input), TIMEOUT)
54+
nUser = result.number
55+
assert(result.url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser)
56+
assert(result.labels_url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser + "/labels{/name}")
57+
assert(result.comments_url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser + "/comments")
58+
assert(result.events_url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser + "/events")
59+
assert(result.html_url == "https://github.com/" + user + "/" + userRepo + "/issues/" + nUser)
60+
assert(result.title == "test issue")
61+
assert(result.user.login == user)
62+
assert(result.labels.head.name == "question")
63+
assert(result.state == "open")
64+
assert(result.locked == false)
65+
assert(result.assignee.get.login == user)
66+
assert(result.milestone.get.number == 1)
67+
assert(result.comments == 0)
68+
assert(result.created_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000)
69+
assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000)
70+
assert(result.closed_at.isEmpty)
71+
assert(result.body.get == "testing")
72+
assert(result.closed_by.isEmpty)
73+
}
74+
75+
it("should create issue for organization's repo.") {
76+
val result = Await.result(api.createIssue(organization, tRepo, input), TIMEOUT)
77+
nOrg = result.number
78+
assert(result.url == "https://api.github.com/repos/" + organization + "/" + tRepo + "/issues/" + nOrg)
79+
assert(result.labels_url == "https://api.github.com/repos/" + organization + "/" + tRepo + "/issues/" + nOrg + "/labels{/name}")
80+
assert(result.comments_url == "https://api.github.com/repos/" + organization + "/" + tRepo+ "/issues/" + nOrg + "/comments")
81+
assert(result.events_url == "https://api.github.com/repos/" + organization + "/" + tRepo + "/issues/" + nOrg + "/events")
82+
assert(result.html_url == "https://github.com/" + organization + "/" + tRepo + "/issues/" + nOrg)
83+
assert(result.title == "test issue")
84+
assert(result.user.login == user)
85+
assert(result.labels.head.name == "question")
86+
assert(result.state == "open")
87+
assert(result.locked == false)
88+
assert(result.assignee.get.login == user)
89+
assert(result.milestone.get.number == 1)
90+
assert(result.comments == 0)
91+
assert(result.created_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000)
92+
assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000)
93+
assert(result.body.get == "testing")
94+
assert(result.closed_by.isEmpty)
95+
}
96+
}
97+
98+
describe("getIssue(owner, repo, number)") {
99+
it("should return issue from user's own repo.") {
100+
val result = Await.result(api.getIssue(user, userRepo, nUser), TIMEOUT)
101+
assert(result.get.title == "test issue")
102+
}
103+
104+
it("should return issue from organization's repo.") {
105+
val result = Await.result(api.getIssue(organization, tRepo, nOrg), TIMEOUT)
106+
assert(result.get.title == "test issue")
107+
}
108+
}
109+
110+
describe("unassign(owner, repo, number)") {
111+
it("should succeed with valid inputs on issues in user's own repo.") {
112+
val result = Await.result(api.unassign(user, userRepo, nUser), TIMEOUT)
113+
assert(result.opt("assignee").isEmpty)
13114
}
14115

15-
it("unassign should succeed") {
16-
val result = Await.result(api.unassign(organization, repo, number), TIMEOUT)
116+
it("should succeed with valid inputs on issues in organization's repo.") {
117+
val result = Await.result(api.unassign(organization, tRepo, nOrg), TIMEOUT)
17118
assert(result.opt("assignee").isEmpty)
18119
}
19120
}
121+
122+
describe("assign(owner, repo, number, assignee)") {
123+
it("should succeed with valid inputs on issues in user's own repo.") {
124+
val result = Await.result(api.assign(user, userRepo, nUser, user), TIMEOUT)
125+
assert(result.get("assignee.login") == user)
126+
}
127+
128+
it("should succeed with valid inputs on issues in organization's repo.") {
129+
val result = Await.result(api.assign(organization, tRepo, nOrg, user), TIMEOUT)
130+
assert(result.get("assignee.login") == user)
131+
}
132+
}
133+
134+
describe("listAllIssues(option)") {
135+
it("shold return at least one issue.") {
136+
val result = Await.result(api.listAllIssues(), TIMEOUT)
137+
assert(result.length > 0)
138+
}
139+
140+
it("shold return only two issues when using options.") {
141+
val option = IssueListOption(IssueFilter.created, IssueState.open, Seq("question"), since=Some(nTime))
142+
val result = Await.result(api.listAllIssues(option), TIMEOUT)
143+
assert(result.length == 2)
144+
assert(result.head.title == "test issue")
145+
}
146+
}
147+
148+
describe("listUserIssues(option)") {
149+
it("shold return at least one issue.") {
150+
val result = Await.result(api.listUserIssues(), TIMEOUT)
151+
assert(result.length > 0)
152+
}
153+
154+
it("shold return only one issues when using options.") {
155+
val option = IssueListOption(IssueFilter.created, IssueState.open, Seq("question"), since=Some(nTime))
156+
val result = Await.result(api.listUserIssues(option), TIMEOUT)
157+
assert(result.length == 1)
158+
assert(result.head.title == "test issue")
159+
}
160+
}
161+
162+
describe("listOrgIssues(org, option)") {
163+
it("should return at least one issue.") {
164+
val result = Await.result(api.listOrgIssues(organization), TIMEOUT)
165+
assert(result.length > 0)
166+
}
167+
168+
it("shold return only one issues when using options.") {
169+
val option = IssueListOption(IssueFilter.created, IssueState.open, Seq("question"), since=Some(nTime))
170+
val result = Await.result(api.listOrgIssues(organization, option), TIMEOUT)
171+
assert(result.length == 1)
172+
assert(result.head.title == "test issue")
173+
}
174+
}
175+
176+
describe("listRepositoryIssues(owner, repo, option)") {
177+
it("should return at least one issue from user's own repo.") {
178+
val result = Await.result(api.listRepositoryIssues(user, userRepo), TIMEOUT)
179+
assert(result.length > 0)
180+
}
181+
182+
it("should return at least one issue from organization's repo.") {
183+
val result = Await.result(api.listRepositoryIssues(organization, tRepo), TIMEOUT)
184+
assert(result.length > 0)
185+
}
186+
187+
it("should return only one issue from user's own repo when using options.") {
188+
val option = new IssueListOption4Repository(Some(MilestoneSearchOption(1)), IssueState.open, Some(user), Some(user), labels=Seq("question"), since=Some(nTime))
189+
val result = Await.result(api.listRepositoryIssues(user, userRepo, option), TIMEOUT)
190+
//showResponse(option.q)
191+
assert(result.length == 1)
192+
assert(result.head.title == "test issue")
193+
}
194+
195+
it("should return only one issue from organization's repo when using options.") {
196+
val option = new IssueListOption4Repository(Some(MilestoneSearchOption(1)), IssueState.open, Some(user), Some(user), labels=Seq("question"), since=Some(nTime))
197+
val result = Await.result(api.listRepositoryIssues(organization, tRepo, option), TIMEOUT)
198+
assert(result.length == 1)
199+
assert(result.head.title == "test issue")
200+
}
201+
}
202+
203+
describe("editIssue(owner, repo, number, input)") {
204+
val input = IssueInput(Some("test issue edited"), Some("testing again"), Some(user), Some(2), Seq("question", "bug"), Some(IssueState.closed))
205+
206+
it("should edit the issue in user's own repo.") {
207+
val result = Await.result(api.editIssue(user, userRepo, nUser, input), TIMEOUT)
208+
assert(result.title == "test issue edited")
209+
assert(result.body.get == "testing again")
210+
assert(result.milestone.get.number == 2)
211+
assert(result.labels.head.name == "bug")
212+
assert(result.state == "closed")
213+
assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000)
214+
}
215+
216+
it("should edit the issue in organization's repo.") {
217+
val result = Await.result(api.editIssue(organization, tRepo, nOrg, input), TIMEOUT)
218+
assert(result.title == "test issue edited")
219+
assert(result.body.get == "testing again")
220+
assert(result.milestone.get.number == 2)
221+
assert(result.labels.head.name == "bug")
222+
assert(result.state == "closed")
223+
assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000)
224+
}
225+
}
20226
}

0 commit comments

Comments
 (0)