diff --git a/server/app/com/xsn/explorer/errors/ipAddressErrors.scala b/server/app/com/xsn/explorer/errors/ipAddressErrors.scala new file mode 100644 index 0000000..96af5b2 --- /dev/null +++ b/server/app/com/xsn/explorer/errors/ipAddressErrors.scala @@ -0,0 +1,15 @@ +package com.xsn.explorer.errors + +import com.alexitc.playsonify.models.{FieldValidationError, InputValidationError, PublicError} +import play.api.i18n.{Lang, MessagesApi} + +trait IPAddressError + +case object IPAddressFormatError extends IPAddressError with InputValidationError { + + override def toPublicErrorList(messagesApi: MessagesApi)(implicit lang: Lang): List[PublicError] = { + val message = messagesApi("error.ipAddress.invalid") + val error = FieldValidationError("ip", message) + List(error) + } +} diff --git a/server/app/com/xsn/explorer/services/MasternodeService.scala b/server/app/com/xsn/explorer/services/MasternodeService.scala index 92ba467..0f78458 100644 --- a/server/app/com/xsn/explorer/services/MasternodeService.scala +++ b/server/app/com/xsn/explorer/services/MasternodeService.scala @@ -3,12 +3,15 @@ package com.xsn.explorer.services import javax.inject.Inject import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureOps, OrOps} -import com.alexitc.playsonify.core.FuturePaginatedResult +import com.alexitc.playsonify.core.{FutureApplicationResult, FuturePaginatedResult} import com.alexitc.playsonify.models._ import com.alexitc.playsonify.validators.PaginatedQueryValidator +import com.xsn.explorer.errors.IPAddressFormatError +import com.xsn.explorer.models.IPAddress import com.xsn.explorer.models.fields.MasternodeField import com.xsn.explorer.models.rpc.Masternode import com.xsn.explorer.parsers.MasternodeOrderingParser +import org.scalactic.{Bad, Good} import scala.concurrent.ExecutionContext @@ -28,6 +31,20 @@ class MasternodeService @Inject() ( result.toFuture } + def getMasternode(ipAddressString: String): FutureApplicationResult[Masternode] = { + val result = for { + ipAddress <- IPAddress + .from(ipAddressString) + .map(Good(_)) + .getOrElse(Bad(IPAddressFormatError).accumulating) + .toFutureOr + + masternode <- xsnService.getMasternode(ipAddress).toFutureOr + } yield masternode + + result.toFuture + } + private def build(list: List[Masternode], query: PaginatedQuery, ordering: FieldOrdering[MasternodeField]) = { val partial = sort(list, ordering) .slice(query.offset.int, query.offset.int + query.limit.int) diff --git a/server/app/controllers/MasternodesController.scala b/server/app/controllers/MasternodesController.scala index 5d67147..f54dab0 100644 --- a/server/app/controllers/MasternodesController.scala +++ b/server/app/controllers/MasternodesController.scala @@ -17,4 +17,8 @@ class MasternodesController @Inject() ( masternodeService.getMasternodes(paginatedQuery, orderingQuery) } + + def getBy(ipAddress: String) = publicNoInput { _ => + masternodeService.getMasternode(ipAddress) + } } diff --git a/server/conf/messages b/server/conf/messages index adbc3b9..3254af4 100644 --- a/server/conf/messages +++ b/server/conf/messages @@ -12,6 +12,8 @@ error.block.notFound=Block not found error.masternode.notFound=Masternode not found +error.ipAddress.invalid=Invalid ip address + error.paginatedQuery.offset.invalid=Invalid offset, it should be a number greater or equal than 0 error.paginatedQuery.limit.invalid=Invalid limit, it should be a number between 1 and and {0} diff --git a/server/conf/routes b/server/conf/routes index 684f131..94e2711 100644 --- a/server/conf/routes +++ b/server/conf/routes @@ -17,3 +17,4 @@ GET /stats controllers.StatisticsController.getStatus() GET /balances controllers.BalancesController.get(offset: Int ?= 0, limit: Int ?= 10, orderBy: String ?= "") GET /masternodes controllers.MasternodesController.get(offset: Int ?= 0, limit: Int ?= 10, orderBy: String ?= "") +GET /masternodes/:ip controllers.MasternodesController.getBy(ip: String) diff --git a/server/test/controllers/MasternodesControllerSpec.scala b/server/test/controllers/MasternodesControllerSpec.scala index 1af26e7..a476d64 100644 --- a/server/test/controllers/MasternodesControllerSpec.scala +++ b/server/test/controllers/MasternodesControllerSpec.scala @@ -1,12 +1,14 @@ package controllers +import com.alexitc.playsonify.PublicErrorRenderer import com.alexitc.playsonify.core.FutureApplicationResult +import com.xsn.explorer.errors.MasternodeNotFoundError import com.xsn.explorer.helpers.DummyXSNService -import com.xsn.explorer.models.{Address, TransactionId} import com.xsn.explorer.models.rpc.Masternode +import com.xsn.explorer.models.{Address, IPAddress, TransactionId} import com.xsn.explorer.services.XSNService import controllers.common.MyAPISpec -import org.scalactic.{Good, One, Or} +import org.scalactic.{Bad, Good} import play.api.inject.bind import play.api.libs.json.JsValue import play.api.test.Helpers._ @@ -35,10 +37,20 @@ class MasternodesControllerSpec extends MyAPISpec { payee = Address.from("XdNDRAiMUC9KiVRzhCTg9w44jQRdCpCRe3").get) ) + val masternode = masternodes.last + val xsnService = new DummyXSNService { override def getMasternodes(): FutureApplicationResult[List[Masternode]] = { Future.successful(Good(masternodes)) } + + override def getMasternode(ipAddress: IPAddress): FutureApplicationResult[Masternode] = { + if (masternode.ip.startsWith(ipAddress.string)) { + Future.successful(Good(masternode)) + } else { + Future.successful(Bad(MasternodeNotFoundError).accumulating) + } + } } override val application = guiceApplicationBuilder @@ -48,7 +60,7 @@ class MasternodesControllerSpec extends MyAPISpec { "GET /masternodes" should { "return the masternodes" in { val expected = masternodes.head - val response = GET("/masternodes?offset=1&limit=10&orderByactiveSeconds:desc") + val response = GET("/masternodes?offset=1&limit=10&orderBy=activeSeconds:desc") status(response) mustEqual OK val json = contentAsJson(response) @@ -67,4 +79,49 @@ class MasternodesControllerSpec extends MyAPISpec { (item \ "status").as[String] mustEqual expected.status } } + + "GET /masternodes/:ip" should { + "return the masternode" in { + val expected = masternode + val response = GET("/masternodes/45.32.148.13") + status(response) mustEqual OK + + val json = contentAsJson(response) + (json \ "activeSeconds").as[Long] mustEqual expected.activeSeconds + (json \ "ip").as[String] mustEqual expected.ip + (json \ "lastSeen").as[Long] mustEqual expected.lastSeen + (json \ "payee").as[String] mustEqual expected.payee.string + (json \ "protocol").as[String] mustEqual expected.protocol + (json \ "status").as[String] mustEqual expected.status + (json \ "txid").as[String] mustEqual expected.txid.string + } + + "fail on masternode not found" in { + val response = GET("/masternodes/45.32.149.13") + status(response) mustEqual NOT_FOUND + + val json = contentAsJson(response) + val errorList = (json \ "errors").as[List[JsValue]] + errorList.size mustEqual 1 + + val error = errorList.head + (error \ "type").as[String] mustEqual PublicErrorRenderer.FieldValidationErrorType + (error \ "field").as[String] mustEqual "ip" + (error \ "message").as[String].nonEmpty mustEqual true + } + + "fail on bad ip format" in { + val response = GET("/masternodes/45.32.149.1333") + status(response) mustEqual BAD_REQUEST + + val json = contentAsJson(response) + val errorList = (json \ "errors").as[List[JsValue]] + errorList.size mustEqual 1 + + val error = errorList.head + (error \ "type").as[String] mustEqual PublicErrorRenderer.FieldValidationErrorType + (error \ "field").as[String] mustEqual "ip" + (error \ "message").as[String].nonEmpty mustEqual true + } + } }