Skip to content

Commit

Permalink
Task/psgs 55 improve coverage (#20)
Browse files Browse the repository at this point in the history
Improved coverage in TxMeta #PSGS-55
  • Loading branch information
maciejbak85 committed Oct 13, 2020
1 parent ed112d6 commit 657591d
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 30 deletions.
2 changes: 1 addition & 1 deletion src/main/scala/iog/psg/cardano/CardanoApiCodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ object CardanoApiCodec {
private[cardano] implicit val decodeDelegationStatus: Decoder[DelegationStatus] = Decoder.decodeString.map(DelegationStatus.withName)
private[cardano] implicit val encodeDelegationStatus: Encoder[DelegationStatus] = (a: DelegationStatus) => Json.fromString(a.toString)

private[cardano] implicit val decodeTxMetadataOut: Decoder[TxMetadataOut] = Decoder.decodeJson.map(TxMetadataOut)
private[cardano] implicit val decodeTxMetadataOut: Decoder[TxMetadataOut] = Decoder.decodeJson.map(TxMetadataOut.apply)
private[cardano] implicit val decodeKeyMetadata: KeyDecoder[MetadataKey] = (key: String) => Some(MetadataValueStr(key))

sealed trait MetadataValue
Expand Down
30 changes: 18 additions & 12 deletions src/main/scala/iog/psg/cardano/TxMetadataOut.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,38 @@ import io.circe.CursorOp.DownField
import io.circe._
import iog.psg.cardano.CardanoApiCodec._

final case class TxMetadataOut(json: Json) {
object TxMetadataOut {
private val ValueTypeString = "string"
private val ValueTypeLong = "int" //named int but will work as long
private val ValueTypeBytes = "bytes"
private val ValueTypeList = "list"
private val ValueTypeMap = "map"

type DecodingEither[T] = Either[DecodingFailure, T]
type KeyVal = Map[Long, MetadataValue]
}

final val ValueTypeString = "string"
final val ValueTypeLong = "int" //named int but will work as long
final val ValueTypeBytes = "bytes"
final val ValueTypeList = "list"
final val ValueTypeMap = "map"
final case class TxMetadataOut(json: Json) {
import TxMetadataOut._

def toMetadataMap: Decoder.Result[Map[Long, MetadataValue]] = {
type KeyVal = Map[Long, MetadataValue]

implicit val decodeMap: Decoder[Map[Long, MetadataValue]] = (c: HCursor) => {

def extractStringField(cursor: ACursor): DecodingEither[MetadataValueStr] =
cursor.downField(ValueTypeString).as[String].fold(
err => Left(err),
err => Left(err.copy(message = s"Not a String type")),
(value: String) => Right(MetadataValueStr(value))
)

def extractLongField(cursor: ACursor): DecodingEither[MetadataValueLong] =
cursor.downField(ValueTypeLong).as[Long].fold(
err => Left(err),
err => Left(err.copy(message = s"Not a Long type")),
(value: Long) => Right(MetadataValueLong(value))
)

def extractBytesField(cursor: ACursor): DecodingEither[MetadataValueByteString] =
cursor.downField(ValueTypeBytes).as[String].fold(
err => Left(err),
err => Left(err.copy(message = s"Not a Bytes type")),
(value: String) => Right(MetadataValueByteString(ByteString(value)))
)

Expand All @@ -50,7 +52,9 @@ final case class TxMetadataOut(json: Json) {
case Some(ValueTypeBytes) =>
extractBytesField(cursor)

case _ => Left(DecodingFailure("Invalid type value", List(DownField(key))))
case Some(valueType) => Left(DecodingFailure(s"Invalid type '$valueType'", List(DownField(key))))

case None => Left(DecodingFailure("Missing value under key", List(DownField(key))))
}
}

Expand Down Expand Up @@ -115,6 +119,8 @@ final case class TxMetadataOut(json: Json) {
case Some(valueType) if valueType == ValueTypeMap =>
extractMapField(keyDownField, key).map(valueMap => map.+(key.toLong -> valueMap))

case Some(valueType) => Left(DecodingFailure(s"Invalid type '$valueType'", List(DownField(key))))

case None => Left(DecodingFailure("Missing value under key", List(DownField(key))))
}
})
Expand Down
4 changes: 3 additions & 1 deletion src/test/scala/iog/psg/cardano/CardanoApiSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ class CardanoApiSpec
}

"POST /wallets" should "" in {
api.createRestoreWallet(wallet.name, "Pass9128!", mnemonicSentence).executeOrFail() shouldBe wallet
api
.createRestoreWallet(wallet.name, "Pass9128!", mnemonicSentence, Some(mnemonicSecondFactor), Some(500))
.executeOrFail() shouldBe wallet
}

"GET /wallets/{walletId}/addresses?state=unused" should "return wallet's unused addresses" in {
Expand Down
16 changes: 14 additions & 2 deletions src/test/scala/iog/psg/cardano/CardanoJpiSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import java.time.ZonedDateTime
import java.util.concurrent.CompletionStage

import akka.actor.ActorSystem
import iog.psg.cardano.CardanoApiCodec.{MetadataValueStr, TxMetadataMapIn}
import iog.psg.cardano.jpi.{AddressFilter, JpiResponseCheck, ListTransactionsParamBuilder}
import iog.psg.cardano.util.{Configure, DummyModel, InMemoryCardanoApi, JsonFiles, ModelCompare}
import org.scalatest.concurrent.ScalaFutures
Expand Down Expand Up @@ -51,7 +52,13 @@ class CardanoJpiSpec

"POST /wallets" should "" in {
api
.createRestore(wallet.name, "Pass9128!", mnemonicSentence.mnemonicSentence.toList.asJava, 5)
.createRestore(
wallet.name,
"Pass9128!",
mnemonicSentence.mnemonicSentence.toList.asJava,
mnemonicSecondFactor.mnemonicSentence.toList.asJava,
5
)
.toCompletableFuture
.get() shouldBe wallet
}
Expand Down Expand Up @@ -109,8 +116,13 @@ class CardanoJpiSpec
}

"POST /wallets/{walletId}/transactions" should "create transaction" in {
val metadata = TxMetadataMapIn(Map(
0L -> MetadataValueStr("0" * 64),
1L -> MetadataValueStr("1" * 64)
))

api
.createTransaction(wallet.id, "MySecret", payments.payments.asJava)
.createTransaction(wallet.id, "MySecret", payments.payments.asJava, metadata, "50")
.toCompletableFuture
.get()
.id shouldBe firstTransactionId
Expand Down
93 changes: 83 additions & 10 deletions src/test/scala/iog/psg/cardano/TxMetadataCodecSpec.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package iog.psg.cardano

import akka.util.ByteString
import io.circe.ParsingFailure
import io.circe.parser.parse
import io.circe.syntax.EncoderOps
import io.circe.{ParsingFailure, parser}
import iog.psg.cardano.CardanoApiCodec._
import iog.psg.cardano.util.DummyModel
import org.scalatest.flatspec.AnyFlatSpec
Expand Down Expand Up @@ -36,22 +37,22 @@ class TxMetadataCodecSpec extends AnyFlatSpec with Matchers with DummyModel {

it should "encode list value to proper json" in {
val map: Map[Long, MetadataValue] = Map(3L -> MetadataValueArray(List(
MetadataValueLong(14), MetadataValueLong(42), MetadataValueStr("1337")
MetadataValueLong(14), MetadataValueLong(42), MetadataValueStr("1337"), MetadataValueByteString(ByteString("2512a00e9653fe49a44a5886202e24d77eeb998f"))
)))
val metaDataIn: TxMetadataIn = TxMetadataMapIn(map)
val metaInJsonStr = metaDataIn.asJson.noSpaces

metaInJsonStr shouldBe """{"3":{"list":[{"int":14},{"int":42},{"string":"1337"}]}}"""
metaInJsonStr shouldBe """{"3":{"list":[{"int":14},{"int":42},{"string":"1337"},{"bytes":"2512a00e9653fe49a44a5886202e24d77eeb998f"}]}}"""
}

it should "encode map value to proper json" in {
val map: Map[Long, MetadataValue] = Map(4L -> MetadataValueMap(Map(
MetadataValueStr("key") -> MetadataValueStr("value"),
MetadataValueStr("key") -> MetadataValueByteString(ByteString("2512a00e9653fe49a44a5886202e24d77eeb998f")),
MetadataValueLong(14) -> MetadataValueLong(42)
)))
val metaDataIn: TxMetadataIn = TxMetadataMapIn(map)
val metaInJsonStr = metaDataIn.asJson.noSpaces
metaInJsonStr shouldBe """{"4":{"map":[{"k":{"string":"key"},"v":{"string":"value"}},{"k":{"int":14},"v":{"int":42}}]}}"""
metaInJsonStr shouldBe """{"4":{"map":[{"k":{"string":"key"},"v":{"bytes":"2512a00e9653fe49a44a5886202e24d77eeb998f"}},{"k":{"int":14},"v":{"int":42}}]}}"""
}

it should "encode properly all types to json" in {
Expand All @@ -60,17 +61,17 @@ class TxMetadataCodecSpec extends AnyFlatSpec with Matchers with DummyModel {
1L -> MetadataValueLong(14),
2L -> MetadataValueByteString(ByteString("2512a00e9653fe49a44a5886202e24d77eeb998f")),
3L -> MetadataValueArray(List(
MetadataValueLong(14), MetadataValueLong(42), MetadataValueStr("1337")
MetadataValueByteString(ByteString("2512a00e9653fe49a44a5886202e24d77eeb998f")), MetadataValueLong(42), MetadataValueStr("1337")
)),
4L -> MetadataValueMap(Map(
MetadataValueStr("key") -> MetadataValueStr("value"),
MetadataValueStr("key") -> MetadataValueByteString(ByteString("2512a00e9653fe49a44a5886202e24d77eeb998f")),
MetadataValueLong(14) -> MetadataValueLong(42)
))
)
val metaDataIn: TxMetadataIn = TxMetadataMapIn(map)
val metaInJsonStr = metaDataIn.asJson.noSpaces

metaInJsonStr shouldBe """{"0":{"string":"cardano"},"1":{"int":14},"2":{"bytes":"2512a00e9653fe49a44a5886202e24d77eeb998f"},"3":{"list":[{"int":14},{"int":42},{"string":"1337"}]},"4":{"map":[{"k":{"string":"key"},"v":{"string":"value"}},{"k":{"int":14},"v":{"int":42}}]}}""".stripMargin
metaInJsonStr shouldBe """{"0":{"string":"cardano"},"1":{"int":14},"2":{"bytes":"2512a00e9653fe49a44a5886202e24d77eeb998f"},"3":{"list":[{"bytes":"2512a00e9653fe49a44a5886202e24d77eeb998f"},{"int":42},{"string":"1337"}]},"4":{"map":[{"k":{"string":"key"},"v":{"bytes":"2512a00e9653fe49a44a5886202e24d77eeb998f"}},{"k":{"int":14},"v":{"int":42}}]}}""".stripMargin
}

"txMetadataOut toMapMetadataStr" should "be parsed properly" in {
Expand All @@ -80,16 +81,88 @@ class TxMetadataCodecSpec extends AnyFlatSpec with Matchers with DummyModel {
2 -> MetadataValueByteString(ByteString("2512a00e9653fe49a44a5886202e24d77eeb998f")),
3 -> MetadataValueArray(Seq(
MetadataValueLong(14),
MetadataValueLong(42),
MetadataValueByteString(ByteString("2512a00e9653fe49a44a5886202e24d77eeb998f")),
MetadataValueStr("1337")
)),
4 -> MetadataValueMap(
Map(
MetadataValueStr("key") -> MetadataValueStr("value"),
MetadataValueStr("key") -> MetadataValueByteString(ByteString("2512a00e9653fe49a44a5886202e24d77eeb998f")),
MetadataValueLong(14) -> MetadataValueLong(42)
)))
}

it should "fail on missing type field" in {
val jsonWithInvalidTypeField = parser.parse("""{"0":"cardano"}""").getOrElse(fail("Invalid json structure"))
val tvMeta = TxMetadataOut(jsonWithInvalidTypeField)

val error = tvMeta.toMetadataMap.swap.getOrElse(fail("Should fail"))
error.getMessage() shouldBe "Missing value under key: DownField(0)"
}

it should "fail on unsupported type field" in {
val jsonWithInvalidTypeField = parser.parse("""{"0":{"superdouble":"cardano"}}""").getOrElse(fail("Invalid json structure"))
val tvMeta = TxMetadataOut(jsonWithInvalidTypeField)

val error = tvMeta.toMetadataMap.swap.getOrElse(fail("Should fail"))
error.getMessage() shouldBe "Invalid type 'superdouble': DownField(0)"
}

it should "fail on unsupported type field in list" in {
val jsonWithInvalidTypeField = parser.parse("""{"3":{"list":[{"superdouble":14},{"int":42},{"string":"1337"}]}}""").getOrElse(fail("Invalid json structure"))
val tvMeta = TxMetadataOut(jsonWithInvalidTypeField)

val error = tvMeta.toMetadataMap.swap.getOrElse(fail("Should fail"))
error.getMessage() shouldBe "Invalid type 'superdouble': DownField(3)"
}

it should "fail on missing type field in list" in {
val jsonWithInvalidTypeField = parser.parse("""{"3":{"list":[14,{"int":42},{"string":"1337"}]}}""").getOrElse(fail("Invalid json structure"))
val tvMeta = TxMetadataOut(jsonWithInvalidTypeField)

val error = tvMeta.toMetadataMap.swap.getOrElse(fail("Should fail"))
error.getMessage() shouldBe "Missing value under key: DownField(3)"
}

it should "fail on unsupported type field in map" in {
val jsonWithInvalidTypeField = parser.parse("""{"4":{"map":[{"k":{"string":"key"},"v":{"superdouble":"value"}},{"k":{"int":14},"v":{"int":42}}]}}""").getOrElse(fail("Invalid json structure"))
val tvMeta = TxMetadataOut(jsonWithInvalidTypeField)

val error = tvMeta.toMetadataMap.swap.getOrElse(fail("Should fail"))
error.getMessage() shouldBe "Invalid type 'superdouble': DownField(4)"
}

it should "fail on missing 'k' field in map" in {
val jsonWithInvalidTypeField = parser.parse("""{"4":{"map":[{"kx":{"string":"key"},"v":{"superdouble":"value"}},{"k":{"int":14},"v":{"int":42}}]}}""").getOrElse(fail("Invalid json structure"))
val tvMeta = TxMetadataOut(jsonWithInvalidTypeField)

val error = tvMeta.toMetadataMap.swap.getOrElse(fail("Should fail"))
error.getMessage() shouldBe "Missing 'k' value: DownField(4)"
}

it should "fail on int defined as a string type" in {
val jsonWithInvalidTypeField = parser.parse("""{"0":{"string": 12345}}""").getOrElse(fail("Invalid json structure"))
val tvMeta = TxMetadataOut(jsonWithInvalidTypeField)

val error = tvMeta.toMetadataMap.swap.getOrElse(fail("Should fail"))
error.getMessage() shouldBe "Not a String type: DownField(string),DownField(0)"
}

it should "fail on string defined as a int type" in {
val jsonWithInvalidTypeField = parser.parse("""{"0":{"int":"abc123"}}""").getOrElse(fail("Invalid json structure"))
val tvMeta = TxMetadataOut(jsonWithInvalidTypeField)

val error = tvMeta.toMetadataMap.swap.getOrElse(fail("Should fail"))
error.getMessage() shouldBe "Not a Long type: DownField(int),DownField(0)"
}

it should "fail on int defined as a bytes type" in {
val jsonWithInvalidTypeField = parser.parse("""{"0":{"bytes":12345}}""").getOrElse(fail("Invalid json structure"))
val tvMeta = TxMetadataOut(jsonWithInvalidTypeField)

val error = tvMeta.toMetadataMap.swap.getOrElse(fail("Should fail"))
error.getMessage() shouldBe "Not a Bytes type: DownField(bytes),DownField(0)"
}

"Raw Good TxMetadata" should "be parsed properly" in {
val asString = txMetadataOut.json.noSpaces
val Right(rawTxMetaJsonIn) = JsonMetadata.parse(asString)
Expand Down
5 changes: 3 additions & 2 deletions src/test/scala/iog/psg/cardano/util/DummyModel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ trait DummyModel { self: Assertions =>
| "int": 14
| },
| {
| "int": 42
| "bytes": "2512a00e9653fe49a44a5886202e24d77eeb998f"
| },
| {
| "string": "1337"
Expand All @@ -65,7 +65,7 @@ trait DummyModel { self: Assertions =>
| "string": "key"
| },
| "v": {
| "string": "value"
| "bytes": "2512a00e9653fe49a44a5886202e24d77eeb998f"
| }
| },
| {
Expand Down Expand Up @@ -165,6 +165,7 @@ trait DummyModel { self: Assertions =>
)

final lazy val mnemonicSentence = GenericMnemonicSentence("a b c d e a b c d e a b c d e")
final lazy val mnemonicSecondFactor = GenericMnemonicSecondaryFactor("a b c d e a b c d")

final lazy val payments = Payments(Seq(Payment(unUsedAddresses.head.id, QuantityUnit(100000, Units.lovelace))))

Expand Down
26 changes: 24 additions & 2 deletions src/test/scala/iog/psg/cardano/util/InMemoryCardanoApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ trait InMemoryCardanoApi {
}

implicit final class InMemoryExecutor[T](req: CardanoApiRequest[T]) {
def executeOrFail(): T = inMemoryExecutor.execute(req).futureValue.getOrElse(fail("Request failed."))
def executeOrFail(): T =
inMemoryExecutor.execute(req).futureValue match {
case Left(value) => fail(s"Request failed: ${value.message}")
case Right(value) => value
}

def executeExpectingErrorOrFail(): ErrorMessage =
inMemoryExecutor.execute(req).futureValue.swap.getOrElse(fail("Request should failed."))
Expand Down Expand Up @@ -89,6 +93,17 @@ trait InMemoryCardanoApi {
request.mapper(HttpResponse(status = StatusCodes.NotFound, entity = entity))
}

def unmarshalJsonBody(): Future[Json] =
Unmarshal(request.request.entity)
.to[String]
.map(str => parser.parse(str).getOrElse(fail("Could not parse json body")))

def checkIfContainsProperJsonKeys(json: Json, expectedList: List[String]): Future[Unit] =
if (json.hcursor.keys.getOrElse(Nil).toList == expectedList)
Future.successful(())
else
Future.failed(new CardanoApiException("Invalid json body", "400"))

def toJsonResponse[A](resp: A)(implicit enc: io.circe.Encoder[A]) =
request.mapper(
HttpEntity(resp.asJson.noSpaces)
Expand All @@ -105,7 +120,14 @@ trait InMemoryCardanoApi {
request.mapper(httpEntityFromJson("wallets.json"))

case ("wallets", HttpMethods.POST) =>
request.mapper(httpEntityFromJson("wallet.json"))
for {
jsonBody <- unmarshalJsonBody()
_ <- checkIfContainsProperJsonKeys(
jsonBody,
List("name", "passphrase", "mnemonic_sentence", "mnemonic_second_factor", "address_pool_gap")
)
response <- request.mapper(httpEntityFromJson("wallet.json"))
} yield response

case (s"wallets/${jsonFileWallet.id}", HttpMethods.GET) =>
request.mapper(httpEntityFromJson("wallet.json"))
Expand Down

0 comments on commit 657591d

Please sign in to comment.