From 327789a6175659540106ce4746ba32f9d6694632 Mon Sep 17 00:00:00 2001 From: "Ross A. Baker" Date: Fri, 2 Jul 2021 10:54:38 -0400 Subject: [PATCH 1/2] Use scalacheck-effect --- build.sbt | 5 +- .../scala/com/banno/vault/MissingPieces.scala | 40 ++++ .../scala/com/banno/vault/VaultSpec.scala | 174 +++++++++--------- 3 files changed, 127 insertions(+), 92 deletions(-) create mode 100644 core/src/test/scala/com/banno/vault/MissingPieces.scala diff --git a/build.sbt b/build.sbt index b4a04ae5..bd55daf9 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ val http4sV = "0.21.24" val specs2V = "4.10.6" val munitCatsEffectV = "0.13.1" val munitScalaCheckV = "0.7.22" +val scalacheckEffectV = "1.0.2" val kindProjectorV = "0.13.0" val betterMonadicForV = "0.3.1" @@ -153,8 +154,8 @@ lazy val commonSettings = Seq( "org.http4s" %% "http4s-dsl" % http4sV % Test, "org.typelevel" %% "munit-cats-effect-2" % munitCatsEffectV % Test, - "org.scalameta" %% "munit-scalacheck" % munitScalaCheckV % Test - + "org.scalameta" %% "munit-scalacheck" % munitScalaCheckV % Test, + "org.typelevel" %% "scalacheck-effect" % scalacheckEffectV % Test, ) ) diff --git a/core/src/test/scala/com/banno/vault/MissingPieces.scala b/core/src/test/scala/com/banno/vault/MissingPieces.scala new file mode 100644 index 00000000..5f32a36c --- /dev/null +++ b/core/src/test/scala/com/banno/vault/MissingPieces.scala @@ -0,0 +1,40 @@ +package com.banno.vault + +import cats.MonadThrow +import org.scalacheck.Prop._ +import org.scalacheck.effect.PropF + +/** Things missing in scalacheck-effect */ +trait MissingPieces { + + implicit class PropFExtensions[F[_]](self: PropF[F]) { + def ==>(p: => PropF[F])(implicit F: MonadThrow[F]): PropF[F] = + self.flatMap { res => + res.status match { + case Proof => p.map { pRes => mergeResults(pRes.status, res, pRes) } + case True => p.map { mergeResults(True, res, _) } + case _ => res.copy(status = Undecided) + } + } + + def label(l: String)(implicit F: MonadThrow[F]) = + self.map(r => r.copy(labels = r.labels + l)) + } + + implicit class ResultExtensions[F[_]](self: PropF.Result[F]) { + def success = self.status match { + case True => true + case Proof => true + case _ => false + } + def proved = self.status == Proof + } + + private def mergeResults[F[_]: MonadThrow](st: Status, x: PropF.Result[F], y: PropF.Result[F]) = + PropF.Result[F]( + status = st, + args = x.args ++ y.args, + collected = x.collected ++ y.collected, + labels = x.labels ++ y.labels + ) +} diff --git a/core/src/test/scala/com/banno/vault/VaultSpec.scala b/core/src/test/scala/com/banno/vault/VaultSpec.scala index 33a8102c..88b4e084 100644 --- a/core/src/test/scala/com/banno/vault/VaultSpec.scala +++ b/core/src/test/scala/com/banno/vault/VaultSpec.scala @@ -32,12 +32,12 @@ import org.http4s.circe._ import org.http4s.client.Client import scala.concurrent.duration._ -import munit.ScalaCheckSuite +import munit.{CatsEffectSuite, ScalaCheckSuite} import org.scalacheck._ import scala.util.Random -import org.scalacheck.Prop._ +import org.scalacheck.effect.PropF -class VaultSpec extends ScalaCheckSuite { +class VaultSpec extends CatsEffectSuite with ScalaCheckSuite with MissingPieces { case class RoleId(role_id: String) object RoleId { @@ -290,191 +290,185 @@ class VaultSpec extends ScalaCheckSuite { val mockClient : Client[IO] = Client.fromHttpApp(mockVaultService[IO].orNotFound) -property("login works as expected when sending a valid roleId") { - Prop.forAll(VaultArbitraries.validVaultUri) { uri => - Vault.login(mockClient, uri)(validRoleId).unsafeRunSync() == validToken +test("login works as expected when sending a valid roleId") { + PropF.forAllF(VaultArbitraries.validVaultUri) { uri => + Vault.login(mockClient, uri)(validRoleId).assertEquals(validToken) } } -property("login should fail when sending an invalid roleId") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("login should fail when sending an invalid roleId") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => Vault.login(mockClient, uri)(UUID.randomUUID().toString) .attempt - .unsafeRunSync() - .isLeft + .map(_.isLeft) + .assert } } -property("login should fail when the response is not a valid") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("login should fail when the response is not a valid") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => Vault.login(mockClient, uri)(invalidJSONRoleId) .attempt - .unsafeRunSync() - .isLeft + .map(_.isLeft) + .assert } } -property("login should fail when the response doesn't contains a token") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("login should fail when the response doesn't contains a token") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => import org.http4s.DecodeFailure Vault.login(mockClient, uri)(roleIdWithoutToken) .attempt - .unsafeRunSync() - .leftMap(_.isInstanceOf[DecodeFailure]) == Left(true) + .map(_.leftMap(_.isInstanceOf[DecodeFailure])) + .assertEquals(Left(true)) } } -property("login should fail when the response doesn't contains a lease duration") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("login should fail when the response doesn't contains a lease duration") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => import org.http4s.DecodeFailure Vault.login(mockClient, uri)(roleIdWithoutLease) .attempt - .unsafeRunSync() - .leftMap(_.isInstanceOf[DecodeFailure]) == Left(true) + .map(_.leftMap(_.isInstanceOf[DecodeFailure])) + .assertEquals(Left(true)) } } -property("kubernetesLogin works as expected when sending valid role and jwt") { - Prop.forAll(VaultArbitraries.validVaultUri) { uri => - Vault.kubernetesLogin(mockClient, uri)(validKubernetesRole, validKubernetesJwt).unsafeRunSync() == validToken +test("kubernetesLogin works as expected when sending valid role and jwt") { + PropF.forAllF(VaultArbitraries.validVaultUri) { uri => + Vault.kubernetesLogin(mockClient, uri)(validKubernetesRole, validKubernetesJwt).assertEquals(validToken) } } -property("kubernetesLogin should fail when sending an invalid roleId") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("kubernetesLogin should fail when sending an invalid roleId") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => Vault.kubernetesLogin(mockClient, uri)(UUID.randomUUID().toString, validKubernetesJwt) .attempt - .unsafeRunSync() - .isLeft + .map(_.isLeft) + .assert } } -property("kubernetesLogin should fail when the response is not a valid JSON") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("kubernetesLogin should fail when the response is not a valid JSON") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => Vault.kubernetesLogin(mockClient, uri)(invalidJSONRoleId, validKubernetesJwt) .attempt - .unsafeRunSync() - .isLeft + .map(_.isLeft) + .assert } } -property("kubernetesLogin should fail when the response doesn't contains a token") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("kubernetesLogin should fail when the response doesn't contains a token") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => import org.http4s.DecodeFailure Vault.kubernetesLogin(mockClient, uri)(roleIdWithoutToken, validKubernetesJwt) .attempt - .unsafeRunSync() - .leftMap(_.isInstanceOf[DecodeFailure]) == Left(true) + .map(_.leftMap(_.isInstanceOf[DecodeFailure])) + .assertEquals(Left(true)) } } -property("kubernetesLogin should fail when the response doesn't contains a lease duration") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("kubernetesLogin should fail when the response doesn't contains a lease duration") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => import org.http4s.DecodeFailure Vault.kubernetesLogin(mockClient, uri)(roleIdWithoutLease, validKubernetesJwt) .attempt - .unsafeRunSync() - .leftMap(_.isInstanceOf[DecodeFailure]) == Left(true) + .map(_.leftMap(_.isInstanceOf[DecodeFailure])) + .assertEquals(Left(true)) } } -property("readSecret works as expected when requesting the postgres password with a valid") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("readSecret works as expected when requesting the postgres password with a valid") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => Vault.readSecret[IO, VaultValue](mockClient, uri)(clientToken, secretPostgresPassPath) - .unsafeRunSync() == VaultSecret(VaultValue(postgresPass), leaseDuration.some, leaseId.some, renewable.some) + .assertEquals(VaultSecret(VaultValue(postgresPass), leaseDuration.some, leaseId.some, renewable.some)) } } -property("readSecret works as expected when requesting the private key with a valid token") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("readSecret works as expected when requesting the private key with a valid token") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => Vault.readSecret[IO, VaultValue](mockClient, uri)(clientToken, secretPrivateKeyPath) - .unsafeRunSync() == VaultSecret(VaultValue(privateKey), leaseDuration.some, leaseId.some, renewable.some) + .assertEquals(VaultSecret(VaultValue(privateKey), leaseDuration.some, leaseId.some, renewable.some)) } } -property("readSecret works as expected when requesting the postgres password with an invalid token") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("readSecret works as expected when requesting the postgres password with an invalid token") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => Vault.readSecret[IO, VaultValue](mockClient, uri)(UUID.randomUUID().toString, secretPostgresPassPath) .attempt - .unsafeRunSync() - .isLeft + .map(_.isLeft) + .assert } } -property("readSecret works as expected when requesting the private key with an invalid token") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("readSecret works as expected when requesting the private key with an invalid token") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => Vault.readSecret[IO, VaultValue](mockClient, uri)(UUID.randomUUID().toString, secretPrivateKeyPath) .attempt - .unsafeRunSync() - .isLeft + .map(_.isLeft) + .assert } } -property("readSecret suppresses echoing the data when JSON decoding fails") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("readSecret suppresses echoing the data when JSON decoding fails") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => Vault.readSecret[IO, TokenValue](mockClient, uri)(clientToken, secretPrivateKeyPath) - .attempt - .unsafeRunSync() - .fold( - { error => - if (error.getMessage.contains(privateKey)) Prop.falsified :| "Secret data in the error message" - else Prop.passed :| "Secret data redacted" - }, - _ => Prop.falsified :| "Data should not be parseable" + .redeem( + error => + if (error.getMessage.contains(privateKey)) PropF.falsified[IO].label("Secret data in the error message") + else PropF.passed[IO].label("Secret data redacted"), + _ => PropF.falsified[IO].label("Data should not be parseable") ) } } -property("listSecrets works as expected when requesting keys under path") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("listSecrets works as expected when requesting keys under path") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => Vault.listSecrets[IO](mockClient, uri)(clientToken, "/secret/postgres/") - .unsafeRunSync() == VaultKeys(List("postgres1", "postgres-pupper")) + .assertEquals(VaultKeys(List("postgres1", "postgres-pupper"))) } } -property("renewToken works as expected when sending a valid token") { - Prop.forAll(VaultArbitraries.validVaultUri){uri => +test("renewToken works as expected when sending a valid token") { + PropF.forAllF(VaultArbitraries.validVaultUri){uri => Vault.renewSelfToken[IO](mockClient, uri)(VaultToken(clientToken, 3600, true), 1.hour) - .unsafeRunSync() === VaultToken(clientToken, 3600, renewable) + .assertEquals(VaultToken(clientToken, 3600, renewable)) } } -property("revokeToken works as expected when revoking a valid token") { - Prop.forAll(VaultArbitraries.validVaultUri){ uri => - Vault.revokeSelfToken[IO](mockClient, uri)(VaultToken(clientToken, 3600, true)).unsafeRunSync() ===( () ) +test("revokeToken works as expected when revoking a valid token") { + PropF.forAllF(VaultArbitraries.validVaultUri){ uri => + Vault.revokeSelfToken[IO](mockClient, uri)(VaultToken(clientToken, 3600, true)).assertEquals(()) } } -property("renewLease works as expected when sending valid input arguments") { - Prop.forAll(VaultArbitraries.validVaultUri) { uri => - Vault.renewLease(mockClient, uri)(leaseId, increment, clientToken).unsafeRunSync() == VaultSecretRenewal(leaseDuration, leaseId, renewable) +test("renewLease works as expected when sending valid input arguments") { + PropF.forAllF(VaultArbitraries.validVaultUri) { uri => + Vault.renewLease(mockClient, uri)(leaseId, increment, clientToken).assertEquals(VaultSecretRenewal(leaseDuration, leaseId, renewable)) } } -property("revokeLease works as expected when sending valid input arguments") { - Prop.forAll(VaultArbitraries.validVaultUri) { uri => - Vault.revokeLease(mockClient, uri)(clientToken, leaseId).unsafeRunSync() ===( () ) +test("revokeLease works as expected when sending valid input arguments") { + PropF.forAllF(VaultArbitraries.validVaultUri) { uri => + Vault.revokeLease(mockClient, uri)(clientToken, leaseId).assertEquals(()) } } -property("generateCertificate works as expected when sending a valid token") { - Prop.forAll(VaultArbitraries.validVaultUri, VaultArbitraries.certRequestGen) { (uri, certRequest) => +test("generateCertificate works as expected when sending a valid token") { + PropF.forAllF(VaultArbitraries.validVaultUri, VaultArbitraries.certRequestGen) { (uri, certRequest) => Vault.generateCertificate(mockClient, uri)(clientToken, generateCertsPath, certRequest) - .unsafeRunSync() === VaultSecret(CertificateData(certificate, issuing_ca, List(ca_chain), private_key, private_key_type, serial_number), leaseDuration.some, leaseId.some, renewable.some) + .assertEquals(VaultSecret(CertificateData(certificate, issuing_ca, List(ca_chain), private_key, private_key_type, serial_number), leaseDuration.some, leaseId.some, renewable.some)) } } -property("loginAndKeepSecretLeased fails when wait duration is longer than lease duration") { - Prop.forAll( +test("loginAndKeepSecretLeased fails when wait duration is longer than lease duration") { + PropF.forAllF( VaultArbitraries.validVaultUri, Arbitrary.arbitrary[FiniteDuration], Arbitrary.arbitrary[FiniteDuration] - ) { case (uri, leaseDuration, waitInterval) => leaseDuration < waitInterval ==> { - import scala.concurrent.ExecutionContext.global - implicit val t = IO.timer(global) - implicit val ct = IO.contextShift(global) + ) { case (uri, leaseDuration, waitInterval) => PropF.boolean[IO](leaseDuration < waitInterval) ==> { Vault.loginAndKeepSecretLeased[IO, Unit](mockClient, uri)(validRoleId, "", leaseDuration, waitInterval) .attempt .compile .last - .unsafeRunSync() == Some(Left(Vault.InvalidRequirement("waitInterval longer than requested Lease Duration"))) + .assertEquals(Some(Left(Vault.InvalidRequirement("waitInterval longer than requested Lease Duration")))) }} } From f4143ea88c78da73edd1d42b200b7a6e802ff643 Mon Sep 17 00:00:00 2001 From: "Ross A. Baker" Date: Fri, 2 Jul 2021 13:26:08 -0400 Subject: [PATCH 2/2] Remove unused import --- core/src/test/scala/com/banno/vault/VaultSpec.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/test/scala/com/banno/vault/VaultSpec.scala b/core/src/test/scala/com/banno/vault/VaultSpec.scala index f03ec75c..6f36df8a 100644 --- a/core/src/test/scala/com/banno/vault/VaultSpec.scala +++ b/core/src/test/scala/com/banno/vault/VaultSpec.scala @@ -34,7 +34,6 @@ import scala.concurrent.duration._ import munit.{CatsEffectSuite, ScalaCheckSuite} import org.scalacheck._ import scala.util.Random -import org.scalacheck.Prop._ import org.scalacheck.effect.PropF import org.typelevel.ci.CIString