I have a endpoint that takes in a JSON body. I have implicit reads and writes for this JSON format. In the endpoint, I do a validation of the JSON and fold on the result! Here it is:
def updatePowerPlant(id: Int) = Action.async(parse.tolerantJson) { request =>
request.body.validate[PowerPlantConfig].fold(
errors => {
Future.successful(
BadRequest(
Json.obj("message" -> s"invalid PowerPlantConfig ${errors.mkString(",")}")
).enableCors
)
},
success => {
dbService.insertOrUpdatePowerPlant(success).runAsync.materialize.map {
case Failure(ex) =>
InternalServerError(s"Error updating PowerPlant " +
s"Reason => ${ex.getMessage}").enableCors
case Success(result) =>
result match {
case Left(errorMessage) =>
BadRequest(Json.obj("message" -> s"invalid PowerPlantConfig $errorMessage")).enableCors
case Right(updatedConfig) =>
Ok(Json.prettyPrint(Json.toJson(updatedConfig))).enableCors
}
}
}
)
}
So as it can be seen that I fold on the error and I return a BadRequest. But when I tried writing a unit test, I do not get the HTTP status as BadRequest as I expect, but the test crashes with an exception as below:
JsResultException(errors:List((,List(ValidationError(List(error.expected.jsnumber),WrappedArray())))))
play.api.libs.json.JsResultException: JsResultException(errors:List((,List(ValidationError(List(error.expected.jsnumber),WrappedArray())))))
at play.api.libs.json.JsReadable$$anonfun$2.apply(JsReadable.scala:23)
at play.api.libs.json.JsReadable$$anonfun$2.apply(JsReadable.scala:23)
at play.api.libs.json.JsResult$class.fold(JsResult.scala:73)
at play.api.libs.json.JsError.fold(JsResult.scala:13)
at play.api.libs.json.JsReadable$class.as(JsReadable.scala:21)
at play.api.libs.json.JsDefined.as(JsLookup.scala:132)
at com.inland24.plantsim.models.package$$anon$1.reads(package.scala:61)
at play.api.libs.json.JsValue$class.validate(JsValue.scala:18)
at play.api.libs.json.JsObject.validate(JsValue.scala:76)
at com.inland24.plantsim.controllers.PowerPlantController$$anonfun$updatePowerPlant1$1.apply(PowerPlantController.scala:64)
at com.inland24.plantsim.controllers.PowerPlantController$$anonfun$updatePowerPlant1$1.apply(PowerPlantController.scala:63)
at play.api.mvc.Action$.invokeBlock(Action.scala:498)
at play.api.mvc.Action$.invokeBlock(Action.scala:495)
at play.api.mvc.ActionBuilder$$anon$2.apply(Action.scala:458)
at com.inland24.plantsim.controllers.PowerPlantControllerTest$$anonfun$4$$anonfun$apply$mcV$sp$11.apply(PowerPlantControllerTest.scala:313)
at com.inland24.plantsim.controllers.PowerPlantControllerTest$$anonfun$4$$anonfun$apply$mcV$sp$11.apply(PowerPlantControllerTest.scala:296)
at org.scalatest.OutcomeOf$class.outcomeOf(OutcomeOf.scala:85)
at org.scalatest.OutcomeOf$.outcomeOf(OutcomeOf.scala:104)
at org.scalatest.Transformer.apply(Transformer.scala:22)
at org.scalatest.Transformer.apply(Transformer.scala:20)
at org.scalatest.WordSpecLike$$anon$1.apply(WordSpecLike.scala:1078)
at org.scalatest.TestSuite$class.withFixture(TestSuite.scala:196)
at com.inland24.plantsim.controllers.PowerPlantControllerTest.withFixture(PowerPlantControllerTest.scala:40)
at org.scalatest.WordSpecLike$class.invokeWithFixture$1(WordSpecLike.scala:1075)
at org.scalatest.WordSpecLike$$anonfun$runTest$1.apply(WordSpecLike.scala:1088)
at org.scalatest.WordSpecLike$$anonfun$runTest$1.apply(WordSpecLike.scala:1088)
at org.scalatest.SuperEngine.runTestImpl(Engine.scala:289)
at org.scalatest.WordSpecLike$class.runTest(WordSpecLike.scala:1088)
at com.inland24.plantsim.controllers.PowerPlantControllerTest.runTest(PowerPlantControllerTest.scala:40)
at org.scalatest.WordSpecLike$$anonfun$runTests$1.apply(WordSpecLike.scala:1147)
at org.scalatest.WordSpecLike$$anonfun$runTests$1.apply(WordSpecLike.scala:1147)
at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:396)
at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:384)
at scala.collection.immutable.List.foreach(List.scala:392)
at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:384)
at org.scalatest.SuperEngine.org$scalatest$SuperEngine$$runTestsInBranch(Engine.scala:373)
at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:410)
at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:384)
at scala.collection.immutable.List.foreach(List.scala:392)
at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:384)
at org.scalatest.SuperEngine.org$scalatest$SuperEngine$$runTestsInBranch(Engine.scala:379)
at org.scalatest.SuperEngine.runTestsImpl(Engine.scala:461)
at org.scalatest.WordSpecLike$class.runTests(WordSpecLike.scala:1147)
at com.inland24.plantsim.controllers.PowerPlantControllerTest.runTests(PowerPlantControllerTest.scala:40)
at org.scalatest.Suite$class.run(Suite.scala:1147)
at com.inland24.plantsim.controllers.PowerPlantControllerTest.org$scalatest$WordSpecLike$$super$run(PowerPlantControllerTest.scala:40)
at org.scalatest.WordSpecLike$$anonfun$run$1.apply(WordSpecLike.scala:1192)
at org.scalatest.WordSpecLike$$anonfun$run$1.apply(WordSpecLike.scala:1192)
at org.scalatest.SuperEngine.runImpl(Engine.scala:521)
at org.scalatest.WordSpecLike$class.run(WordSpecLike.scala:1192)
at com.inland24.plantsim.controllers.PowerPlantControllerTest.org$scalatest$BeforeAndAfterAll$$super$run(PowerPlantControllerTest.scala:40)
at org.scalatest.BeforeAndAfterAll$class.liftedTree1$1(BeforeAndAfterAll.scala:213)
at org.scalatest.BeforeAndAfterAll$class.run(BeforeAndAfterAll.scala:210)
at com.inland24.plantsim.controllers.PowerPlantControllerTest.run(PowerPlantControllerTest.scala:40)
at org.scalatest.tools.SuiteRunner.run(SuiteRunner.scala:45)
at org.scalatest.tools.Runner$$anonfun$doRunRunRunDaDoRunRun$1.apply(Runner.scala:1340)
at org.scalatest.tools.Runner$$anonfun$doRunRunRunDaDoRunRun$1.apply(Runner.scala:1334)
at scala.collection.immutable.List.foreach(List.scala:392)
at org.scalatest.tools.Runner$.doRunRunRunDaDoRunRun(Runner.scala:1334)
at org.scalatest.tools.Runner$$anonfun$runOptionallyWithPassFailReporter$2.apply(Runner.scala:1011)
at org.scalatest.tools.Runner$$anonfun$runOptionallyWithPassFailReporter$2.apply(Runner.scala:1010)
at org.scalatest.tools.Runner$.withClassLoaderAndDispatchReporter(Runner.scala:1500)
at org.scalatest.tools.Runner$.runOptionallyWithPassFailReporter(Runner.scala:1010)
at org.scalatest.tools.Runner$.run(Runner.scala:850)
at org.scalatest.tools.Runner.run(Runner.scala)
at org.jetbrains.plugins.scala.testingSupport.scalaTest.ScalaTestRunner.runScalaTest2(ScalaTestRunner.java:138)
at org.jetbrains.plugins.scala.testingSupport.scalaTest.ScalaTestRunner.main(ScalaTestRunner.java:28)
Here is my unit test:
"not update for an invalid PowerPlantConfig JSON" in {
// We are updating the PowerPlant with id = 101, Notice that the powerPlantId is invalid
val jsBody =
"""
|{
| "powerPlantId":"invalidId",
| "powerPlantName":"joesan 1",
| "minPower":100,
| "maxPower":800,
| "rampPowerRate":20.0,
| "rampRateInSeconds":"2 seconds",
| "powerPlantType":"RampUpType"
|}
""".stripMargin
val result: Future[Result] =
controller.updatePowerPlant(101)
.apply(
FakeRequest().withBody(Json.parse(jsBody))
)
result.materialize.map {
case Success(succ) =>
assert(succ.header.status === BAD_REQUEST)
case Failure(_) =>
fail("Unexpected test failure when Updating a PowerPlant! Please Analyze!")
}
}
Any idea why I'm not getting the expected behavior? I'm expecting that I get a HTTP BadRequest back!
EDIT: To get rid of the unexpected exception, I had to wrap my code into a Try block and I do not want that. So this piece of code gets rid of the error:
def updatePowerPlant(id: Int) = Action.async(parse.tolerantJson) { request =>
scala.util.Try(request.body.validate[PowerPlantConfig]) match {
case Failure(fail) =>
Future.successful(InternalServerError(s"Error updating PowerPlant " +
s"Reason => ${fail.getMessage}").enableCors)
case Success(succ) =>
succ.fold(
errors => {
Future.successful(
BadRequest(
Json.obj("message" -> s"invalid PowerPlantConfig ${errors.mkString(",")}")
).enableCors
)
},
success => {
dbService.insertOrUpdatePowerPlant(success).runAsync.materialize.map {
case Failure(ex) =>
InternalServerError(s"Error updating PowerPlant " +
s"Reason => ${ex.getMessage}").enableCors
case Success(result) =>
result match {
case Left(errorMessage) =>
BadRequest(Json.obj("message" -> s"invalid PowerPlantConfig $errorMessage")).enableCors
case Right(updatedConfig) =>
Ok(Json.prettyPrint(Json.toJson(updatedConfig))).enableCors
}
}
}
)
}
}
But as it can be seen that there is this additional Try(....) block and I do not want this!
Here is my definition of PowerPlantConfig:
sealed trait PowerPlantConfig {
def id: Int
def name: String
def minPower: Double
def maxPower: Double
def powerPlantType: PowerPlantType
}
object PowerPlantConfig {
case class OnOffTypeConfig(
id: Int,
name: String,
minPower: Double,
maxPower: Double,
powerPlantType: PowerPlantType
) extends PowerPlantConfig
case class RampUpTypeConfig(
id: Int,
name: String,
minPower: Double,
maxPower: Double,
rampPowerRate: Double,
rampRateInSeconds: FiniteDuration,
powerPlantType: PowerPlantType
) extends PowerPlantConfig
case class UnknownConfig(
id: Int = -1,
name: String,
minPower: Double,
maxPower: Double,
powerPlantType: PowerPlantType
) extends PowerPlantConfig
// represents all the PowerPlant's from the database
case class PowerPlantsConfig(
snapshotDateTime: DateTime,
powerPlantConfigSeq: Seq[PowerPlantConfig]
)
}
Here is my JSON reads and writes:
implicit val powerPlantCfgFormat: Format[PowerPlantConfig] = new Format[PowerPlantConfig] {
def reads(json: JsValue): JsResult[PowerPlantConfig] = {
val powerPlantTyp = PowerPlantType.fromString((json \ "powerPlantType").as[String])
powerPlantTyp match {
case PowerPlantType.OnOffType =>
JsSuccess(OnOffTypeConfig(
id = (json \ "powerPlantId").as[Int],
name = (json \ "powerPlantName").as[String],
minPower = (json \ "minPower").as[Double],
maxPower = (json \ "maxPower").as[Double],
powerPlantType = powerPlantTyp
))
case PowerPlantType.RampUpType =>
JsSuccess(RampUpTypeConfig(
id = (json \ "powerPlantId").as[Int],
name = (json \ "powerPlantName").as[String],
minPower = (json \ "minPower").as[Double],
rampPowerRate = (json \ "rampPowerRate").as[Double],
rampRateInSeconds = Duration.apply((json \ "rampRateInSeconds").as[String]).asInstanceOf[FiniteDuration],
maxPower = (json \ "maxPower").as[Double],
powerPlantType = powerPlantTyp
))
case _ =>
JsSuccess(UnknownConfig(
id = (json \ "powerPlantId").as[Int],
name = (json \ "powerPlantName").as[String],
minPower = (json \ "minPower").as[Double],
maxPower = (json \ "maxPower").as[Double],
powerPlantType = powerPlantTyp
))
}
}
def writes(o: PowerPlantConfig): JsValue = {
if (o.powerPlantType == RampUpType) {
Json.obj(
"powerPlantId" -> o.id,
"powerPlantName" -> o.name,
"minPower" -> o.minPower,
"maxPower" -> o.maxPower,
"rampPowerRate" -> o.asInstanceOf[RampUpTypeConfig].rampPowerRate,
"rampRateInSeconds" -> o.asInstanceOf[RampUpTypeConfig].rampRateInSeconds.toString(),
"powerPlantType" -> PowerPlantType.toString(o.powerPlantType)
)
}
else {
Json.obj(
"powerPlantId" -> o.id,
"powerPlantName" -> o.name,
"minPower" -> o.minPower,
"maxPower" -> o.maxPower,
"powerPlantType" -> PowerPlantType.toString(o.powerPlantType)
)
}
}
}