From 0f3d674368d519c3989c235b7d04358a7347647c Mon Sep 17 00:00:00 2001 From: Alexis Hernandez Date: Sat, 10 Mar 2018 22:24:27 -0600 Subject: [PATCH] server: Add GET /transactions/:txid --- .../controllers/TransactionsController.scala | 16 +++ .../common/MyAuthenticatorService.scala | 16 +++ .../controllers/common/MyJsonController.scala | 25 ++++ .../common/MyJsonControllerComponents.scala | 15 +++ server/conf/routes | 2 + .../TransactionsControllerSpec.scala | 121 ++++++++++++++++++ .../test/controllers/common/MyAPISpec.scala | 18 +++ 7 files changed, 213 insertions(+) create mode 100644 server/app/controllers/TransactionsController.scala create mode 100644 server/app/controllers/common/MyAuthenticatorService.scala create mode 100644 server/app/controllers/common/MyJsonController.scala create mode 100644 server/app/controllers/common/MyJsonControllerComponents.scala create mode 100644 server/test/controllers/TransactionsControllerSpec.scala create mode 100644 server/test/controllers/common/MyAPISpec.scala diff --git a/server/app/controllers/TransactionsController.scala b/server/app/controllers/TransactionsController.scala new file mode 100644 index 0000000..b6da392 --- /dev/null +++ b/server/app/controllers/TransactionsController.scala @@ -0,0 +1,16 @@ +package controllers + +import javax.inject.Inject + +import com.xsn.explorer.services.TransactionService +import controllers.common.{MyJsonController, MyJsonControllerComponents} + +class TransactionsController @Inject() ( + transactionService: TransactionService, + cc: MyJsonControllerComponents) + extends MyJsonController(cc) { + + def getTransaction(txid: String) = publicNoInput { _ => + transactionService.getTransaction(txid) + } +} diff --git a/server/app/controllers/common/MyAuthenticatorService.scala b/server/app/controllers/common/MyAuthenticatorService.scala new file mode 100644 index 0000000..37a4e98 --- /dev/null +++ b/server/app/controllers/common/MyAuthenticatorService.scala @@ -0,0 +1,16 @@ +package controllers.common + +import com.alexitc.playsonify.AbstractAuthenticatorService +import com.alexitc.playsonify.core.FutureApplicationResult +import org.scalactic.Good +import play.api.libs.json.JsValue +import play.api.mvc.Request + +import scala.concurrent.Future + +class MyAuthenticatorService extends AbstractAuthenticatorService[Nothing] { + + override def authenticate(request: Request[JsValue]): FutureApplicationResult[Nothing] = { + Future.successful(Good(???)) + } +} diff --git a/server/app/controllers/common/MyJsonController.scala b/server/app/controllers/common/MyJsonController.scala new file mode 100644 index 0000000..bcdf3d3 --- /dev/null +++ b/server/app/controllers/common/MyJsonController.scala @@ -0,0 +1,25 @@ +package controllers.common + +import com.alexitc.playsonify.AbstractJsonController +import com.alexitc.playsonify.models.{ErrorId, ServerError} +import org.slf4j.LoggerFactory + +abstract class MyJsonController(cc: MyJsonControllerComponents) extends AbstractJsonController[Nothing](cc) { + + protected val logger = LoggerFactory.getLogger(this.getClass) + + override def onServerError(error: ServerError, errorId: ErrorId): Unit = { + val message = s"Unexpected server error, id = ${errorId.string}, error = $error" + + error + .cause + .orElse { + logger.warn(message) + None + } + .foreach { cause => + // we'll log as error when there is an exception involved + logger.error(message, cause) + } + } +} diff --git a/server/app/controllers/common/MyJsonControllerComponents.scala b/server/app/controllers/common/MyJsonControllerComponents.scala new file mode 100644 index 0000000..4d48efa --- /dev/null +++ b/server/app/controllers/common/MyJsonControllerComponents.scala @@ -0,0 +1,15 @@ +package controllers.common + +import javax.inject.Inject + +import com.alexitc.playsonify.{JsonControllerComponents, PublicErrorRenderer} +import play.api.mvc.MessagesControllerComponents + +import scala.concurrent.ExecutionContext + +class MyJsonControllerComponents @Inject() ( + override val messagesControllerComponents: MessagesControllerComponents, + override val executionContext: ExecutionContext, + override val publicErrorRenderer: PublicErrorRenderer, + override val authenticatorService: MyAuthenticatorService) + extends JsonControllerComponents[Nothing] diff --git a/server/conf/routes b/server/conf/routes index 2b18828..39edd60 100644 --- a/server/conf/routes +++ b/server/conf/routes @@ -2,3 +2,5 @@ # This file defines all application routes (Higher priority routes first) # https://www.playframework.com/documentation/latest/ScalaRouting # ~~~~ + +GET /transactions/:txid controllers.TransactionsController.getTransaction(txid: String) diff --git a/server/test/controllers/TransactionsControllerSpec.scala b/server/test/controllers/TransactionsControllerSpec.scala new file mode 100644 index 0000000..4c00be5 --- /dev/null +++ b/server/test/controllers/TransactionsControllerSpec.scala @@ -0,0 +1,121 @@ +package controllers + +import com.alexitc.playsonify.PublicErrorRenderer +import com.alexitc.playsonify.core.FutureApplicationResult +import com.xsn.explorer.errors.TransactionNotFoundError +import com.xsn.explorer.models.TransactionId +import com.xsn.explorer.services.XSNService +import controllers.common.MyAPISpec +import org.scalactic.{Bad, Good} +import play.api.inject.bind +import play.api.libs.json.{JsValue, Json} +import play.api.test.Helpers._ + +import scala.concurrent.Future + +class TransactionsControllerSpec extends MyAPISpec { + + val customXSNService = new XSNService { + val map = Map( + TransactionId.from("024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c").get -> + s""" + |{ + | "blockhash": "000003fb382f6892ae96594b81aa916a8923c70701de4e7054aac556c7271ef7", + | "blocktime": 1520276270, + | "confirmations": 5347, + | "height": 1, + | "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff010000000000000000232103e8c52f2c5155771492907095753a43ce776e1fa7c5e769a67a9f3db4467ec029ac00000000", + | "locktime": 0, + | "size": 98, + | "time": 1520276270, + | "txid": "024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c", + | "version": 1, + | "vin": [ + | { + | "coinbase": "510101", + | "sequence": 4294967295 + | } + | ], + | "vout": [ + | { + | "n": 0, + | "scriptPubKey": { + | "addresses": [ + | "XdJnCKYNwzCz8ATv8Eu75gonaHyfr9qXg9" + | ], + | "asm": "03e8c52f2c5155771492907095753a43ce776e1fa7c5e769a67a9f3db4467ec029 OP_CHECKSIG", + | "hex": "2103e8c52f2c5155771492907095753a43ce776e1fa7c5e769a67a9f3db4467ec029ac", + | "reqSigs": 1, + | "type": "pubkey" + | }, + | "value": 0, + | "valueSat": 0 + | } + | ] + |} + """.stripMargin.trim + ) + + override def getTransaction(txid: TransactionId): FutureApplicationResult[JsValue] = { + val result = map.get(txid) + .map(s => Json.toJson(s)) + .map(Good(_)) + .getOrElse { + Bad(TransactionNotFoundError).accumulating + } + + Future.successful(result) + } + } + + override val application = guiceApplicationBuilder + .overrides(bind[XSNService].to(customXSNService)) + .build() + + "GET /transactions/:txid" should { + def url(txid: String) = s"/transactions/$txid" + + "return an existing transaction" in { + val txid = "024aba1d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c" + val response = GET(url(txid)) + + status(response) mustEqual OK + // TODO: Match result + //val json = contentAsJson(response) + //(json \ "txid").as[String] mustEqual txid + } + + "fail on wrong transaction format" in { + // 63 characters + val txid = "000001d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c" + val response = GET(url(txid)) + + 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 "transactionId" + (error \ "message").as[String].nonEmpty mustEqual true + } + + "fail on unknown transaction" in { + val txid = "0000001d535cfe5dd3ea465d46a828a57b00e1df012d7a2d158e0f7484173f7c" + val response = GET(url(txid)) + + 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 "transactionId" + (error \ "message").as[String].nonEmpty mustEqual true + } + } +} diff --git a/server/test/controllers/common/MyAPISpec.scala b/server/test/controllers/common/MyAPISpec.scala new file mode 100644 index 0000000..11b085a --- /dev/null +++ b/server/test/controllers/common/MyAPISpec.scala @@ -0,0 +1,18 @@ +package controllers.common + +import com.alexitc.playsonify.test.PlayAPISpec +import org.slf4j.LoggerFactory +import play.api.mvc.Result +import play.api.test.FakeRequest +import play.api.test.Helpers._ + +import scala.concurrent.Future + +trait MyAPISpec extends PlayAPISpec { + + protected val logger = LoggerFactory.getLogger(this.getClass) + + override protected def log[T](request: FakeRequest[T], response: Future[Result]): Unit = { + logger.info(s"> REQUEST, $request; < RESPONSE, status = ${status(response)}, body = ${contentAsString(response)}") + } +}