diff --git a/src/main/scala/codecheck/github/models/Issue.scala b/src/main/scala/codecheck/github/models/Issue.scala index 325ed32..ab9e0f5 100644 --- a/src/main/scala/codecheck/github/models/Issue.scala +++ b/src/main/scala/codecheck/github/models/Issue.scala @@ -4,6 +4,7 @@ import org.json4s.JValue import org.json4s.JString import org.json4s.JNothing import org.json4s.JNull +import org.json4s.JInt import org.json4s.JArray import org.json4s.JsonDSL._ import org.joda.time.DateTime @@ -54,6 +55,18 @@ object IssueSort { def fromString(str: String) = values.filter(_.name == str).head } +sealed abstract class MilestoneSearchOption(val name: String) { + override def toString = name +} + +object MilestoneSearchOption { + case object all extends MilestoneSearchOption("*") + case object none extends MilestoneSearchOption("none") + case class Specified(number: Int) extends MilestoneSearchOption(number.toString()) + + def apply(number: Int) = Specified(number) +} + case class IssueListOption( filter: IssueFilter = IssueFilter.assigned, state: IssueState = IssueState.open, @@ -63,11 +76,31 @@ case class IssueListOption( since: Option[DateTime] = None ) { def q = s"?filter=$filter&state=$state&sort=$sort&direction=$direction" + - (if (labels.length == 0) "" else "&labels=" + labels.mkString(",")) + - since.map("&since=" + _.toString("yyyy-MM-dd'T'HH:mm:ssZ")) + (if (!labels.isEmpty) "&labels=" + labels.mkString(",") else "") + + (if (!since.isEmpty) (since map ("&since=" + _.toString("yyyy-MM-dd'T'HH:mm:ss'Z'"))).get else "") } -/*case*/ class IssueListOption4Repository extends ToDo +case class IssueListOption4Repository( + milestone: Option[MilestoneSearchOption] = None, + state: IssueState = IssueState.open, + assignee: Option[String] = None, + creator: Option[String] = None, + mentioned: Option[String] = None, + labels: Seq[String] = Nil, + sort: IssueSort = IssueSort.created, + direction: SortDirection = SortDirection.desc, + since: Option[DateTime] = None +) { + def q = "?" + (if (!milestone.isEmpty) (milestone map (t => s"milestone=$t&")).get else "") + + s"state=$state" + + (if (!assignee.isEmpty) (assignee map (t => s"&assignee=$t")).get else "") + + (if (!creator.isEmpty) (creator map (t => s"&creator=$t")).get else "") + + (if (!mentioned.isEmpty) (mentioned map (t => s"&mentioned=$t")).get else "") + + (if (!labels.isEmpty) "&labels=" + labels.mkString(",") else "") + + s"&sort=$sort" + + s"&direction=$direction" + + (if (!since.isEmpty) (since map ("&since=" + _.toString("yyyy-MM-dd'T'HH:mm:ss'Z'"))).get else "") + } case class IssueInput( title: Option[String] = None, @@ -96,23 +129,36 @@ object IssueInput { def apply(title: String, body: Option[String], assignee: Option[String], milestone: Option[Int], labels: Seq[String]): IssueInput = IssueInput(Some(title), body, assignee, milestone, labels, None) } + case class Issue(value: JValue) extends AbstractJson(value) { + def url = get("url") + def labels_url = get("labels_url") + def comments_url = get("comments_url") + def events_url = get("events_url") + def html_url = get("html_url") + def id = get("id").toLong def number = get("number").toLong def title = get("title") - def body = opt("body") - - lazy val assignee = objectOpt("assignee")(v => User(v)) - lazy val milestone = objectOpt("milestone")(v => Milestone(v)) lazy val user = new User(value \ "user") lazy val labels = (value \ "labels") match { case JArray(arr) => arr.map(new Label(_)) case _ => Nil } - lazy val repository = new Repository(value \ "repository") + + def state = get("state") + def locked = boolean("locked") + + lazy val assignee = objectOpt("assignee")(v => User(v)) + lazy val milestone = objectOpt("milestone")(v => Milestone(v)) def comments = get("comments").toInt def created_at = getDate("created_at") def updated_at = getDate("updated_at") def closed_at = dateOpt("closed_at") + def body = opt("body") + + lazy val closed_by = objectOpt("closed_by")(v => User(v)) + + lazy val repository = new Repository(value \ "repository") } diff --git a/src/main/scala/codecheck/github/operations/IssueOp.scala b/src/main/scala/codecheck/github/operations/IssueOp.scala index 7cb15dd..d347299 100644 --- a/src/main/scala/codecheck/github/operations/IssueOp.scala +++ b/src/main/scala/codecheck/github/operations/IssueOp.scala @@ -14,13 +14,11 @@ import codecheck.github.models.Issue import codecheck.github.models.IssueListOption import codecheck.github.models.IssueListOption4Repository -import codecheck.github.utils.ToDo - trait IssueOp { self: GitHubAPI => private def doList(path: String): Future[List[Issue]] = { - exec("GET", path).map( + exec("GET", path).map( _.body match { case JArray(arr) => arr.map(v => Issue(v)) case _ => throw new IllegalStateException() @@ -28,19 +26,21 @@ trait IssueOp { ) } - def listAllIssues(option: IssueListOption = IssueListOption()): Future[List[Issue]] = + //Only listAll/User/OrgIssues return Repository object + def listAllIssues(option: IssueListOption = IssueListOption()): Future[List[Issue]] = doList("/issues" + option.q) - def listUserIssues(option: IssueListOption = IssueListOption()): Future[List[Issue]] = + def listUserIssues(option: IssueListOption = IssueListOption()): Future[List[Issue]] = doList("/user/issues" + option.q) def listOrgIssues(org: String, option: IssueListOption = IssueListOption()): Future[List[Issue]] = doList(s"/orgs/$org/issues" + option.q) - def listRepositoryIssues(owner: String, repo: String, option: IssueListOption4Repository): Future[List[Issue]] = ToDo[Future[List[Issue]]] + def listRepositoryIssues(owner: String, repo: String, option: IssueListOption4Repository = IssueListOption4Repository()): Future[List[Issue]] = + doList(s"/repos/$owner/$repo/issues" + option.q) - def getIssue(owner: String, repo: String, number: Long): Future[Option[Issue]] = - exec("GET", s"/repos/$owner/$repo/issues/$number", fail404=false).map(res => + def getIssue(owner: String, repo: String, number: Long): Future[Option[Issue]] = + exec("GET", s"/repos/$owner/$repo/issues/$number", fail404=false).map(res => res.statusCode match { case 404 => None case 200 => Some(Issue(res.body)) diff --git a/src/test/scala/Constants.scala b/src/test/scala/Constants.scala index 5711024..297d3e2 100644 --- a/src/test/scala/Constants.scala +++ b/src/test/scala/Constants.scala @@ -1,4 +1,3 @@ - import com.ning.http.client.AsyncHttpClient import codecheck.github.api.GitHubAPI import scala.concurrent.duration._ diff --git a/src/test/scala/IssueOpSpec.scala b/src/test/scala/IssueOpSpec.scala index f384574..533c1c0 100644 --- a/src/test/scala/IssueOpSpec.scala +++ b/src/test/scala/IssueOpSpec.scala @@ -1,20 +1,226 @@ import org.scalatest.FunSpec +import org.scalatest.BeforeAndAfterAll import scala.concurrent.Await +import org.joda.time.DateTime +import org.joda.time.DateTimeZone -class IssueOpSpec extends FunSpec with Constants { +import codecheck.github.models.IssueListOption +import codecheck.github.models.IssueFilter +import codecheck.github.models.IssueListOption4Repository +import codecheck.github.models.IssueState +import codecheck.github.models.Issue +import codecheck.github.models.IssueInput +import codecheck.github.models.MilestoneSearchOption + +import codecheck.github.models.MilestoneInput +import codecheck.github.models.MilestoneListOption +import codecheck.github.models.MilestoneState +import codecheck.github.models.Milestone + +class IssueOpSpec extends FunSpec with Constants with BeforeAndAfterAll { val number = 1 + var nUser: Long = 0 + var nOrg: Long = 0 + var nTime: DateTime = DateTime.now().toDateTime(DateTimeZone.UTC) + val tRepo = repo + "2" - describe("assign operations") { - it("assign should succeed") { - val result = Await.result(api.assign(organization, repo, number, user), TIMEOUT) - showResponse(result) - assert(result.get("assignee.login") == user) + override def beforeAll() { + val userMilestones = Await.result(api.listMilestones(user, userRepo, MilestoneListOption(state=MilestoneState.all)), TIMEOUT) + userMilestones.foreach { m => + Await.result(api.removeMilestone(user, userRepo, m.number), TIMEOUT) + } + + val orgMilestones = Await.result(api.listMilestones(organization, tRepo, MilestoneListOption(state=MilestoneState.all)), TIMEOUT) + orgMilestones.foreach { m => + Await.result(api.removeMilestone(organization, tRepo, m.number), TIMEOUT) + } + + val nInput = new MilestoneInput(Some("test milestone")) + val nInput2 = new MilestoneInput(Some("test milestone 2")) + + Await.result(api.createMilestone(user, userRepo, nInput), TIMEOUT) + Await.result(api.createMilestone(user, userRepo, nInput2), TIMEOUT) + + Await.result(api.createMilestone(organization, tRepo, nInput), TIMEOUT) + Await.result(api.createMilestone(organization, tRepo, nInput2), TIMEOUT) + } + + describe("createIssue(owner, repo, input)") { + val input = IssueInput(Some("test issue"), Some("testing"), Some(user), Some(1), Seq("question")) + + it("should create issue for user's own repo.") { + val result = Await.result(api.createIssue(user, userRepo, input), TIMEOUT) + nUser = result.number + assert(result.url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser) + assert(result.labels_url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser + "/labels{/name}") + assert(result.comments_url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser + "/comments") + assert(result.events_url == "https://api.github.com/repos/" + user + "/" + userRepo + "/issues/" + nUser + "/events") + assert(result.html_url == "https://github.com/" + user + "/" + userRepo + "/issues/" + nUser) + assert(result.title == "test issue") + assert(result.user.login == user) + assert(result.labels.head.name == "question") + assert(result.state == "open") + assert(result.locked == false) + assert(result.assignee.get.login == user) + assert(result.milestone.get.number == 1) + assert(result.comments == 0) + assert(result.created_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000) + assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000) + assert(result.closed_at.isEmpty) + assert(result.body.get == "testing") + assert(result.closed_by.isEmpty) + } + + it("should create issue for organization's repo.") { + val result = Await.result(api.createIssue(organization, tRepo, input), TIMEOUT) + nOrg = result.number + assert(result.url == "https://api.github.com/repos/" + organization + "/" + tRepo + "/issues/" + nOrg) + assert(result.labels_url == "https://api.github.com/repos/" + organization + "/" + tRepo + "/issues/" + nOrg + "/labels{/name}") + assert(result.comments_url == "https://api.github.com/repos/" + organization + "/" + tRepo+ "/issues/" + nOrg + "/comments") + assert(result.events_url == "https://api.github.com/repos/" + organization + "/" + tRepo + "/issues/" + nOrg + "/events") + assert(result.html_url == "https://github.com/" + organization + "/" + tRepo + "/issues/" + nOrg) + assert(result.title == "test issue") + assert(result.user.login == user) + assert(result.labels.head.name == "question") + assert(result.state == "open") + assert(result.locked == false) + assert(result.assignee.get.login == user) + assert(result.milestone.get.number == 1) + assert(result.comments == 0) + assert(result.created_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000) + assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000) + assert(result.body.get == "testing") + assert(result.closed_by.isEmpty) + } + } + + describe("getIssue(owner, repo, number)") { + it("should return issue from user's own repo.") { + val result = Await.result(api.getIssue(user, userRepo, nUser), TIMEOUT) + assert(result.get.title == "test issue") + } + + it("should return issue from organization's repo.") { + val result = Await.result(api.getIssue(organization, tRepo, nOrg), TIMEOUT) + assert(result.get.title == "test issue") + } + } + + describe("unassign(owner, repo, number)") { + it("should succeed with valid inputs on issues in user's own repo.") { + val result = Await.result(api.unassign(user, userRepo, nUser), TIMEOUT) + assert(result.opt("assignee").isEmpty) } - it("unassign should succeed") { - val result = Await.result(api.unassign(organization, repo, number), TIMEOUT) + it("should succeed with valid inputs on issues in organization's repo.") { + val result = Await.result(api.unassign(organization, tRepo, nOrg), TIMEOUT) assert(result.opt("assignee").isEmpty) } } + + describe("assign(owner, repo, number, assignee)") { + it("should succeed with valid inputs on issues in user's own repo.") { + val result = Await.result(api.assign(user, userRepo, nUser, user), TIMEOUT) + assert(result.get("assignee.login") == user) + } + + it("should succeed with valid inputs on issues in organization's repo.") { + val result = Await.result(api.assign(organization, tRepo, nOrg, user), TIMEOUT) + assert(result.get("assignee.login") == user) + } + } + + describe("listAllIssues(option)") { + it("shold return at least one issue.") { + val result = Await.result(api.listAllIssues(), TIMEOUT) + assert(result.length > 0) + } + + it("shold return only two issues when using options.") { + val option = IssueListOption(IssueFilter.created, IssueState.open, Seq("question"), since=Some(nTime)) + val result = Await.result(api.listAllIssues(option), TIMEOUT) + assert(result.length == 2) + assert(result.head.title == "test issue") + } + } + + describe("listUserIssues(option)") { + it("shold return at least one issue.") { + val result = Await.result(api.listUserIssues(), TIMEOUT) + assert(result.length > 0) + } + + it("shold return only one issues when using options.") { + val option = IssueListOption(IssueFilter.created, IssueState.open, Seq("question"), since=Some(nTime)) + val result = Await.result(api.listUserIssues(option), TIMEOUT) + assert(result.length == 1) + assert(result.head.title == "test issue") + } + } + + describe("listOrgIssues(org, option)") { + it("should return at least one issue.") { + val result = Await.result(api.listOrgIssues(organization), TIMEOUT) + assert(result.length > 0) + } + + it("shold return only one issues when using options.") { + val option = IssueListOption(IssueFilter.created, IssueState.open, Seq("question"), since=Some(nTime)) + val result = Await.result(api.listOrgIssues(organization, option), TIMEOUT) + assert(result.length == 1) + assert(result.head.title == "test issue") + } + } + + describe("listRepositoryIssues(owner, repo, option)") { + it("should return at least one issue from user's own repo.") { + val result = Await.result(api.listRepositoryIssues(user, userRepo), TIMEOUT) + assert(result.length > 0) + } + + it("should return at least one issue from organization's repo.") { + val result = Await.result(api.listRepositoryIssues(organization, tRepo), TIMEOUT) + assert(result.length > 0) + } + + it("should return only one issue from user's own repo when using options.") { + val option = new IssueListOption4Repository(Some(MilestoneSearchOption(1)), IssueState.open, Some(user), Some(user), labels=Seq("question"), since=Some(nTime)) + val result = Await.result(api.listRepositoryIssues(user, userRepo, option), TIMEOUT) + //showResponse(option.q) + assert(result.length == 1) + assert(result.head.title == "test issue") + } + + it("should return only one issue from organization's repo when using options.") { + val option = new IssueListOption4Repository(Some(MilestoneSearchOption(1)), IssueState.open, Some(user), Some(user), labels=Seq("question"), since=Some(nTime)) + val result = Await.result(api.listRepositoryIssues(organization, tRepo, option), TIMEOUT) + assert(result.length == 1) + assert(result.head.title == "test issue") + } + } + + describe("editIssue(owner, repo, number, input)") { + val input = IssueInput(Some("test issue edited"), Some("testing again"), Some(user), Some(2), Seq("question", "bug"), Some(IssueState.closed)) + + it("should edit the issue in user's own repo.") { + val result = Await.result(api.editIssue(user, userRepo, nUser, input), TIMEOUT) + assert(result.title == "test issue edited") + assert(result.body.get == "testing again") + assert(result.milestone.get.number == 2) + assert(result.labels.head.name == "bug") + assert(result.state == "closed") + assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000) + } + + it("should edit the issue in organization's repo.") { + val result = Await.result(api.editIssue(organization, tRepo, nOrg, input), TIMEOUT) + assert(result.title == "test issue edited") + assert(result.body.get == "testing again") + assert(result.milestone.get.number == 2) + assert(result.labels.head.name == "bug") + assert(result.state == "closed") + assert(result.updated_at.toDateTime(DateTimeZone.UTC).getMillis() - DateTime.now(DateTimeZone.UTC).getMillis() <= 5000) + } + } } diff --git a/src/test/scala/MilestoneOpSpec.scala b/src/test/scala/MilestoneOpSpec.scala index 41ab208..6da7c93 100644 --- a/src/test/scala/MilestoneOpSpec.scala +++ b/src/test/scala/MilestoneOpSpec.scala @@ -11,8 +11,8 @@ import scala.concurrent.Await import scala.concurrent.ExecutionContext.Implicits.global import org.joda.time.DateTime -class MilestoneOpSpec extends FunSpec - with Constants +class MilestoneOpSpec extends FunSpec + with Constants { private def removeAll = { @@ -67,7 +67,7 @@ class MilestoneOpSpec extends FunSpec val input = MilestoneInput(gName, gDescription, d1) val ex = Await.result(api.createMilestone(organization, repoInvalid, input).failed, TIMEOUT) ex match { - case e: NotFoundException => + case e: NotFoundException => case _ => fail } } @@ -168,7 +168,7 @@ class MilestoneOpSpec extends FunSpec it("with wrong reponame should fail") { val ex = Await.result(api.listMilestones(organization, repoInvalid).failed, TIMEOUT) ex match { - case e: NotFoundException => + case e: NotFoundException => case _ => fail } } @@ -189,7 +189,7 @@ class MilestoneOpSpec extends FunSpec val ex = Await.result(api.removeMilestone(organization, repo, m1.number).failed, TIMEOUT) ex match { - case e: NotFoundException => + case e: NotFoundException => case _ => fail } } diff --git a/src/test/scala/OraganizationOpSpec.scala b/src/test/scala/OraganizationOpSpec.scala index 0dcee7b..f8a4899 100644 --- a/src/test/scala/OraganizationOpSpec.scala +++ b/src/test/scala/OraganizationOpSpec.scala @@ -80,7 +80,7 @@ class OrganizationOpSpec extends FunSpec with Constants with BeforeAndAfter { assert(org.blog == "") assert(org.location == "Tokyo") assert(org.email == "") - assert(org.public_repos == 1) + assert(org.public_repos == 2) assert(org.public_gists == 0) assert(org.followers == 0) assert(org.following == 0)