diff --git a/build.sbt b/build.sbt index 9c897caa..fad9e955 100644 --- a/build.sbt +++ b/build.sbt @@ -2,12 +2,12 @@ import Dependencies._ import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} import org.scalajs.sbtplugin.ScalaJSCrossVersion -val scala212 = "2.12.8" -val scala213 = "2.13.0-RC2" +val scala212 = "2.12.11" +val scala213 = "2.13.1" inThisBuild( List( - scalaVersion := "2.12.8", + scalaVersion := scala212, // crossScalaVersions := List(scala212, scala213), licenses += ("MIT", url("http://opensource.org/licenses/MIT")), homepage := Some(url("https://github.com/buildo/retro")), @@ -19,6 +19,7 @@ inThisBuild( url("https://github.com/gabro"), ), ), + testFrameworks += new TestFramework("munit.Framework"), ), ) @@ -118,16 +119,8 @@ lazy val metarpheusCore = crossProject(JSPlatform, JVMPlatform) .settings( name := "metarpheus-core", dynverTagPrefix := "metarpheus-", - ) - .jvmSettings( libraryDependencies ++= metarpheusCoreDependencies, ) - .jsSettings( - libraryDependencies ++= metarpheusCoreDependencies.map { dep => - if (dep.configurations == Some(Test.name)) dep - else dep.cross(ScalaJSCrossVersion.binary) - }, - ) lazy val metarpheusJsFacade = project .in(file("metarpheus/jsFacade")) @@ -135,7 +128,6 @@ lazy val metarpheusJsFacade = project .settings( name := "metarpheus-js-facade", scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }, - scalacOptions += "-P:scalajs:sjsDefinedByDefault", libraryDependencies ++= metarpheusJsFacadeDependencies.map(_.cross(ScalaJSCrossVersion.binary)), dynverTagPrefix := "metarpheus-", ) diff --git a/ci/pipeline.yml b/ci/pipeline.yml index f8f2fb32..8c4c8c9b 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -107,7 +107,7 @@ resources: icon: docker source: repository: buildo/scala-sbt-alpine - tag: 8u201_2.12.8_1.2.8 + tag: 8u201_2.12.11_1.3.8 jobs: diff --git a/ci/release.yml b/ci/release.yml index a8683650..6212e955 100644 --- a/ci/release.yml +++ b/ci/release.yml @@ -4,7 +4,7 @@ image_resource: type: docker-image source: repository: hseeberger/scala-sbt - tag: 8u181_2.12.8_1.2.8 + tag: 8u242_1.3.8_2.12.10 inputs: - name: retro diff --git a/enumero/ci/test.yml b/enumero/ci/test.yml index 328338dd..416e4dc7 100644 --- a/enumero/ci/test.yml +++ b/enumero/ci/test.yml @@ -4,7 +4,7 @@ image_resource: type: docker-image source: repository: buildo/scala-sbt-alpine - tag: 8u201_2.12.8_1.2.8 + tag: 8u201_2.12.11_1.3.8 inputs: - name: retro diff --git a/enumero/circe/src/test/scala/CirceSupportSpec.scala b/enumero/circe/src/test/scala/CirceSupportSpec.scala index 95de07c9..e1067922 100644 --- a/enumero/circe/src/test/scala/CirceSupportSpec.scala +++ b/enumero/circe/src/test/scala/CirceSupportSpec.scala @@ -1,12 +1,10 @@ import io.buildo.enumero._ import io.buildo.enumero.circe._ -import io.circe.{DecodingFailure, Json} import io.circe.syntax._ -import io.circe.parser._ +import io.circe.parser.parse -import org.scalatest.{Matchers, WordSpec} +class CirceSupportSuite extends munit.FunSuite { -class CirceSupportSpec extends WordSpec with Matchers { sealed trait Planet extends CaseEnum object Planet { case object Mercury extends Planet @@ -17,36 +15,35 @@ class CirceSupportSpec extends WordSpec with Matchers { val planetMap = Map[Planet, Int]( Planet.Mercury -> 12, Planet.Venus -> 812763, - Planet.Earth -> 0 + Planet.Earth -> 0, ) - val planetMapJson: Json = parse(""" + val planetMapJson = parse(""" { "Mercury": 12, "Venus": 812763, "Earth": 0 } - """).getOrElse(Json.Null) + """).right.get - "CirceSupport handles encoding of a map with CaseEnum keys" in { + test("CirceSupport handles encoding a map with CaseEnum keys") { val encodedJson = planetMap.asJson - - encodedJson shouldBe planetMapJson + assertEquals(encodedJson, planetMapJson) } - "CirceSupport handles dencoding of a json with CaseEnum keys" in { - planetMapJson.as[Map[Planet, Int]].getOrElse(Json.Null) shouldBe planetMap + test("CirceSupport handles decoding a json with CaseEnum keys") { + assertEquals(planetMapJson.as[Map[Planet, Int]].right.get, planetMap) } - "CirceSupport handles dencoding of a json with wrong CaseEnum keys" in { - parse(""" + test("CirceSupport handles decoding a json with wrong CaseEnum keys") { + val decodeResult = parse(""" { "Mercury": 12, "Venus": 812763, "wrongKey": 0 } - """) - .getOrElse(Json.Null) - .as[Map[Planet, Int]] shouldBe a[Left[_, DecodingFailure]] + """).right.get + .as[Map[Planet, Int]] + assert(decodeResult.isLeft) } } diff --git a/enumero/core/src/test/scala/CaseEnumIndex.scala b/enumero/core/src/test/scala/CaseEnumIndex.scala index 8c6f43f8..3d53521e 100644 --- a/enumero/core/src/test/scala/CaseEnumIndex.scala +++ b/enumero/core/src/test/scala/CaseEnumIndex.scala @@ -1,8 +1,7 @@ import io.buildo.enumero._ +import scala.language.reflectiveCalls -import org.scalatest.{Matchers, WordSpec} - -class CaseEnumIndexSpec extends WordSpec with Matchers { +class CaseEnumIndexSuite extends munit.FunSuite { sealed trait Planet extends IndexedCaseEnum { type Index = Int } object Planet { case object Mercury extends Planet { val index = 1 } @@ -10,57 +9,53 @@ class CaseEnumIndexSpec extends WordSpec with Matchers { case object Earth extends Planet { val index = 3 } } - "CaseEnumIndexMacro" should { - "construct a sensible CaseEnumIndex" in { - val converter = CaseEnumIndex.caseEnumIndex[Planet] + test("CaseEnumIndexMacro should construct a sensible CaseEnumIndex") { + val converter = CaseEnumIndex.caseEnumIndex[Planet] - val pairs = List(Planet.Mercury -> 1, Planet.Venus -> 2, Planet.Earth -> 3) + val pairs = List(Planet.Mercury -> 1, Planet.Venus -> 2, Planet.Earth -> 3) - for ((co, index) <- pairs) { - converter.caseToIndex(co).shouldBe(index) - converter.caseFromIndex(index).shouldBe(Some(co)) - } + for ((co, index) <- pairs) { + assertEquals(converter.caseToIndex(co), index) + assertEquals(converter.caseFromIndex(index), Some(co)) } } - "CaseEnumIndex" should { - "provide the typeclass instance" in { - trait FakeBinaryPickler[T] { - def pickle(c: T)(picklerState: { def writeInt(int: Int) }): Unit - def unpickle(unpicklerState: { def getInt(): Int }): Option[T] - } + test("CaseEnumIndex should provide the typeclass instance") { + trait FakeBinaryPickler[T] { + def pickle(c: T)(picklerState: { def writeInt(int: Int): Unit }): Unit + def unpickle(unpicklerState: { def getInt(): Int }): Option[T] + } - implicit def fakeBinaryPickler[T <: IndexedCaseEnum { type Index = Int }]( - implicit instance: CaseEnumIndex[T] - ) = new FakeBinaryPickler[T] { + implicit def fakeBinaryPickler[T <: IndexedCaseEnum { type Index = Int }]( + implicit instance: CaseEnumIndex[T], + ) = new FakeBinaryPickler[T] { - def pickle(c: T)(picklerState: { def writeInt(int: Int) }): Unit = { - picklerState.writeInt(instance.caseToIndex(c)) - } - def unpickle(unpicklerState: { def getInt(): Int }): Option[T] = { - instance.caseFromIndex(unpicklerState.getInt()) - } + def pickle(c: T)(picklerState: { def writeInt(int: Int): Unit }): Unit = { + picklerState.writeInt(instance.caseToIndex(c)) } - - object picklerState { - var value: Int = 0 - def writeInt(int: Int): Unit = { - value = int - } + def unpickle(unpicklerState: { def getInt(): Int }): Option[T] = { + instance.caseFromIndex(unpicklerState.getInt()) } - val binaryPickler = implicitly[FakeBinaryPickler[Planet]] - binaryPickler.pickle(Planet.Venus)(picklerState) - picklerState.value.shouldBe(2) + } - object unpicklerState { - def getInt(): Int = 3 + object picklerState { + var value: Int = 0 + def writeInt(int: Int): Unit = { + value = int } - binaryPickler.unpickle(unpicklerState).shouldBe(Some(Planet.Earth)) } + val binaryPickler = implicitly[FakeBinaryPickler[Planet]] + binaryPickler.pickle(Planet.Venus)(picklerState) + assertEquals(picklerState.value, 2) - "retrieve a typeclass instance using apply" in { - CaseEnumIndex[Planet].caseFromIndex(1) shouldBe Some(Planet.Mercury) + object unpicklerState { + def getInt(): Int = 3 } + assertEquals(binaryPickler.unpickle(unpicklerState), Some(Planet.Earth)) + } + test("CaseEnumIndex should retrieve a typeclass instance using apply") { + assertEquals(CaseEnumIndex[Planet].caseFromIndex(1), Some(Planet.Mercury)) } + } diff --git a/enumero/core/src/test/scala/CaseEnumMacroSpec.scala b/enumero/core/src/test/scala/CaseEnumMacroSpec.scala index dfc3d2fa..0b796ebb 100644 --- a/enumero/core/src/test/scala/CaseEnumMacroSpec.scala +++ b/enumero/core/src/test/scala/CaseEnumMacroSpec.scala @@ -1,8 +1,6 @@ import io.buildo.enumero.annotations.{enum, indexedEnum} -import org.scalatest.{Matchers, WordSpec} - -class CaseEnumMacroSpec extends WordSpec with Matchers { +class CaseEnumMacroSuite extends munit.FunSuite { @enum trait Planet { Mercury Venus @@ -14,43 +12,39 @@ class CaseEnumMacroSpec extends WordSpec with Matchers { object Ale } - "@enum annotation" should { - "produce a valid CaseEnum-style ADT" in { - Planet.Earth shouldBe a[Product] - Planet.Earth shouldBe a[Serializable] - Planet.Earth shouldBe a[Planet] - Planet.Mercury should not be a[Planet.Earth.type] - Planet.Earth shouldBe Planet.Earth - } + test("@enum annotation should produce a valid CaseEnum-style ADT") { + assert(Planet.Earth.isInstanceOf[Product]) + assert(Planet.Earth.isInstanceOf[Serializable]) + assert(Planet.Earth.isInstanceOf[Planet]) + assertEquals(Planet.Earth, Planet.Earth) + } - "produce a valid CaseEnum-style ADT (alternative syntax)" in { - Beer.Lager shouldBe a[Product] - Beer.Lager shouldBe a[Serializable] - Beer.Lager shouldBe a[Beer] - Beer.Ale should not be a[Beer.Lager.type] - Beer.Lager shouldBe Beer.Lager - } + test("@enum annotation should produce a valid CaseEnum-style ADT (alternative syntax)") { + assert(Beer.Lager.isInstanceOf[Product]) + assert(Beer.Lager.isInstanceOf[Serializable]) + assert(Beer.Lager.isInstanceOf[Beer]) + assertEquals(Beer.Lager, Beer.Lager) + } - "allow accessing the values of the enumeration" in { - val typecheck: Set[Planet] = Planet.values - Planet.values shouldBe Set(Planet.Mercury, Planet.Venus, Planet.Earth) - } + test("@enum annotation should allow accessing the values of the enumeration") { + (Planet.values: Set[Planet]) // typecheck + assertEquals(Planet.values, Set(Planet.Mercury, Planet.Venus, Planet.Earth)) + } - "allow printing / parsing the values of the enumeration" in { - Planet.caseFromString("Earth") shouldBe Some(Planet.Earth) - Planet.caseFromString("Nope") shouldBe None - Planet.caseToString(Planet.Earth) shouldBe "Earth" - "Planet.caseToString(Beer.Lager)" shouldNot typeCheck - } + test("@enum annotation should allow printing / parsing the values of the enumeration") { + assertEquals(Planet.caseFromString("Earth"), Some(Planet.Earth)) + assertEquals(Planet.caseFromString("Nope"), None) + assertEquals(Planet.caseToString(Planet.Earth), "Earth") + compileErrors("Planet.caseToString(Beer.Lager)") + } - "allow accessing the enumeration name" in { - Planet.name shouldBe "Planet" - } + test("@enum annotation should allow accessing the enumeration name") { + assertEquals(Planet.name, "Planet") } } -class IndexedCaseEnumMacroSpec extends WordSpec with Matchers { +class IndexedCaseEnumMacroSpec extends munit.FunSuite { @indexedEnum trait Planet { type Index = Int Mercury { 1 } @@ -64,26 +58,24 @@ class IndexedCaseEnumMacroSpec extends WordSpec with Matchers { Ale { 2 } } - "@indexedEnum annotation" should { - "produce a valid IndexedCaseEnum-style ADT" in { - val typecheck: Int = 3: Planet#Index - Planet.Earth shouldBe a[Product] - Planet.Earth shouldBe a[Serializable] - Planet.Earth shouldBe a[Planet] - Planet.Mercury should not be a[Planet.Earth.type] - Planet.Earth shouldBe Planet.Earth - Planet.Earth.index shouldBe 3 - } + test("@indexedEnum annotation should produce a valid IndexedCaseEnum-style ADT") { + val _: Int = 3: Planet#Index // typecheck + assert(Planet.Earth.isInstanceOf[Product]) + assert(Planet.Earth.isInstanceOf[Serializable]) + assert(Planet.Earth.isInstanceOf[Planet]) + assertEquals(Planet.Earth, Planet.Earth) + assertEquals(Planet.Earth.index, 3) + } - "produce a valid IndexedCaseEnum-style ADT (alternative syntax)" in { - val typecheck: Int = 2: Planet#Index - Beer.Lager shouldBe a[Product] - Beer.Lager shouldBe a[Serializable] - Beer.Lager shouldBe a[Beer] - Beer.Ale should not be a[Beer.Lager.type] - Beer.Lager shouldBe Beer.Lager - Beer.Ale.index shouldBe 2 - } + test( + "@indexedEnum annotation should produce a valid IndexedCaseEnum-style ADT (alternative syntax)", + ) { + val _: Int = 2: Planet#Index // typecheck + assert(Beer.Lager.isInstanceOf[Product]) + assert(Beer.Lager.isInstanceOf[Serializable]) + assert(Beer.Lager.isInstanceOf[Beer]) + assertEquals(Beer.Lager, Beer.Lager) + assertEquals(Beer.Ale.index, 2) } } diff --git a/enumero/core/src/test/scala/CaseEnumSpec.scala b/enumero/core/src/test/scala/CaseEnumSpec.scala index 83426cc5..6a2b3d50 100644 --- a/enumero/core/src/test/scala/CaseEnumSpec.scala +++ b/enumero/core/src/test/scala/CaseEnumSpec.scala @@ -1,8 +1,6 @@ import io.buildo.enumero._ -import org.scalatest.{Matchers, WordSpec} - -class CaseEnumSpec extends WordSpec with Matchers { +class CaseEnumSpec extends munit.FunSuite { sealed trait Planet extends CaseEnum object Planet { case object Mercury extends Planet @@ -10,55 +8,49 @@ class CaseEnumSpec extends WordSpec with Matchers { case object Earth extends Planet } - "CaseEnumMacro" should { - "construct a sensible CaseEnumSerialization" in { - val serialization = CaseEnumSerialization.caseEnumSerialization[Planet] + test("CaseEnumMacro should construct a sensible CaseEnumSerialization") { + val serialization = CaseEnumSerialization.caseEnumSerialization[Planet] - val pairs = - List(Planet.Mercury -> "Mercury", Planet.Venus -> "Venus", Planet.Earth -> "Earth") + val pairs = + List(Planet.Mercury -> "Mercury", Planet.Venus -> "Venus", Planet.Earth -> "Earth") - for ((co, str) <- pairs) { - serialization.caseToString(co).shouldBe(str) - serialization.caseFromString(str).shouldBe(Some(co)) - } + for ((co, str) <- pairs) { + assertEquals(serialization.caseToString(co), str) + assertEquals(serialization.caseFromString(str), Some(co)) } } - "SerializationSupport" should { - "provide the typeclass instance" in { - trait FakeJsonSerializer[T] { - def toString(value: T): String - def fromString(str: String): Either[String, T] - } + test("SerializationSupport should provide the typeclass instance") { + trait FakeJsonSerializer[T] { + def toString(value: T): String + def fromString(str: String): Either[String, T] + } - implicit def fakeJsonSerializer[T <: CaseEnum](implicit instance: CaseEnumSerialization[T]) = - new FakeJsonSerializer[T] { - def toString(value: T): String = instance.caseToString(value) - def fromString(str: String): Either[String, T] = - instance.caseFromString(str) match { - case Some(v) => Right(v) - case None => - Left( - s"$str is not a valid ${instance.name}. Valid values are: ${instance.values.mkString(", ")}" - ) - } - } + implicit def fakeJsonSerializer[T <: CaseEnum](implicit instance: CaseEnumSerialization[T]) = + new FakeJsonSerializer[T] { + def toString(value: T): String = instance.caseToString(value) + def fromString(str: String): Either[String, T] = + instance.caseFromString(str) match { + case Some(v) => Right(v) + case None => + Left( + s"$str is not a valid ${instance.name}. Valid values are: ${instance.values.mkString(", ")}", + ) + } + } - implicitly[FakeJsonSerializer[Planet]] - .fromString("Mercury") - .shouldBe(Right(Planet.Mercury)) - implicitly[FakeJsonSerializer[Planet]] - .fromString("Wrong") - .shouldBe( - Left( - "Wrong is not a valid Planet. Valid values are: Mercury, Venus, Earth" - ) - ) - } + assertEquals( + implicitly[FakeJsonSerializer[Planet]].fromString("Mercury"), + Right(Planet.Mercury), + ) + assertEquals( + implicitly[FakeJsonSerializer[Planet]].fromString("Wrong"), + Left("Wrong is not a valid Planet. Valid values are: Mercury, Venus, Earth"), + ) + } - "retrieve a typeclass instance using apply" in { - CaseEnumSerialization[Planet].caseFromString("Mercury") shouldBe Some(Planet.Mercury) - } + test("retrieve a typeclass instance using apply") { + assertEquals(CaseEnumSerialization[Planet].caseFromString("Mercury"), Some(Planet.Mercury)) } } diff --git a/mailo/ci/test.yml b/mailo/ci/test.yml index adcaf46a..bc1bd559 100644 --- a/mailo/ci/test.yml +++ b/mailo/ci/test.yml @@ -4,7 +4,7 @@ image_resource: type: docker-image source: repository: buildo/scala-sbt-alpine - tag: 8u201_2.12.8_1.2.8 + tag: 8u201_2.12.11_1.3.8 inputs: - name: retro diff --git a/mailo/src/main/scala/mailo/AtLeastOnceMailo.scala b/mailo/src/main/scala/mailo/AtLeastOnceMailo.scala index 58f32c92..6d5c692c 100644 --- a/mailo/src/main/scala/mailo/AtLeastOnceMailo.scala +++ b/mailo/src/main/scala/mailo/AtLeastOnceMailo.scala @@ -1,8 +1,6 @@ package mailo -import java.util.concurrent.TimeUnit - -import akka.actor.{ActorSystem, Props} +import akka.actor.ActorSystem import akka.util.Timeout import akka.pattern.ask import com.typesafe.config.{Config, ConfigFactory} @@ -13,7 +11,6 @@ import mailo.persistence.{EmailPersistanceActor, LoggingActor, SendEmail} import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} -import scala.language.postfixOps case class MailPersistenceError(override val message: String) extends MailError(message) @@ -25,12 +22,13 @@ class AtLeastOnceMailo( ec: ExecutionContext, conf: Config = ConfigFactory.load(), system: ActorSystem = ActorSystem("mailo"), - enqueueTimeout: Timeout = Timeout(200 milliseconds) + enqueueTimeout: Timeout = Timeout(200 milliseconds), ) extends Mailo with LazyLogging { private[this] val emailSender = new EmailSender(data, client) private[this] val loggingActor = system.actorOf(LoggingActor.props()) - private[this] val emailPersistanceActor = system.actorOf(EmailPersistanceActor.props(emailSender, loggingActor)) + private[this] val emailPersistanceActor = + system.actorOf(EmailPersistanceActor.props(emailSender, loggingActor)) def send(mail: Mail): Future[Either[MailError, MailResult]] = { ask(emailPersistanceActor, SendEmail(mail)) diff --git a/mailo/src/main/scala/mailo/Helpers.scala b/mailo/src/main/scala/mailo/Helpers.scala index 65a1ddb3..e1c57e18 100644 --- a/mailo/src/main/scala/mailo/Helpers.scala +++ b/mailo/src/main/scala/mailo/Helpers.scala @@ -3,7 +3,6 @@ package mailo import java.io.{PrintWriter, StringWriter} import akka.actor.ActorSystem -import akka.stream.ActorMaterializer import mailo.data.S3MailData import mailo.http.MailgunClient @@ -12,8 +11,7 @@ import scala.concurrent.{ExecutionContext, Future} class S3MailgunMailo( implicit system: ActorSystem, - materializer: ActorMaterializer, - ec: ExecutionContext + ec: ExecutionContext, ) { private[this] val s3 = new S3MailData() private[this] val mailgun = new MailgunClient() @@ -29,16 +27,13 @@ class S3MailgunMailo( templateName: String, params: Map[String, String], attachments: List[Attachment] = Nil, - tags: List[String] = Nil + tags: List[String] = Nil, ): Future[Either[MailError, MailResponse]] = mailgunS3Mailo.send(Mail(to, from, cc, bcc, subject, templateName, params, attachments, tags)) } class S3SendinblueMailo( - implicit - system: ActorSystem, - materializer: ActorMaterializer, - ec: ExecutionContext + implicit ec: ExecutionContext, ) { import data.S3MailData import http.SendinblueClient @@ -57,10 +52,10 @@ class S3SendinblueMailo( templateName: String, params: Map[String, String], attachments: List[Attachment] = Nil, - tags: List[String] = Nil + tags: List[String] = Nil, ): Future[Either[MailError, MailResponse]] = sendinblueS3Mailo.send( - Mail(to, from, cc, bcc, subject, templateName, params, attachments, tags) + Mail(to, from, cc, bcc, subject, templateName, params, attachments, tags), ) } diff --git a/mailo/src/main/scala/mailo/data/S3MailData.scala b/mailo/src/main/scala/mailo/data/S3MailData.scala index b1867ed2..b0ef92a6 100644 --- a/mailo/src/main/scala/mailo/data/S3MailData.scala +++ b/mailo/src/main/scala/mailo/data/S3MailData.scala @@ -2,11 +2,11 @@ package mailo.data import awscala.s3.{Bucket, S3} import awscala.Credentials -import com.typesafe.config.{Config, ConfigFactory} +import com.typesafe.config.ConfigFactory +import com.typesafe.config.Config import com.amazonaws.regions.RegionUtils import cats.syntax.either._ import cats.syntax.traverse._ -import cats.instances.map._ import cats.instances.either._ import alleycats.std.all._ import mailo.data.S3MailDataError._ @@ -15,13 +15,12 @@ import scala.concurrent.Future import scala.concurrent.ExecutionContext import mailo.{MailError, MailRawContent} -import scala.language.postfixOps import scala.util.{Failure, Success, Try} class S3MailData( implicit ec: ExecutionContext, - conf: com.typesafe.config.Config = ConfigFactory.load() + conf: Config = ConfigFactory.load(), ) extends MailData { type EitherMailError[A] = Either[MailError, A] @@ -30,7 +29,7 @@ class S3MailData( secret: String, bucket: String, regionName: String, - partialsFolder: String + partialsFolder: String, ) private[S3MailData] val s3Config = S3Config( @@ -38,7 +37,7 @@ class S3MailData( secret = conf.getString(s"mailo.s3.secret"), bucket = conf.getString(s"mailo.s3.bucket"), regionName = conf.getString(s"mailo.s3.region"), - partialsFolder = conf.getString(s"mailo.s3.partialsFolder") + partialsFolder = conf.getString(s"mailo.s3.partialsFolder"), ) private[S3MailData] implicit val region = RegionUtils.getRegion(s3Config.regionName) @@ -53,7 +52,8 @@ class S3MailData( //filtering partial objects dropping initial chars partialObjects <- { val result: EitherMailError[Map[String, String]] = (partials - .map(n => n.drop(folder.length + 1) -> getObject(n)).toMap) + .map(n => n.drop(folder.length + 1) -> getObject(n)) + .toMap) .sequence[EitherMailError, String] result } @@ -72,20 +72,20 @@ class S3MailData( case None => ObjectNotFound.asLeft[Set[String]] }) match { case Success(result) => result - case Failure(e) => S3InternalError(e.getMessage).asLeft[Set[String]] + case Failure(e) => S3InternalError(e.getMessage).asLeft[Set[String]] } } private[this] def getObject(name: String): Either[MailError, String] = - Try(bucket match { - case Some(b) => - b.getObject(name) match { - case Some(o) => convertStreamToString(o.content).asRight[MailError] - case None => ObjectNotFound.asLeft[String] - } - case None => BucketNotFound.asLeft[String] - }) match { - case Success(result) => result - case Failure(e) => S3InternalError(e.getMessage).asLeft[String] - } + Try(bucket match { + case Some(b) => + b.getObject(name) match { + case Some(o) => convertStreamToString(o.content).asRight[MailError] + case None => ObjectNotFound.asLeft[String] + } + case None => BucketNotFound.asLeft[String] + }) match { + case Success(result) => result + case Failure(e) => S3InternalError(e.getMessage).asLeft[String] + } } diff --git a/mailo/src/main/scala/mailo/http/MailgunClient.scala b/mailo/src/main/scala/mailo/http/MailgunClient.scala index 1bea1603..b306d9ce 100644 --- a/mailo/src/main/scala/mailo/http/MailgunClient.scala +++ b/mailo/src/main/scala/mailo/http/MailgunClient.scala @@ -9,7 +9,6 @@ import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials, Ra import akka.http.scaladsl.marshalling._ import akka.http.scaladsl.unmarshalling._ -import akka.stream.ActorMaterializer import akka.stream.scaladsl.Source import akka.actor.ActorSystem @@ -31,8 +30,7 @@ import util._ class MailgunClient( implicit system: ActorSystem, - materializer: ActorMaterializer, - conf: Config = ConfigFactory.load() + conf: Config = ConfigFactory.load(), ) extends MailClient with MimeMailClient with LazyLogging { @@ -42,7 +40,7 @@ class MailgunClient( private[this] case class MailgunConfig(key: String, uri: String) private[this] val mailgunConfig = MailgunConfig( key = conf.getString("mailo.mailgun.key"), - uri = conf.getString("mailo.mailgun.uri") + uri = conf.getString("mailo.mailgun.uri"), ) private[this] val auth = Authorization(BasicHttpCredentials("api", mailgunConfig.key)) @@ -50,25 +48,25 @@ class MailgunClient( message: MimeMessage, tags: List[String] = List.empty, attachments: List[Attachment] = List.empty, - headers: Map[String, String] = Map.empty + headers: Map[String, String] = Map.empty, )( implicit - executionContext: ExecutionContext + executionContext: ExecutionContext, ): Future[Either[MailError, MailResponse]] = { val inputs = for { toRecipients <- message.getRecipients(RecipientType.TO) match { case addresses if addresses.nonEmpty => addresses.asRight case _ => InvalidInput("No recipients in MimeMessage").asLeft } - ccRecipients <- message.getRecipients(RecipientType.CC).asRight - bccRecipients <- message.getRecipients(RecipientType.BCC).asRight - fromAddress <- Either.fromOption( + _ <- message.getRecipients(RecipientType.CC).asRight + _ <- message.getRecipients(RecipientType.BCC).asRight + _ <- Either.fromOption( Option(message.getFrom()), - InvalidInput("No 'from' in MimeMessage"): MailError + InvalidInput("No 'from' in MimeMessage"): MailError, ) - subject <- Either.fromOption( + _ <- Either.fromOption( Option(message.getSubject()), - InvalidInput("No 'subject' in MimeMessage"): MailError + InvalidInput("No 'subject' in MimeMessage"): MailError, ) } yield { val to = toRecipients.map(_.toString).mkString(",") @@ -88,7 +86,7 @@ class MailgunClient( method = HttpMethods.POST, uri = s"${mailgunConfig.uri}/messages.mime", headers = List(auth), - entity = entity + entity = entity, ) res <- EitherT(sendRequest(request)) } yield res).value @@ -103,10 +101,10 @@ class MailgunClient( content: MailRefinedContent, attachments: List[Attachment], tags: List[String], - headers: Map[String, String] = Map.empty + headers: Map[String, String] = Map.empty, )( implicit - executionContext: scala.concurrent.ExecutionContext + executionContext: scala.concurrent.ExecutionContext, ): Future[Either[MailError, MailResponse]] = for { entity <- entity( @@ -118,20 +116,20 @@ class MailgunClient( content = content, attachments = attachments, tags = tags, - headers = headers + headers = headers, ) request = HttpRequest( method = HttpMethods.POST, uri = s"${mailgunConfig.uri}/messages", headers = List(auth), - entity = entity + entity = entity, ) res <- sendRequest(request) } yield res private[this] def sendRequest(request: HttpRequest)( implicit - ec: ExecutionContext + ec: ExecutionContext, ): Future[Either[MailError, MailResponse]] = { import de.heikoseeberger.akkahttpcirce.ErrorAccumulatingCirceSupport._ import io.circe.generic.auto._ @@ -159,7 +157,7 @@ class MailgunClient( name: String, `type`: ContentType, content: String, - transferEncoding: Option[String] = None + transferEncoding: Option[String], ) = { Multipart.FormData.BodyPart.Strict( name = "attachment", @@ -168,7 +166,7 @@ class MailgunClient( additionalHeaders = transferEncoding match { case Some(e) => List(RawHeader("Content-Transfer-Encoding", e)) case None => Nil - } + }, ) } @@ -183,7 +181,7 @@ class MailgunClient( message: String, tags: List[String], headers: Map[String, String], - attachments: List[Attachment] + attachments: List[Attachment], )(implicit ec: ExecutionContext): Future[RequestEntity] = { val attachmentsForm = attachments.map( @@ -192,8 +190,8 @@ class MailgunClient( attachment.name, attachment.`type`, attachment.content, - attachment.transferEncoding - ) + attachment.transferEncoding, + ), ) val multipartForm = Multipart.FormData( @@ -202,11 +200,11 @@ class MailgunClient( Multipart.FormData.BodyPart.Strict( name = "message", entity = HttpEntity(ContentTypes.`text/plain(UTF-8)`, ByteString(message)), - additionalDispositionParams = Map("filename" -> "message") + additionalDispositionParams = Map("filename" -> "message"), ), - Multipart.FormData.BodyPart.Strict("to", to) - ) ++ tagsForm(tags) ++ headersForm(headers) ++ attachmentsForm - ) + Multipart.FormData.BodyPart.Strict("to", to), + ) ++ tagsForm(tags) ++ headersForm(headers) ++ attachmentsForm, + ), ) Marshal(multipartForm).to[RequestEntity] @@ -221,10 +219,10 @@ class MailgunClient( content: MailRefinedContent, attachments: List[Attachment], tags: List[String], - headers: Map[String, String] + headers: Map[String, String], )( implicit - executionCon: scala.concurrent.ExecutionContext + executionCon: scala.concurrent.ExecutionContext, ): Future[RequestEntity] = { import mailo.MailRefinedContent._ @@ -239,8 +237,8 @@ class MailgunClient( attachment.name, attachment.`type`, attachment.content, - attachment.transferEncoding - ) + attachment.transferEncoding, + ), ) val multipartForm = Multipart.FormData( @@ -248,12 +246,12 @@ class MailgunClient( List( Multipart.FormData.BodyPart.Strict("from", from), Multipart.FormData.BodyPart.Strict("to", to), - Multipart.FormData.BodyPart.Strict("subject", subject) + Multipart.FormData.BodyPart.Strict("subject", subject), ) ++ List( cc.map(Multipart.FormData.BodyPart.Strict("cc", _)), - bcc.map(Multipart.FormData.BodyPart.Strict("bcc", _)) - ).flatten ++ tagsForm(tags) ++ attachmentsForm ++ headersForm(headers) :+ contentForm - ) + bcc.map(Multipart.FormData.BodyPart.Strict("bcc", _)), + ).flatten ++ tagsForm(tags) ++ attachmentsForm ++ headersForm(headers) :+ contentForm, + ), ) Marshal(multipartForm).to[RequestEntity] diff --git a/mailo/src/main/scala/mailo/http/SMTPClient.scala b/mailo/src/main/scala/mailo/http/SMTPClient.scala index 711f3423..f24342db 100644 --- a/mailo/src/main/scala/mailo/http/SMTPClient.scala +++ b/mailo/src/main/scala/mailo/http/SMTPClient.scala @@ -12,52 +12,53 @@ import scala.util._ import javax.mail._ import mailo.MailRefinedContent.{HTMLContent, TEXTContent} import mailo.http.MailClientError.{BadRequest, UnknownError} +import scala.util.control.NonFatal -class SMTPClient(implicit conf: Config = ConfigFactory.load()) extends MailClient with MimeMailClient with LazyLogging{ +class SMTPClient(implicit conf: Config = ConfigFactory.load()) + extends MailClient + with MimeMailClient + with LazyLogging { lazy val server = conf.getString("mailo.smtp.server") lazy val port = conf.getString("mailo.smtp.port") - private [this] def internalSend(message: MimeMessage, addresses: Array[Address]) = + private[this] def internalSend(message: MimeMessage) = Try(Transport.send(message, message.getAllRecipients)) match { case Success(_) => Future.successful(Right(MailResponse(UUID.randomUUID().toString, "ok"))) - case Failure(exception: SendFailedException) => - Future.successful(Left(UnknownError(exception.getMessage))) - case Failure(exception: MessagingException) => + case Failure(NonFatal(exception)) => Future.successful(Left(UnknownError(exception.getMessage))) } //attachments - tags guard - private [this] def internalSend( - message: MimeMessage, - addresses: Array[Address], - attachments: List[Attachment], - tags: List[String]): Future[Either[MailError, MailResponse]] = { + private[this] def internalSend( + message: MimeMessage, + attachments: List[Attachment], + tags: List[String], + ): Future[Either[MailError, MailResponse]] = { if (attachments.nonEmpty) { logger.error("attachments with smtp not yet supported") Future.successful(Left(BadRequest)) - } - else if (tags.nonEmpty) { + } else if (tags.nonEmpty) { logger.error("tags with smtp not yet supported") Future.successful(Left(BadRequest)) - } - else internalSend(message, addresses) + } else internalSend(message) } - private[this] def send(from: InternetAddress, - to : Array[Address], - recipients: String, - subject: String, - content: MailRefinedContent.MailRefinedContent, - attachments: List[Attachment], - tags: List[String], - headers: Map[String, String]) = { + private[this] def send( + from: InternetAddress, + recipients: String, + subject: String, + content: MailRefinedContent.MailRefinedContent, + attachments: List[Attachment], + tags: List[String], + headers: Map[String, String], + ) = { val props = new Properties props.put("mail.smtp.host", server) props.put("mail.smtp.port", port) props.put("mail.smtp.auth", "false") val session = Session.getInstance(props, null) - val msg = new MimeMessage(session){ + val msg = new MimeMessage(session) { setFrom(from) setRecipients(Message.RecipientType.TO, recipients) setSubject(subject) @@ -70,45 +71,44 @@ class SMTPClient(implicit conf: Config = ConfigFactory.load()) extends MailClien } headers.foreach(h => msg.addHeader(h._1, h._2)) - internalSend(msg, to, attachments, tags) + internalSend(msg, attachments, tags) } override def send( - to: String, - from: String, - cc: Option[String], - bcc: Option[String], - subject: String, - content: MailRefinedContent.MailRefinedContent, - attachments: List[Attachment], - tags: List[String], - headers: Map[String, String] - )( - implicit executionContext: ExecutionContext - ): Future[ - Either[MailError, MailResponse] - ] = { + to: String, + from: String, + cc: Option[String], + bcc: Option[String], + subject: String, + content: MailRefinedContent.MailRefinedContent, + attachments: List[Attachment], + tags: List[String], + headers: Map[String, String], + )( + implicit executionContext: ExecutionContext, + ): Future[ + Either[MailError, MailResponse], + ] = { val recipients = to + cc.map(a => s"; $a").getOrElse("") //handling addresses parsing like mailo wants it - (for{ + (for { from <- Try(new InternetAddress(from)) - to <- Try(Array(Some(to),cc,bcc).flatMap(_.map(a => new InternetAddress(a).asInstanceOf[Address]))) - } yield send(from, to, recipients, subject, content, attachments, tags, headers)).getOrElse( - Future.successful(Left(BadRequest)) + } yield send(from, recipients, subject, content, attachments, tags, headers)).getOrElse( + Future.successful(Left(BadRequest)), ) } override def sendMime( - message: MimeMessage, - tags: List[String], - attachments: List[Attachment], - headers: Map[String, String] - )( - implicit executionContext: ExecutionContext - ): Future[ - Either[MailError, MailResponse] - ] = { + message: MimeMessage, + tags: List[String], + attachments: List[Attachment], + headers: Map[String, String], + )( + implicit executionContext: ExecutionContext, + ): Future[ + Either[MailError, MailResponse], + ] = { headers.foreach(h => message.addHeader(h._1, h._2)) - internalSend(message, message.getAllRecipients, attachments, tags) + internalSend(message, attachments, tags) } } diff --git a/mailo/src/main/scala/mailo/http/SendinblueClient.scala b/mailo/src/main/scala/mailo/http/SendinblueClient.scala index f21ac0d1..4448f847 100644 --- a/mailo/src/main/scala/mailo/http/SendinblueClient.scala +++ b/mailo/src/main/scala/mailo/http/SendinblueClient.scala @@ -3,9 +3,6 @@ package http import com.typesafe.scalalogging.LazyLogging -import akka.stream.ActorMaterializer -import akka.actor.ActorSystem - import com.typesafe.config.{Config, ConfigFactory} import cats.syntax.either._ @@ -18,22 +15,19 @@ import sibApi.SmtpApi import MailClientError._ import MailRefinedContent._ -import scala.concurrent.{Future, ExecutionContext} +import scala.concurrent.{ExecutionContext, Future} import javax.mail.internet.MimeMessage class SendinblueClient( - implicit - system: ActorSystem, - materializer: ActorMaterializer, - conf: Config = ConfigFactory.load() + implicit conf: Config = ConfigFactory.load(), ) extends MailClient with MimeMailClient with LazyLogging { private[this] case class SendinblueConfig(key: String) private[this] val sendinblueConfig = SendinblueConfig( - key = conf.getString("mailo.sendinblue.key") + key = conf.getString("mailo.sendinblue.key"), ) private[this] val client = Configuration.getDefaultApiClient() private[this] val apiKey = client.getAuthentication("api-key").asInstanceOf[ApiKeyAuth] @@ -47,14 +41,13 @@ class SendinblueClient( message: MimeMessage, tags: List[String] = List.empty, attachments: List[Attachment] = List.empty, - headers: Map[String, String] = Map.empty + headers: Map[String, String] = Map.empty, )( implicit - executionContext: ExecutionContext + executionContext: ExecutionContext, ): Future[Either[MailError, MailResponse]] = throw new UnsupportedOperationException("unable to send mime messages in Sendinblue") - def send( to: String, from: String, @@ -64,22 +57,19 @@ class SendinblueClient( content: MailRefinedContent, attachments: List[Attachment], tags: List[String], - headers: Map[String, String] = Map.empty + headers: Map[String, String] = Map.empty, )( implicit - executionContext: scala.concurrent.ExecutionContext + executionContext: scala.concurrent.ExecutionContext, ): Future[Either[MailError, MailResponse]] = for { entity <- entity( from = from, to = to, - cc = cc, - bcc = bcc, subject = subject, content = content, attachments = attachments, tags = tags, - headers = headers ) res <- Future(sendinblue.sendTransacEmail(entity)).map { r => MailResponse(r.getMessageId(), "Email sent successfully.").asRight[MailError] @@ -94,13 +84,10 @@ class SendinblueClient( private[this] def entity( from: String, to: String, - cc: Option[String], - bcc: Option[String], subject: String, content: MailRefinedContent, attachments: List[Attachment], tags: List[String], - headers: Map[String, String] )(implicit ec: ExecutionContext): Future[SendSmtpEmail] = Future { import mailo.MailRefinedContent._ import collection.JavaConverters._ diff --git a/mailo/src/main/scala/mailo/parser/ParserError.scala b/mailo/src/main/scala/mailo/parser/ParserError.scala index c71dfdf3..cf8ddf4b 100644 --- a/mailo/src/main/scala/mailo/parser/ParserError.scala +++ b/mailo/src/main/scala/mailo/parser/ParserError.scala @@ -6,31 +6,31 @@ object ParserError { case object HtmlNotValid extends MailError("Content was not valid HTML") case class DisjointParametersAndMatches( justParams: Set[String], - justMatches: Set[String] + justMatches: Set[String], ) extends MailError( - "Disjoint parameters and matches, params (${justParams.toString}), matches (${jusetMatches.toString})" + s"Disjoint parameters and matches, params (${justParams.toString}), matches (${justMatches.toString})", ) case class OverlappedParametersAndMatches( justParams: Set[String], justMatches: Set[String], - overlap: Set[String] + overlap: Set[String], ) extends MailError( - s"Overlapped parameters and matches, but no exact match: just params (${justParams.toString}), just matches (${justMatches.toString}), overlapped params (${overlap.toString})" + s"Overlapped parameters and matches, but no exact match: just params (${justParams.toString}), just matches (${justMatches.toString}), overlapped params (${overlap.toString})", ) case class PartialsDoNotExist(partials: Set[String]) extends MailError( - s"Some of the provided partials do not exist, here is the list ${partials.toString}" + s"Some of the provided partials do not exist, here is the list ${partials.toString}", ) case class TooFewPartialsProvided(partials: Set[String]) extends MailError( - s"Too few partials provided to the document, here is the list ${partials.toString}" + s"Too few partials provided to the document, here is the list ${partials.toString}", ) case class TooFewParamsProvided(params: Set[String]) extends MailError( - s"Too few params provided to the document, here is the list ${params.toString}" + s"Too few params provided to the document, here is the list ${params.toString}", ) case class TooManyParamsProvided(params: Set[String]) extends MailError( - s"Too many params provided to the document, here is the list ${params.toString}" + s"Too many params provided to the document, here is the list ${params.toString}", ) } diff --git a/mailo/src/main/scala/mailo/persistence/EmailPersistanceActor.scala b/mailo/src/main/scala/mailo/persistence/EmailPersistanceActor.scala index e23f1f99..5fe3897a 100644 --- a/mailo/src/main/scala/mailo/persistence/EmailPersistanceActor.scala +++ b/mailo/src/main/scala/mailo/persistence/EmailPersistanceActor.scala @@ -1,16 +1,16 @@ package mailo.persistence -import scala.concurrent.Future -import scala.util.{Success, Failure} +import scala.util.{Failure, Success} import mailo.Mail -import akka.actor.{Actor, ActorLogging, Props, Status, ActorRef} +import akka.actor.Actor +import akka.actor.ActorLogging +import akka.actor.ActorRef +import akka.actor.Props import akka.persistence._ import io.circe.syntax._ import io.circe.generic.auto._ import io.circe.parser._ -import mailo.data.MailData -import mailo.http.MailClient case class SendEmail(email: Mail) case class EmailEvent(content: String) @@ -29,9 +29,11 @@ object LoggingActor { class EmailPersistanceActor( emailSender: mailo.Mailo, - deadLettersHandler: ActorRef + deadLettersHandler: ActorRef, ) extends PersistentActor - with ActorLogging with CustomContentTypeCodecs with AtLeastOnceDelivery { + with ActorLogging + with CustomContentTypeCodecs + with AtLeastOnceDelivery { import context.dispatcher override def persistenceId = "emails-persistence" @@ -40,22 +42,25 @@ class EmailPersistanceActor( def send(email: Mail) = emailSender.send(email) val receiveRecover: Receive = { - case e@EmailEvent(json) => + case e @ EmailEvent(json) => decode[Mail](json) match { - case Right(email) => send(email) + case Right(email) => + send(email) + () case Left(error) => deadLettersHandler ! EmailApplicativeErrorEvent(e, error.getMessage) } } val receiveCommand: Receive = { - case command@SendEmail(email) => + case command @ SendEmail(email) => log.info("received command {}", command.toString) persist(EmailEvent(email.asJson.noSpaces)) { event => sender() ! mailo.Queued send(email).onComplete { case Success(result) => result match { - case Right(_) => () + case Right(_) => + () eventStream.publish(event) case Left(error) => deadLettersHandler ! EmailApplicativeErrorEvent(event, error.getMessage) @@ -73,9 +78,9 @@ class EmailPersistanceActor( class LoggingActor() extends Actor with ActorLogging with CustomContentTypeCodecs { def receive = { - case EmailApplicativeErrorEvent(event, errorMessage) => + case EmailApplicativeErrorEvent(_, errorMessage) => log.error(s"%{event} failed with error: ${errorMessage}") case EmailCommunicationErrorEvent(sendEmail, errorMessage) => log.error(s"${sendEmail} failed with error ${errorMessage},") } -} \ No newline at end of file +} diff --git a/mailo/src/main/scala/mailo/persistence/SnapshotHelper.scala b/mailo/src/main/scala/mailo/persistence/SnapshotHelper.scala index 639a52d9..0bcf755d 100644 --- a/mailo/src/main/scala/mailo/persistence/SnapshotHelper.scala +++ b/mailo/src/main/scala/mailo/persistence/SnapshotHelper.scala @@ -8,7 +8,7 @@ object SnapshotHelper extends LazyLogging { val logSnapshotResult: PartialFunction[Any, Unit] = { case DeleteMessagesSuccess(toSequenceNr) => logger.debug(s"Successfully deleted messages up to $toSequenceNr") - case DeleteMessagesFailure(cause, toSequenceNr) => + case DeleteMessagesFailure(cause, _) => logger.error(s"Unable to delete message because ${cause.getMessage}") } } diff --git a/mailo/src/test/resources/reference.conf b/mailo/src/test/resources/reference.conf index 18893156..120e2726 100644 --- a/mailo/src/test/resources/reference.conf +++ b/mailo/src/test/resources/reference.conf @@ -1,7 +1,11 @@ akka { persistence { - journal.plugin = "inmemory-journal" - snapshot-store.plugin = "inmemory-snapshot-store" + journal.plugin = "akka.persistence.journal.inmem" + journal.inmem.test-serialization = on + } + + actor { + allow-java-serialization = on } # logs disabled during testing, remove this to re-enable diff --git a/mailo/src/test/scala/mailo/CacheSpec.scala b/mailo/src/test/scala/mailo/CacheSpec.scala index f9de7851..93df041b 100644 --- a/mailo/src/test/scala/mailo/CacheSpec.scala +++ b/mailo/src/test/scala/mailo/CacheSpec.scala @@ -1,55 +1,64 @@ package mailo -import org.scalatest.{ FlatSpec, Matchers } -import org.scalatest.concurrent.{ ScalaFutures, Eventually} - import mailo.data.S3MailData import scalacache._ import guava._ import scala.concurrent.duration._ -//It's a test, this is fine import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future //NOTE: run tests in this suite serially -class CacheSpec extends FlatSpec with Matchers with ScalaFutures with Eventually{ - - implicit val defaultPatience = - PatienceConfig(timeout = 20.seconds, interval = 5.seconds) +class CacheSpec extends munit.FunSuite { implicit val scalaCache = ScalaCache(GuavaCache()) val templateName1 = "mail.html" val templateName2 = "mail-image.html" - def action(templateName: String, ttl: Int) = cachingWithTTL(templateName)(ttl.seconds) { + def action(templateName: String, ttl: Duration) = cachingWithTTL(templateName)(ttl) { val s3 = new S3MailData() s3.get(templateName) } - "cache" should "initially be empty" in { - get[String, NoSerialization](templateName1).futureValue should be (None) - get[String, NoSerialization](templateName2).futureValue should be (None) + test("cache should initially be empty") { + for { + v1 <- get[String, NoSerialization](templateName1) + v2 <- get[String, NoSerialization](templateName2) + } yield { + assert(v1.isEmpty) + assert(v2.isEmpty) + } } - "cache" should "be populated correctly " in { - action(templateName1, 10).futureValue - action(templateName2, 30).futureValue - - get[String, NoSerialization](templateName2).futureValue should not be (None) - get[String, NoSerialization](templateName1).futureValue should not be (None) + test("cache should be populated correctly") { + for { + _ <- action(templateName1, ttl = 10.seconds) + _ <- action(templateName2, ttl = 30.seconds) + v2 <- get[String, NoSerialization](templateName2) + v1 <- get[String, NoSerialization](templateName1) + } yield { + assert(v1.isDefined) + assert(v2.isDefined) + } } - "eventually the cache of the first template" should "expire" in { - eventually (timeout(20.seconds)) { - get[String, NoSerialization](templateName1).futureValue should be (None) - get[String, NoSerialization](templateName2).futureValue should not be (None) + test("eventually the cache of the first template should expire") { + for { + _ <- Future(Thread.sleep(20.seconds.toMillis)) + v1 <- get[String, NoSerialization](templateName1) + v2 <- get[String, NoSerialization](templateName2) + } yield { + assert(v1.isEmpty) + assert(v2.isDefined) } } - "cache" should "work correctly after" in { - action(templateName1, 1).futureValue - action(templateName2, 1).futureValue + test("cache should work correctly after") { + for { + _ <- action(templateName1, 1.second) + _ <- action(templateName2, 1.second) + } yield () } } diff --git a/mailo/src/test/scala/mailo/FSMailDataSpec.scala b/mailo/src/test/scala/mailo/FSMailDataSpec.scala index 9990ea6b..1099ed64 100644 --- a/mailo/src/test/scala/mailo/FSMailDataSpec.scala +++ b/mailo/src/test/scala/mailo/FSMailDataSpec.scala @@ -1,66 +1,69 @@ package mailo -import org.scalatest._ -import java.io.{File, PrintWriter} - import mailo.data.{FSMailData} -import scala.concurrent.Await -import scala.concurrent.duration.Duration import java.nio.file.Files +import java.io.{File, PrintWriter} import mailo.data.FSMailDataError.{NotADirectory, TemplateNotFound} -class FSMailDataSpec extends FlatSpec with Matchers { +class FSMailDataSpec extends munit.FunSuite { + implicit val ec = munitExecutionContext val templateTest1 = "

Ciao Mr. {{name}}

" val partialName = "footer" val templateTest2 = s"

Ciao, ecco un parziale

[[$partialName]]" val partial1 = "

Hi, I am an english partial

" - "FSMailData" should "retrieve a simple template" in { + test("FSMailData should retrieve a simple template") { val templateName = "test-template-1" val dir = Files.createTempDirectory("mailo-test") - new PrintWriter(dir.toString+s"/${templateName}") { write(templateTest1); close } + new PrintWriter(dir.toString + s"/${templateName}") { write(templateTest1); close } val dirFile = new File(dir.toString) val mailData = new FSMailData(dirFile, false) - Await.result(mailData.get(templateName), Duration.Inf) should be (Right(MailRawContent(templateTest1, Map.empty[String, String]))) + mailData + .get(templateName) + .map { value => + assertEquals(value, Right(MailRawContent(templateTest1, Map.empty[String, String]))) + dirFile.listFiles().foreach(f => f.delete()) + dirFile.delete() + } - dirFile.listFiles().foreach(f => f.delete()) - dirFile.delete() } - it should "return the correct error if given directory was not found" in { + test("FSMailData should return the correct error if given directory was not found") { val mailData = new FSMailData(new File("asd"), false) - Await.result(mailData.get("asd"), Duration.Inf) match { - case Left(NotADirectory(_)) => succeed - case _ => fail("expected NotADirectory error") + mailData.get("asd").map { + case Left(NotADirectory(_)) => () + case _ => fail("expected NotADirectory error") } } - it should "return the correct error if template was not found" in { + test("FSMailData should return the correct error if template was not found") { val dir = Files.createTempDirectory("mailo-test") val dirFile = new File(dir.toString) val mailData = new FSMailData(dirFile, false) - Await.result(mailData.get("asd"), Duration.Inf) match { - case Left(TemplateNotFound(_)) => succeed - case _ => fail("expected TemplateNotFound error") + mailData.get("asd").map { + case Left(TemplateNotFound(_)) => () + case _ => fail("expected TemplateNotFound error") } } - it should "load and use partials" in { + test("FSMailData should load and use partials") { val templateName = "test-template-1" val dir = Files.createTempDirectory("mailo-test") - new PrintWriter(dir.toString+s"/$templateName") { write(templateTest2); close } - val partialDir = new File(dir+"/partials") + new PrintWriter(dir.toString + s"/$templateName") { write(templateTest2); close } + val partialDir = new File(dir + "/partials") partialDir.mkdir() - new PrintWriter(partialDir.getAbsolutePath+s"/$partialName") { write(partial1); close } + new PrintWriter(partialDir.getAbsolutePath + s"/$partialName") { write(partial1); close } val dirFile = new File(dir.toString) val mailData = new FSMailData(dirFile, true) - Await.result(mailData.get(templateName), Duration.Inf) should be (Right(MailRawContent(templateTest2, Map(partialName -> partial1)))) - partialDir.listFiles().foreach(f => f.delete()) - dirFile.listFiles().foreach(f => f.delete()) - dirFile.delete() + mailData.get(templateName).map { value => + assertEquals(value, Right(MailRawContent(templateTest2, Map(partialName -> partial1)))) + partialDir.listFiles().foreach(f => f.delete()) + dirFile.listFiles().foreach(f => f.delete()) + dirFile.delete() + } } } diff --git a/mailo/src/test/scala/mailo/MailoSMTPSpec.scala b/mailo/src/test/scala/mailo/MailoSMTPSpec.scala index ccbea36e..3f538d56 100644 --- a/mailo/src/test/scala/mailo/MailoSMTPSpec.scala +++ b/mailo/src/test/scala/mailo/MailoSMTPSpec.scala @@ -1,60 +1,58 @@ package mailo -import org.scalatest.{FlatSpec, Matchers} -import org.scalatest.concurrent.ScalaFutures -import cats.syntax.either._ -import mailo.data.S3MailData -import mailo.http.{MailgunClient, SMTPClient} +import mailo.http.SMTPClient -class MailoSMTPSpec extends FlatSpec with AppSpecSmtpClient with Matchers with ScalaFutures { - import org.scalatest.time.{Span, Seconds} +class MailoSMTPSpec extends munit.FunSuite { + implicit val ec = munitExecutionContext - implicit val defaultPatience = - PatienceConfig(timeout = Span(20, Seconds), interval = Span(5, Seconds)) + val mailer = Mailo(new MockedData, new SMTPClient, DeliveryGuarantee.AtMostOnce) - ignore should "be correctly sent" in { - val a = mailer.send(Mail( - to = "receiver@buildo.io", - from = "sender@buildo.io", - subject = "Test mail", - templateName = "mail.html", - params = Map.empty[String, String], - tags = List.empty[String] - )).futureValue.isRight should be (true) + test("should be correctly sent".ignore) { + mailer + .send( + Mail( + to = "receiver@buildo.io", + from = "sender@buildo.io", + subject = "Test mail", + templateName = "mail.html", + params = Map.empty[String, String], + tags = List.empty[String], + ), + ) + .map(value => assert(value.isRight)) } - ignore should "correctly fail" in { - mailer.send(Mail( - to = "postmaster@sandbox119020d8ef954c02bac2ee6db24d635b.mailgun.", - from = "Mailo mailo@buildo.io", - subject = "Test mail 1", - templateName = "mail.html", - params = Map.empty[String, String], - tags = List.empty[String] - )).futureValue.swap.getOrElse(fail) should be (http.MailClientError.BadRequest) + test("should correctly fail".ignore) { + mailer + .send( + Mail( + to = "postmaster@sandbox119020d8ef954c02bac2ee6db24d635b.mailgun.", + from = "Mailo mailo@buildo.io", + subject = "Test mail 1", + templateName = "mail.html", + params = Map.empty[String, String], + tags = List.empty[String], + ), + ) + .map { value => + assertEquals(value, Left(http.MailClientError.BadRequest)) + } } - ignore should "be returned" in { - mailer.send(Mail( - to = "mailo@buildo.io", - from = "Mailo mailo@buildo.io", - subject = "Test mail 2", - templateName = "mail.html", - params = Map("ciao" -> "CIAONI"), - tags = List("test") - )).futureValue.swap.getOrElse(fail) should be (parser.ParserError.TooManyParamsProvided(Set("ciao"))) + test("it should be returned".ignore) { + mailer + .send( + Mail( + to = "mailo@buildo.io", + from = "Mailo mailo@buildo.io", + subject = "Test mail 2", + templateName = "mail.html", + params = Map("ciao" -> "CIAONI"), + tags = List("test"), + ), + ) + .map { value => + assertEquals(value, Left(parser.ParserError.TooManyParamsProvided(Set("ciao")))) + } } } - -trait AppSpecSmtpClient { - import akka.stream.ActorMaterializer - import akka.actor.ActorSystem - - import scala.concurrent.ExecutionContext.Implicits.global - - private[this] implicit val system = ActorSystem() - private[this] implicit val materializer = ActorMaterializer() - - val mailer = Mailo(new MockedData, new SMTPClient, DeliveryGuarantee.AtMostOnce) -} - diff --git a/mailo/src/test/scala/mailo/MailoSpec.scala b/mailo/src/test/scala/mailo/MailoSpec.scala index e062be20..65f0f8bc 100644 --- a/mailo/src/test/scala/mailo/MailoSpec.scala +++ b/mailo/src/test/scala/mailo/MailoSpec.scala @@ -1,115 +1,151 @@ package mailo -import org.scalatest.{FlatSpec, Matchers} -import org.scalatest.concurrent.ScalaFutures -import cats.syntax.either._ import mailo.data.S3MailData import mailo.http.MailgunClient +import akka.actor.ActorSystem +import akka.testkit.TestKit -class MailoSpec extends FlatSpec with AppSpec with Matchers with ScalaFutures { - import org.scalatest.time.{Span, Seconds} +class MailoSpec extends munit.FunSuite { + implicit val ec = munitExecutionContext + implicit val system = ActorSystem() + val mailer = Mailo(new S3MailData, new MailgunClient, DeliveryGuarantee.AtMostOnce) - implicit val defaultPatience = - PatienceConfig(timeout = Span(20, Seconds), interval = Span(5, Seconds)) + override def afterAll: Unit = { + TestKit.shutdownActorSystem(system) + } - "email" should "be correctly sent" in { - mailer.send(Mail( - to = "mailo@buildo.io", - from = "Mailo mailo@buildo.io", - subject = "Test mail", - templateName = "mail.html", - params = Map("ciao" -> "CIAOOOONE"), - tags = List("test") - )).futureValue.isRight should be (true) + test("email should be correctly sent") { + mailer + .send( + Mail( + to = "mailo@buildo.io", + from = "Mailo mailo@buildo.io", + subject = "Test mail", + templateName = "mail.html", + params = Map("ciao" -> "CIAOOOONE"), + tags = List("test"), + ), + ) + .map(value => assert(value.isRight)) } - "email with wrong recipient" should "correctly fail" in { - mailer.send(Mail( - to = "postmaster@sandbox119020d8ef954c02bac2ee6db24d635b.mailgun.", - from = "Mailo mailo@buildo.io", - subject = "Test mail 1", - templateName = "mail.html", - params = Map("ciao" -> "CIAOOOONE"), - tags = List("test") - )).futureValue.swap.getOrElse(fail) should be (http.MailClientError.BadRequest) + test("email with wrong recipient should correctly fail") { + mailer + .send( + Mail( + to = "postmaster@sandbox119020d8ef954c02bac2ee6db24d635b.mailgun.", + from = "Mailo mailo@buildo.io", + subject = "Test mail 1", + templateName = "mail.html", + params = Map("ciao" -> "CIAOOOONE"), + tags = List("test"), + ), + ) + .map { value => + assertEquals(value, Left(http.MailClientError.BadRequest)) + } + } - "too few parameter error" should "be returned" in { - mailer.send(Mail( - to = "mailo@buildo.io", - from = "Mailo mailo@buildo.io", - subject = "Test mail 2", - templateName = "mail.html", - params = Map(), - tags = List("test") - )).futureValue.swap.getOrElse(fail) should be (parser.ParserError.TooFewParamsProvided(Set("ciao"))) + test("too few parameter error should be returned") { + mailer + .send( + Mail( + to = "mailo@buildo.io", + from = "Mailo mailo@buildo.io", + subject = "Test mail 2", + templateName = "mail.html", + params = Map(), + tags = List("test"), + ), + ) + .map { value => + assertEquals(value, Left(parser.ParserError.TooFewParamsProvided(Set("ciao")))) + } } - "too many parameter error" should "be returned" in { - mailer.send(Mail( - to = "mailo@buildo.io", - from = "Mailo mailo@buildo.io", - subject = "Test mail 3", - templateName = "mail.html", - params = Map("ciao" -> "CIAONE", "ciaooo" -> "CIAONE"), - tags = List("test") - )).futureValue.swap.getOrElse(fail) should be (parser.ParserError.TooManyParamsProvided(Set("ciaooo"))) + test("too many parameter error should be returned") { + mailer + .send( + Mail( + to = "mailo@buildo.io", + from = "Mailo mailo@buildo.io", + subject = "Test mail 3", + templateName = "mail.html", + params = Map("ciao" -> "CIAONE", "ciaooo" -> "CIAONE"), + tags = List("test"), + ), + ) + .map { value => + assertEquals(value, Left(parser.ParserError.TooManyParamsProvided(Set("ciaooo")))) + } } - "data error" should "be returned" in { - mailer.send(Mail( - to = "mailo@buildo.io", - from = "Mailo mailo@buildo.io", - subject = "Test mail 4", - templateName = "mail.hl", - params = Map(), - tags = List("test") - )).futureValue.swap.getOrElse(fail) should be (data.S3MailDataError.ObjectNotFound) + test("data error should be returned") { + mailer + .send( + Mail( + to = "mailo@buildo.io", + from = "Mailo mailo@buildo.io", + subject = "Test mail 4", + templateName = "mail.hl", + params = Map(), + tags = List("test"), + ), + ) + .map { value => + assertEquals(value, Left(data.S3MailDataError.ObjectNotFound)) + } } - "email" should "not explode sending attachments" in { + test("email should not explode sending attachments") { import akka.http.scaladsl.model.MediaTypes._ import akka.http.scaladsl.model.HttpCharsets._ - val attachment = Attachment(name = "test.txt", content="test", `type`=`text/plain` withCharset `UTF-8`) - - mailer.send(Mail( - to = "mailo@buildo.io", - from = "Mailo mailo@buildo.io", - subject = "Test mail", - templateName = "mail.html", - params = Map("ciao" -> "CIAOOOONE"), - attachments = List(attachment), - tags = List("test") - )).futureValue.isRight should be (true) + val attachment = + Attachment(name = "test.txt", content = "test", `type` = `text/plain`.withCharset(`UTF-8`)) + + mailer + .send( + Mail( + to = "mailo@buildo.io", + from = "Mailo mailo@buildo.io", + subject = "Test mail", + templateName = "mail.html", + params = Map("ciao" -> "CIAOOOONE"), + attachments = List(attachment), + tags = List("test"), + ), + ) + .map { value => + assert(value.isRight) + } } - "email" should "not explode sending pdf attachments" in { + test("email should not explode sending pdf attachments") { import akka.http.scaladsl.model.MediaTypes._ - val attachment = Attachment(name = "helloworld.pdf", content = pdf.get, `type` = `application/pdf`, transferEncoding = Some("base64")) - - mailer.send(Mail( - to = "mailo@buildo.io", - from = "Mailo mailo@buildo.io", - subject = "Test mail", - templateName = "mail.html", - params = Map("ciao" -> "CIAOOOONE"), - attachments = List(attachment), - tags = List("test") - )).futureValue.isRight should be (true) + val attachment = Attachment( + name = "helloworld.pdf", + content = pdf.get, + `type` = `application/pdf`, + transferEncoding = Some("base64"), + ) + + mailer + .send( + Mail( + to = "mailo@buildo.io", + from = "Mailo mailo@buildo.io", + subject = "Test mail", + templateName = "mail.html", + params = Map("ciao" -> "CIAOOOONE"), + attachments = List(attachment), + tags = List("test"), + ), + ) + .map { value => + assert(value.isRight) + } } } - -trait AppSpec { - import akka.stream.ActorMaterializer - import akka.actor.ActorSystem - - import scala.concurrent.ExecutionContext.Implicits.global - - private[this] implicit val system = ActorSystem() - private[this] implicit val materializer = ActorMaterializer() - - val mailer = Mailo(new S3MailData, new MailgunClient, DeliveryGuarantee.AtMostOnce) -} - diff --git a/mailo/src/test/scala/mailo/ParserSpec.scala b/mailo/src/test/scala/mailo/ParserSpec.scala index b45ff322..b8beaa2e 100644 --- a/mailo/src/test/scala/mailo/ParserSpec.scala +++ b/mailo/src/test/scala/mailo/ParserSpec.scala @@ -1,25 +1,23 @@ package mailo -import org.scalatest.{ FlatSpec, Matchers } -import org.scalatest.concurrent.ScalaFutures - -import cats.syntax.either._ - import parser._ -class ParserSpec extends FlatSpec with AppSpec with Matchers { +class ParserSpec extends munit.FunSuite { val template = "{{replaceMe}}{{replaceMeAgain}} [[p1.html]]" val partials = Map( - "p1.html" -> "{{replaceMe}}" + "p1.html" -> "{{replaceMe}}", ) val params = Map( "replaceMe" -> "replacedYou", - "replaceMeAgain" -> "replacedYouAgain" + "replaceMeAgain" -> "replacedYouAgain", ) val rawContent = MailRawContent(template, partials) - "html parser" should "be correctly parsed" in { - HTMLParser.parse(rawContent, params) should be (Right("replacedYoureplacedYouAgain replacedYou")) + test("html parser should be correctly parsed") { + assertEquals( + HTMLParser.parse(rawContent, params), + Right("replacedYoureplacedYouAgain replacedYou"), + ) } } diff --git a/mailo/src/test/scala/mailo/PersistenceSpec.scala b/mailo/src/test/scala/mailo/PersistenceSpec.scala index 3010f0dc..0e5bb7e6 100644 --- a/mailo/src/test/scala/mailo/PersistenceSpec.scala +++ b/mailo/src/test/scala/mailo/PersistenceSpec.scala @@ -2,29 +2,19 @@ package mailo import java.util.concurrent.ConcurrentLinkedQueue -import akka.actor.{ActorSystem, PoisonPill, Kill} -import akka.stream.{ActorMaterializer, ActorMaterializerSettings} -import akka.testkit.{TestKit, TestProbe, ImplicitSender} - -import akka.persistence.inmemory.extension.{ InMemoryJournalStorage, InMemorySnapshotStorage, StorageExtension } - -import cats.syntax.either._ - import mailo.http.MailClient import mailo.data.MailData -import mailo.persistence.{EmailPersistanceActor, SendEmail, LoggingActor} +import mailo.persistence.{EmailPersistanceActor, LoggingActor, SendEmail} -import org.scalatest.{Matchers, BeforeAndAfterEach, BeforeAndAfterAll, Suite, WordSpecLike, FeatureSpec} -import org.scalatest.concurrent.{ScalaFutures, Eventually} -import org.scalatest.time.{Span, Seconds} - -import scala.collection.mutable.Queue import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ +import akka.actor.{ActorSystem, Kill} import akka.pattern.ask - import akka.util.Timeout +import akka.testkit.TestKitBase +import akka.testkit.ImplicitSender +import akka.testkit.TestKit case class SimpleMail(subject: String) @@ -38,12 +28,13 @@ class MockedClient(val state: ConcurrentLinkedQueue[SimpleMail]) extends MailCli content: MailRefinedContent.MailRefinedContent, attachments: List[Attachment], tags: List[String], - headers: Map[String, String] - )(implicit executionContext: ExecutionContext): Future[Either[MailError, MailResponse]] = Future.successful { - //State needs to be updated by one at the time - state.add(SimpleMail(subject)) - Right(MailResponse(subject, "ok")) - } + headers: Map[String, String], + )(implicit executionContext: ExecutionContext): Future[Either[MailError, MailResponse]] = + Future.successful { + //State needs to be updated by one at the time + state.add(SimpleMail(subject)) + Right(MailResponse(subject, "ok")) + } } class MockedClientWithDelay(val state: ConcurrentLinkedQueue[SimpleMail]) extends MailClient { @@ -56,13 +47,14 @@ class MockedClientWithDelay(val state: ConcurrentLinkedQueue[SimpleMail]) extend content: MailRefinedContent.MailRefinedContent, attachments: List[Attachment], tags: List[String], - headers: Map[String, String] - )(implicit executionContext: ExecutionContext): Future[Either[MailError, MailResponse]] = Future.successful { - //State needs to be updated by one at the time - Thread sleep 200 - state.add(SimpleMail(subject)) - Right(MailResponse(subject, "ok")) - } + headers: Map[String, String], + )(implicit executionContext: ExecutionContext): Future[Either[MailError, MailResponse]] = + Future.successful { + //State needs to be updated by one at the time + Thread.sleep(200) + state.add(SimpleMail(subject)) + Right(MailResponse(subject, "ok")) + } } class MockedData extends MailData { @@ -72,105 +64,100 @@ class MockedData extends MailData { Future(Right(MailRawContent("ciao", partials = Map("name" -> "claudio")))) } -class PersistenceSpec - extends TestKit(ActorSystem("testSystem")) - with ImplicitSender - with WordSpecLike - with Matchers - with BeforeAndAfterAll - with BeforeAndAfterEach - with ScalaFutures { - import Eventually._ +class PersistenceSpec extends { + val system: ActorSystem = ActorSystem("testSystem") +} with munit.FunSuite with TestKitBase with ImplicitSender { implicit def executionContext: ExecutionContext = system.dispatcher - + override def afterAll: Unit = { TestKit.shutdownActorSystem(system) } - override def beforeEach(): Unit = { - val tp = TestProbe() - tp.send(StorageExtension(system).journalStorage, InMemoryJournalStorage.ClearJournal) - tp.send(StorageExtension(system).snapshotStorage, InMemorySnapshotStorage.ClearSnapshots) - } - - implicit val defaultPatience = - PatienceConfig(timeout = Span(20, Seconds), interval = Span(5, Seconds)) - - "persistence actor" should { - "properly deliver email messages" in { - val state = new ConcurrentLinkedQueue[SimpleMail]() - val emailSender = new EmailSender(new MockedData, new MockedClient(state)) - val loggingActor = system.actorOf(LoggingActor.props()) - val emailPersistanceActor = system.actorOf(EmailPersistanceActor.props(emailSender, loggingActor)) - - val mail1 = Mail( - to = "mailo@buildo.io", - from = "Mailo mailo@buildo.io", - subject = "1", - templateName = "mail.html", - params = Map.empty, - tags = List("test") - ) - - val mail2 = mail1.copy(subject = "2") - - emailPersistanceActor ! SendEmail(mail1) - expectMsg(Queued) - - emailPersistanceActor ! SendEmail(mail2) - expectMsg(Queued) - - eventually(timeout(Span(5, Seconds))) { - state.size should be(2) - info(s"${state.size} messages sent") + test("persistence actor should properly deliver email messages") { + val state = new ConcurrentLinkedQueue[SimpleMail]() + val emailSender = new EmailSender(new MockedData, new MockedClient(state)) + val loggingActor = system.actorOf(LoggingActor.props()) + val emailPersistanceActor = + system.actorOf(EmailPersistanceActor.props(emailSender, loggingActor)) + + val mail1 = Mail( + to = "mailo@buildo.io", + from = "Mailo mailo@buildo.io", + subject = "1", + templateName = "mail.html", + params = Map.empty, + tags = List("test"), + ) + + val mail2 = mail1.copy(subject = "2") + + emailPersistanceActor ! SendEmail(mail1) + expectMsg(Queued) + + emailPersistanceActor ! SendEmail(mail2) + expectMsg(Queued) + + def retry[A](times: Int, interval: Duration)(f: => A)(implicit loc: munit.Location): A = { + if (times <= 0) fail("Failed after retrying") + try { + f + } catch { + case _: munit.FailException => + Thread.sleep(interval.toMillis) + retry(times - 1, interval)(f) } } - "should deliver all the queued messages" in { - val state = new ConcurrentLinkedQueue[SimpleMail]() - val emailSender = new EmailSender(new MockedData, new MockedClientWithDelay(state)) - val loggingActor = system.actorOf(LoggingActor.props()) - val emailPersistanceActor = system.actorOf(EmailPersistanceActor.props(emailSender, loggingActor)) - - implicit val enqueueTimeout: Timeout = Timeout(100.milliseconds) - - val mail = Mail( - to = "mailo@buildo.io", - from = "Mailo mailo@buildo.io", - subject = "1", - templateName = "mail.html", - params = Map.empty, - tags = List("test") - ) - - var queuedEmails = 0 - var failedEmails = 0 - info("start") - - val task = system.scheduler.schedule(0.seconds, 1.milliseconds, - new Runnable { - def run(): Unit = { - ask(emailPersistanceActor, SendEmail(mail)) - .onComplete { - case scala.util.Success(result) => queuedEmails += 1 - case scala.util.Failure(error) => failedEmails += 1 - } - } - }) - - Thread sleep 5000 - - emailPersistanceActor ! Kill - - Thread sleep 200 - task.cancel() - - Thread sleep 100 - val receivedEmails = state.size - - queuedEmails should be <= (receivedEmails) - info(s"queued $queuedEmails emails, received ${receivedEmails}, failed (not queued) $failedEmails") + retry(times = 5, interval = 1.second) { + assertEquals(state.size, 2) + println(s"${state.size} messages sent") } } -} \ No newline at end of file + + test("persistence actor should deliver all the queued messages") { + val state = new ConcurrentLinkedQueue[SimpleMail]() + val emailSender = new EmailSender(new MockedData, new MockedClientWithDelay(state)) + val loggingActor = system.actorOf(LoggingActor.props()) + val emailPersistanceActor = + system.actorOf(EmailPersistanceActor.props(emailSender, loggingActor)) + + implicit val enqueueTimeout: Timeout = Timeout(100.milliseconds) + + val mail = Mail( + to = "mailo@buildo.io", + from = "Mailo mailo@buildo.io", + subject = "1", + templateName = "mail.html", + params = Map.empty, + tags = List("test"), + ) + + var queuedEmails = 0 + var failedEmails = 0 + println("start") + + val task = system.scheduler.scheduleAtFixedRate(0.seconds, 1.milliseconds)( + () => + ask(emailPersistanceActor, SendEmail(mail)).onComplete { + case scala.util.Success(_) => queuedEmails += 1 + case scala.util.Failure(_) => failedEmails += 1 + }, + ) + + Thread.sleep(5000) + + emailPersistanceActor ! Kill + + Thread.sleep(200) + task.cancel() + + Thread.sleep(100) + val receivedEmails = state.size + + assert(queuedEmails <= receivedEmails) + println( + s"queued $queuedEmails emails, received ${receivedEmails}, failed (not queued) $failedEmails", + ) + } +} diff --git a/mailo/src/test/scala/mailo/SendinblueSpec.scala b/mailo/src/test/scala/mailo/SendinblueSpec.scala index cdf424da..68715613 100644 --- a/mailo/src/test/scala/mailo/SendinblueSpec.scala +++ b/mailo/src/test/scala/mailo/SendinblueSpec.scala @@ -1,64 +1,55 @@ package mailo -import akka.stream.ActorMaterializer -import akka.actor.ActorSystem - import akka.http.scaladsl.model.MediaTypes._ import akka.http.scaladsl.model.HttpCharsets._ -import cats.syntax.either._ - -import org.scalatest.{FlatSpec, Matchers} -import org.scalatest.concurrent.ScalaFutures - -import org.scalatest.time.{Seconds, Span} - import scala.concurrent.ExecutionContext.Implicits.global -class SendinblueSpec extends FlatSpec with Matchers with ScalaFutures { - private[this] implicit val system = ActorSystem() - private[this] implicit val materializer = ActorMaterializer() - +class SendinblueSpec extends munit.FunSuite { val mailer = new S3SendinblueMailo() - implicit val defaultPatience = - PatienceConfig(timeout = Span(20, Seconds), interval = Span(5, Seconds)) - - "email" should "be correctly sent" in { - (mailer + test("email should be correctly sent") { + mailer .send( to = "mailo@buildo.io", from = "Mailo test mailo@buildo.io", subject = "Test mail", templateName = "mail.html", params = Map("ciao" -> "CIAOOOONE"), - tags = List("test") + tags = List("test"), ) - .futureValue - .getOrElse(fail)) - .message should be("Email sent successfully.") + .map { value => + assertEquals(value.map(_.message), Right("Email sent successfully.")) + } } - "email" should "not be sent if FROM is malformed" in { - (mailer + test("email should not be sent if FROM is malformed") { + mailer .send( to = "mailo@buildo.io", from = "MALFORMED", subject = "Test mail", templateName = "mail.html", params = Map("ciao" -> "CIAOOOONE"), - tags = List("test") + tags = List("test"), ) - .futureValue - .swap - .getOrElse(fail)) should be (http.MailClientError.UnknownError("{\"code\":\"missing_parameter\",\"message\":\"sender name is missing\"}")) + .map { value => + assertEquals( + value, + Left( + http.MailClientError.UnknownError( + "{\"code\":\"missing_parameter\",\"message\":\"sender name is missing\"}", + ), + ), + ) + } } - "email" should "not explode sending attachments" in { + test("email should not explode sending attachments") { val attachment = Attachment(name = "test.txt", content = "test", `type` = `text/plain`.withCharset(`UTF-8`)) - (mailer + mailer .send( to = "mailo@buildo.io", from = "Mailo mailo@buildo.io", @@ -66,22 +57,22 @@ class SendinblueSpec extends FlatSpec with Matchers with ScalaFutures { templateName = "mail.html", params = Map("ciao" -> "CIAOOOONE"), attachments = List(attachment), - tags = List("test") + tags = List("test"), ) - .futureValue - .getOrElse(fail)) - .message should be("Email sent successfully.") + .map { value => + assertEquals(value.map(_.message), Right("Email sent successfully.")) + } } - "email" should "not explode sending pdf attachments" in { + test("email should not explode sending pdf attachments") { val attachment = Attachment( name = "helloworld.pdf", content = pdf.get, `type` = `application/pdf`, - transferEncoding = Some("base64") + transferEncoding = Some("base64"), ) - (mailer + mailer .send( to = "mailo@buildo.io", from = "Mailo mailo@buildo.io", @@ -89,10 +80,10 @@ class SendinblueSpec extends FlatSpec with Matchers with ScalaFutures { templateName = "mail.html", params = Map("ciao" -> "CIAOOOONE"), attachments = List(attachment), - tags = List("test") + tags = List("test"), ) - .futureValue - .getOrElse(fail)) - .message should be("Email sent successfully.") + .map { value => + assertEquals(value.map(_.message), Right("Email sent successfully.")) + } } } diff --git a/metarpheus/.drone.yml b/metarpheus/.drone.yml deleted file mode 100644 index a123961b..00000000 --- a/metarpheus/.drone.yml +++ /dev/null @@ -1,24 +0,0 @@ -build: - test: - image: hseeberger/scala-sbt - commands: - - ./scalafmt --test - - sbt coreJVM/test -Dsbt.ivy.home=/drone/.ivy - - sbt cli/test -Dsbt.ivy.home=/drone/.ivy - - build: - image: hseeberger/scala-sbt - commands: - - sbt cli/assembly -Dsbt.ivy.home=/drone/.ivy - - mv cli/target/scala-2.12/metarpheus-cli-assembly-$$TAG.jar metarpheus.jar - when: - event: tag - -publish: - github_release: - api_key: $$GITHUB_TOKEN - files: metarpheus.jar - -cache: - mount: - - /drone/.ivy diff --git a/metarpheus/ci/test.yml b/metarpheus/ci/test.yml index ef07429e..d6219705 100644 --- a/metarpheus/ci/test.yml +++ b/metarpheus/ci/test.yml @@ -4,7 +4,7 @@ image_resource: type: docker-image source: repository: buildo/scala-sbt-alpine - tag: 8u201_2.12.8_1.2.8 + tag: 8u201_2.12.11_1.3.8 inputs: - name: retro diff --git a/metarpheus/core/src/main/scala/io.buildo.metarpheus/core/intermediate/intermediate.scala b/metarpheus/core/src/main/scala/io.buildo.metarpheus/core/intermediate/intermediate.scala index e4b131c3..87bc17ae 100644 --- a/metarpheus/core/src/main/scala/io.buildo.metarpheus/core/intermediate/intermediate.scala +++ b/metarpheus/core/src/main/scala/io.buildo.metarpheus/core/intermediate/intermediate.scala @@ -9,7 +9,7 @@ object Type { case class Apply(name: String, args: Seq[Type]) extends Type } -sealed trait Model { +sealed trait Model extends Product with Serializable { val name: String } case class CaseClass( diff --git a/metarpheus/core/src/test/scala/io.buildo.metarpheus/core/extractors/ApiSuite.scala b/metarpheus/core/src/test/scala/io.buildo.metarpheus/core/extractors/ApiSuite.scala index a837f7f2..ac2b0ca4 100644 --- a/metarpheus/core/src/test/scala/io.buildo.metarpheus/core/extractors/ApiSuite.scala +++ b/metarpheus/core/src/test/scala/io.buildo.metarpheus/core/extractors/ApiSuite.scala @@ -2,11 +2,9 @@ package io.buildo.metarpheus package core package test -import org.scalatest._ - import extractors._ -class ApiSuite extends FunSuite { +class ApiSuite extends munit.FunSuite { lazy val parsed = { import scala.meta._ List( diff --git a/metarpheus/core/src/test/scala/io.buildo.metarpheus/core/extractors/ControllerSuite.scala b/metarpheus/core/src/test/scala/io.buildo.metarpheus/core/extractors/ControllerSuite.scala index 403687db..18b650f0 100644 --- a/metarpheus/core/src/test/scala/io.buildo.metarpheus/core/extractors/ControllerSuite.scala +++ b/metarpheus/core/src/test/scala/io.buildo.metarpheus/core/extractors/ControllerSuite.scala @@ -2,11 +2,9 @@ package io.buildo.metarpheus package core package test -import org.scalatest._ - import extractors._ -class ControllerSuite extends FunSuite { +class ControllerSuite extends munit.FunSuite { lazy val parsed = { import scala.meta._ Fixtures.controllers.parse[Source].get @@ -21,215 +19,215 @@ class ControllerSuite extends FunSuite { val result = controller.extractAllRoutes(parsed) - assert( - result.toString === - List( - Route( - method = "get", - route = List( - RouteSegment.String("campings"), - RouteSegment.String("getByCoolnessAndSize"), - ), - params = List( - RouteParam( - Some("coolness"), - Type.Name("String"), - true, - Some("how cool it is"), - ), - RouteParam( - Some("size"), - Type.Name("Int"), - false, - Some("the number of tents"), - ), - RouteParam( - Some("nickname"), - Type.Name("String"), - true, - Some("a friendly name for the camping"), - ), - ), - pathName = Some("campings"), - controllerType = Type.Apply("CampingController", List()), - authenticated = false, - returns = Type.Apply("List", List(Type.Name("Camping"))), - error = Some(Type.Name("String")), - desc = Some("get campings matching the requested coolness and size"), - name = List("campingController", "getByCoolnessAndSize"), - controllerPackage = List("io", "buildo", "baseexample", "controllers"), - ), - Route( - method = "get", - route = List( - RouteSegment.String("campings"), - RouteSegment.String("getBySizeAndDistance"), - ), - params = List( - RouteParam( - Some("size"), - Type.Name("Int"), - true, - Some("the number of tents"), - ), - RouteParam( - Some("distance"), - Type.Name("Int"), - true, - Some("how distant it is"), - ), - ), - pathName = Some("campings"), - controllerType = Type.Apply("CampingController", List()), - authenticated = false, - returns = Type.Apply("List", List(Type.Name("Camping"))), - error = Some(Type.Name("String")), - desc = Some("get campings matching the requested size and distance"), - name = List("campingController", "getBySizeAndDistance"), - controllerPackage = List("io", "buildo", "baseexample", "controllers"), - ), - Route( - method = "get", - route = List( - RouteSegment.String("campings"), - RouteSegment.String("getById"), - ), - params = List( - RouteParam( - Some("id"), - Type.Name("Int"), - true, - Some("camping id"), - ), - ), - pathName = Some("campings"), - controllerType = Type.Apply("CampingController", List()), - authenticated = true, - returns = Type.Name("Camping"), - error = Some(Type.Name("String")), - desc = Some("get a camping by id"), - name = List("campingController", "getById"), - controllerPackage = List("io", "buildo", "baseexample", "controllers"), - ), - Route( - method = "get", - route = List( - RouteSegment.String("campings"), - RouteSegment.String("getByTypedId"), - ), - params = List( - RouteParam( - Some("id"), - Type.Apply("Id", Seq(Type.Name("Camping"))), - true, - None, - ), - ), - pathName = Some("campings"), - controllerType = Type.Apply("CampingController", List()), - authenticated = true, - returns = Type.Name("Camping"), - error = Some(Type.Name("String")), - desc = Some("get a camping by typed id"), - name = List("campingController", "getByTypedId"), - controllerPackage = List("io", "buildo", "baseexample", "controllers"), - ), - Route( - method = "get", - route = List( - RouteSegment.String("campings"), - RouteSegment.String("getByHasBeach"), + assertEquals( + result, + List( + Route( + method = "get", + route = List( + RouteSegment.String("campings"), + RouteSegment.String("getByCoolnessAndSize"), + ), + params = List( + RouteParam( + Some("coolness"), + Type.Name("String"), + true, + Some("how cool it is"), + ), + RouteParam( + Some("size"), + Type.Name("Int"), + false, + Some("the number of tents"), + ), + RouteParam( + Some("nickname"), + Type.Name("String"), + true, + Some("a friendly name for the camping"), ), - params = List( - RouteParam( - Some("hasBeach"), - Type.Name("Boolean"), - true, - Some("whether there's a beach"), - ), + ), + authenticated = false, + returns = Type.Apply("List", List(Type.Name("Camping"))), + error = Some(Type.Name("String")), + pathName = Some("campings"), + controllerType = Type.Apply("CampingController", Nil), + desc = Some("get campings matching the requested coolness and size"), + name = List("campingController", "getByCoolnessAndSize"), + controllerPackage = List("io", "buildo", "baseexample", "controllers"), + ), + Route( + method = "get", + route = List( + RouteSegment.String("campings"), + RouteSegment.String("getBySizeAndDistance"), + ), + params = List( + RouteParam( + Some("size"), + Type.Name("Int"), + true, + Some("the number of tents"), + ), + RouteParam( + Some("distance"), + Type.Name("Int"), + true, + Some("how distant it is"), ), - pathName = Some("campings"), - controllerType = Type.Apply("CampingController", List()), - authenticated = false, - returns = Type.Apply("List", List(Type.Name("Camping"))), - error = Some(Type.Name("String")), - desc = Some("get campings based on whether they're close to a beach"), - name = List("campingController", "getByHasBeach"), - controllerPackage = List("io", "buildo", "baseexample", "controllers"), - ), - Route( - method = "post", - route = List( - RouteSegment.String("campings"), - RouteSegment.String("create"), + ), + authenticated = false, + returns = Type.Apply("List", List(Type.Name("Camping"))), + error = Some(Type.Name("String")), + pathName = Some("campings"), + controllerType = Type.Apply("CampingController", Nil), + desc = Some("get campings matching the requested size and distance"), + name = List("campingController", "getBySizeAndDistance"), + controllerPackage = List("io", "buildo", "baseexample", "controllers"), + ), + Route( + method = "get", + route = List( + RouteSegment.String("campings"), + RouteSegment.String("getById"), + ), + params = List( + RouteParam( + Some("id"), + Type.Name("Int"), + true, + Some("camping id"), ), - params = List( - RouteParam( - Some("camping"), - Type.Name("Camping"), - true, - None, - inBody = true, - ), + ), + authenticated = true, + returns = Type.Name("Camping"), + error = Some(Type.Name("String")), + pathName = Some(""), + controllerType = Type.Name("CampingController"), + desc = Some("get a camping by id"), + name = List("campingController", "getById"), + controllerPackage = List("io", "buildo", "baseexample", "controllers"), + ), + Route( + method = "get", + route = List( + RouteSegment.String("campings"), + RouteSegment.String("getByTypedId"), + ), + params = List( + RouteParam( + Some("id"), + Type.Apply("Id", Seq(Type.Name("Camping"))), + true, + None, ), - pathName = Some("campings"), - controllerType = Type.Apply("CampingController", List()), - authenticated = false, - returns = Type.Name("Camping"), - error = Some(Type.Name("CreateCampingError")), - desc = Some("create a camping"), - name = List("campingController", "create"), - controllerPackage = List("io", "buildo", "baseexample", "controllers"), - ), - Route( - method = "get", - route = List( - RouteSegment.String("campings"), - RouteSegment.String("taglessFinalRouteV1"), + ), + authenticated = true, + returns = Type.Name("Camping"), + error = Some(Type.Name("String")), + pathName = Some(""), + controllerType = Type.Name("CampingController"), + desc = Some("get a camping by typed id"), + name = List("campingController", "getByTypedId"), + controllerPackage = List("io", "buildo", "baseexample", "controllers"), + ), + Route( + method = "get", + route = List( + RouteSegment.String("campings"), + RouteSegment.String("getByHasBeach"), + ), + params = List( + RouteParam( + Some("hasBeach"), + Type.Name("Boolean"), + true, + Some("whether there's a beach"), ), - params = List( - RouteParam( - Some("input"), - Type.Name("String"), - true, - None, - inBody = false, - ), + ), + authenticated = false, + returns = Type.Apply("List", List(Type.Name("Camping"))), + error = Some(Type.Name("String")), + pathName = Some(""), + controllerType = Type.Name("CampingController"), + desc = Some("get campings based on whether they're close to a beach"), + name = List("campingController", "getByHasBeach"), + controllerPackage = List("io", "buildo", "baseexample", "controllers"), + ), + Route( + method = "post", + route = List( + RouteSegment.String("campings"), + RouteSegment.String("create"), + ), + params = List( + RouteParam( + Some("camping"), + Type.Name("Camping"), + true, + None, + inBody = true, ), - pathName = Some("campings"), - controllerType = Type.Apply("CampingController", List()), - authenticated = false, - returns = Type.Name("String"), - error = None, - desc = None, - name = List("campingController", "taglessFinalRouteV1"), - controllerPackage = List("io", "buildo", "baseexample", "controllers"), - ), - Route( - method = "get", - route = List( - RouteSegment.String("campings"), - RouteSegment.String("taglessFinalRouteV2"), + ), + authenticated = false, + returns = Type.Name("Camping"), + error = Some(Type.Name("CreateCampingError")), + pathName = Some(""), + controllerType = Type.Name("CampingController"), + desc = Some("create a camping"), + name = List("campingController", "create"), + controllerPackage = List("io", "buildo", "baseexample", "controllers"), + ), + Route( + method = "get", + route = List( + RouteSegment.String("campings"), + RouteSegment.String("taglessFinalRouteV1"), + ), + params = List( + RouteParam( + Some("input"), + Type.Name("String"), + true, + None, + inBody = false, ), - params = List( - RouteParam( - Some("input"), - Type.Name("String"), - true, - None, - inBody = false, - ), + ), + authenticated = false, + returns = Type.Name("String"), + error = None, + pathName = Some(""), + controllerType = Type.Name("CampingController"), + desc = None, + name = List("campingController", "taglessFinalRouteV1"), + controllerPackage = List("io", "buildo", "baseexample", "controllers"), + ), + Route( + method = "get", + route = List( + RouteSegment.String("campings"), + RouteSegment.String("taglessFinalRouteV2"), + ), + params = List( + RouteParam( + Some("input"), + Type.Name("String"), + true, + None, + inBody = false, ), - pathName = Some("campings"), - controllerType = Type.Apply("CampingController", List()), - authenticated = false, - returns = Type.Name("String"), - error = Some(Type.Name("Exception")), - desc = None, - name = List("campingController", "taglessFinalRouteV2"), - controllerPackage = List("io", "buildo", "baseexample", "controllers"), - ), - ).toString, + ), + authenticated = false, + returns = Type.Name("String"), + error = Some(Type.Name("Exception")), + pathName = Some(""), + controllerType = Type.Name("CampingController"), + desc = None, + name = List("campingController", "taglessFinalRouteV2"), + controllerPackage = List("io", "buildo", "baseexample", "controllers"), + ), + ), ) } diff --git a/metarpheus/core/src/test/scala/io.buildo.metarpheus/core/extractors/ModelSuite.scala b/metarpheus/core/src/test/scala/io.buildo.metarpheus/core/extractors/ModelSuite.scala index f00198c5..cf37ff48 100644 --- a/metarpheus/core/src/test/scala/io.buildo.metarpheus/core/extractors/ModelSuite.scala +++ b/metarpheus/core/src/test/scala/io.buildo.metarpheus/core/extractors/ModelSuite.scala @@ -2,13 +2,9 @@ package io.buildo.metarpheus package core package test -import org.scalatest._ -import ai.x.diff.DiffShow -import ai.x.diff.conversions._ - import extractors._ -class ModelSuite extends FunSuite { +class ModelSuite extends munit.FunSuite { lazy val parsed = { import scala.meta._ Fixtures.models.parse[Source].get @@ -172,15 +168,6 @@ class ModelSuite extends FunSuite { desc = Some("Errors that can happen when creating a camping"), `package` = List("io", "buildo", "baseexample", "models"), ), - CaseClass( - name = "IgnoreMe", - members = List( - CaseClass.Member(name = "ignore", tpe = Type.Name("String"), desc = None), - ), - desc = None, - isValueClass = false, - `package` = List("io", "buildo", "baseexample", "models"), - ), CaseClass( name = "DuplicateName", members = List( @@ -191,6 +178,16 @@ class ModelSuite extends FunSuite { ), ), desc = Some("The name is already in use"), + isValueClass = false, + `package` = List("io", "buildo", "baseexample", "models"), + ), + CaseClass( + name = "IgnoreMe", + members = List( + CaseClass.Member(name = "ignore", tpe = Type.Name("String"), desc = None), + ), + desc = None, + isValueClass = false, `package` = List("io", "buildo", "baseexample", "models"), ), CaseClass( @@ -216,8 +213,8 @@ class ModelSuite extends FunSuite { `package` = List("io", "buildo", "baseexample", "models"), ), ) - val comparison = DiffShow.diff[List[Model]](expected, result) - assert(comparison.isIdentical, comparison.string) + + assertEquals(result.sortBy(_.name), expected.sortBy(_.name)) } } diff --git a/metarpheus/jsFacade/src/main/scala/io.buildo.metarpheus/core/JSFacade.scala b/metarpheus/jsFacade/src/main/scala/io.buildo.metarpheus/core/JSFacade.scala index 1340c793..e1cd4210 100644 --- a/metarpheus/jsFacade/src/main/scala/io.buildo.metarpheus/core/JSFacade.scala +++ b/metarpheus/jsFacade/src/main/scala/io.buildo.metarpheus/core/JSFacade.scala @@ -33,7 +33,7 @@ object JSFacade { try { val result = Metarpheus.run(paths.toList, config) val printer = Printer.noSpaces.copy(dropNullValues = true) - js.JSON.parse(printer.pretty(result.asJson)) + js.JSON.parse(printer.print(result.asJson)) } catch { case NonFatal(e) => throw js.JavaScriptException(e.getMessage) } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index f90bf7a3..19506e67 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -5,15 +5,13 @@ import scala.language.reflectiveCalls object Dependencies { val V = new { - val circe = "0.12.0-M1" - val scalatest = "3.0.5" + val circe = "0.13.0" val scalacheck = "1.14.0" val scalacheckMagnolia = "0.3.2" val mockito = "1.9.5" val akka = "2.6.4" - val akkaHttp = "10.1.3" - val akkaPersistence = "2.5.15.1" - val akkaHttpCirce = "1.25.2" + val akkaHttp = "10.1.11" + val akkaHttpCirce = "1.31.0" val awscala = "0.5.+" val cats = "1.6.0" val catsEffect = "1.3.0" @@ -32,19 +30,19 @@ object Dependencies { val flyway = "5.2.4" val bcrypt = "0.4" val slf4j = "1.7.25" - val scalameta = "4.1.10" + val scalameta = "4.3.6" val scalafmtCore = "2.0.0-RC5" val plantuml = "8059" val pprint = "0.5.9" val sbtLogging = "1.3.3" val tapir = "0.12.24" + val munit = "0.7.1" } val circeCore = "io.circe" %% "circe-core" % V.circe val circeParser = "io.circe" %% "circe-parser" % V.circe val circeGeneric = "io.circe" %% "circe-generic" % V.circe val circeGenericExtras = "io.circe" %% "circe-generic-extras" % V.circe - val scalatest = "org.scalatest" %% "scalatest" % V.scalatest val scalacheck = "org.scalacheck" %% "scalacheck" % V.scalacheck val scalacheckMagnolia = "com.github.chocpanda" %% "scalacheck-magnolia" % V.scalacheckMagnolia val mockito = "org.mockito" % "mockito-all" % V.mockito @@ -63,7 +61,6 @@ object Dependencies { val logback = "ch.qos.logback" % "logback-classic" % V.logback val levelDb = "org.fusesource.leveldbjni" % "leveldbjni-all" % V.leveldb val mailin = "com.sendinblue" % "sib-api-v3-sdk" % V.mailin - val akkaPersistenceInMemory = "com.github.dnvriend" %% "akka-persistence-inmemory" % V.akkaPersistence val akkaTestkit = "com.typesafe.akka" %% "akka-testkit" % V.akka val jakartaMail = "com.sun.mail" % "jakarta.mail" % V.jakartaMail val slick = "com.typesafe.slick" %% "slick" % V.slick @@ -75,7 +72,6 @@ object Dependencies { val ldap = "com.unboundid" % "unboundid-ldapsdk" % V.ldap val monixCatnap = "io.monix" %% "monix-catnap" % V.monixCatnap val slf4jNop = "org.slf4j" % "slf4j-nop" % V.slf4j - val diff = "ai.x" %% "diff" % "2.0" val scalameta = "org.scalameta" %% "scalameta" % V.scalameta val scalafmtCore = "org.scalameta" %% "scalafmt-core" % V.scalafmtCore val plantuml = "net.sourceforge.plantuml" % "plantuml" % V.plantuml @@ -85,9 +81,11 @@ object Dependencies { val tapirJsonCirce = "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % V.tapir val tapirCore = "com.softwaremill.sttp.tapir" %% "tapir-core" % V.tapir val tapirHttp4s = "com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % V.tapir + val munit = "org.scalameta" %% "munit" % V.munit + val munitScalaCheck = "org.scalameta" %% "munit-scalacheck" % V.munit val enumeroDependencies = List( - scalatest, + munit, mockito, ).map(_ % Test) @@ -95,7 +93,7 @@ object Dependencies { circeCore, ) ++ List( circeParser, - scalatest, + munit, ).map(_ % Test) val mailoDependencies = List( @@ -117,17 +115,17 @@ object Dependencies { levelDb, jakartaMail, ) ++ List( - scalatest, + munit, logback, akkaTestkit, - akkaPersistenceInMemory, ).map(_ % Test) val toctocCoreDependencies = List( bcrypt, catsCore, ) ++ List( - scalatest, + munit, + munitScalaCheck, scalacheck, scalacheckMagnolia, slf4jNop, @@ -141,7 +139,7 @@ object Dependencies { catsEffect, monixCatnap, ) ++ List( - scalatest, + munit, slf4jNop, ).map(_ % Test) @@ -152,7 +150,7 @@ object Dependencies { catsEffect, monixCatnap, ) ++ List( - scalatest, + munit, slf4jNop, ).map(_ % Test) @@ -168,16 +166,16 @@ object Dependencies { circeCore, circeGeneric, ) ++ List( - scalatest, scalacheck, scalacheckMagnolia, + munit, + munitScalaCheck, ).map(_ % Test) val metarpheusCoreDependencies = List( scalameta, ) ++ List( - scalatest, - diff, + munit, ).map(_ % Test) val metarpheusJsFacadeDependencies = List( @@ -191,7 +189,7 @@ object Dependencies { scalameta, scalafmtCore, circeCore, - pprint + pprint, ) val docsDependencies = List( @@ -199,7 +197,7 @@ object Dependencies { tapir, tapirJsonCirce, tapirCore, - tapirHttp4s + tapirHttp4s, ) } diff --git a/project/build.properties b/project/build.properties index c0bab049..a919a9b5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.2.8 +sbt.version=1.3.8 diff --git a/project/plugins.sbt b/project/plugins.sbt index 39cf4998..81ccbd79 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,8 +1,8 @@ addSbtPlugin("io.buildo" %% "sbt-buildo" % "0.11.5") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.1.4") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.32") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.1") addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.17.0") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0") resolvers += Resolver.sonatypeRepo("public") diff --git a/sbt-buildo/ci/compile.yml b/sbt-buildo/ci/compile.yml index c95623ef..b68707bd 100644 --- a/sbt-buildo/ci/compile.yml +++ b/sbt-buildo/ci/compile.yml @@ -4,7 +4,7 @@ image_resource: type: docker-image source: repository: buildo/scala-sbt-alpine - tag: 8u201_2.12.8_1.2.8 + tag: 8u201_2.12.11_1.3.8 inputs: - name: retro diff --git a/tapiro/ci/test.yml b/tapiro/ci/test.yml index f10e3755..ac210598 100644 --- a/tapiro/ci/test.yml +++ b/tapiro/ci/test.yml @@ -4,7 +4,7 @@ image_resource: type: docker-image source: repository: buildo/scala-sbt-alpine - tag: 8u201_2.12.8_1.2.8 + tag: 8u201_2.12.11_1.3.8 inputs: - name: retro diff --git a/tapiro/sbt-tapiro/src/sbt-test/sbt-tapiro/simple/src/main/resources/log4j2.xml b/tapiro/sbt-tapiro/src/sbt-test/sbt-tapiro/simple/src/main/resources/log4j2.xml new file mode 100644 index 00000000..94106519 --- /dev/null +++ b/tapiro/sbt-tapiro/src/sbt-test/sbt-tapiro/simple/src/main/resources/log4j2.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/toctoc/ci/docker-compose.yml b/toctoc/ci/docker-compose.yml index b4fb243c..1df0fda9 100644 --- a/toctoc/ci/docker-compose.yml +++ b/toctoc/ci/docker-compose.yml @@ -2,7 +2,7 @@ version: "3" services: tests: - image: buildo/scala-sbt-alpine:8u201_2.12.8_1.2.8 + image: buildo/scala-sbt-alpine:8u201_2.12.11_1.3.8 container_name: tests environment: DB_USER: postgres diff --git a/toctoc/circe/src/test/scala/io/buildo/toctoc/circe/CirceSupportSpec.scala b/toctoc/circe/src/test/scala/io/buildo/toctoc/circe/CirceSupportSpec.scala index e512ee18..07fa3209 100644 --- a/toctoc/circe/src/test/scala/io/buildo/toctoc/circe/CirceSupportSpec.scala +++ b/toctoc/circe/src/test/scala/io/buildo/toctoc/circe/CirceSupportSpec.scala @@ -2,8 +2,7 @@ package io.buildo.toctoc.circe import io.buildo.toctoc.core.authentication.TokenBasedAuthentication._ -import org.scalatest._ -import org.scalatest.prop._ +import org.scalacheck.Prop.forAll import org.scalacheck.magnolia._ import io.circe.syntax._ import io.circe.Json @@ -12,7 +11,7 @@ import org.scalacheck.Gen import java.time.Duration import java.time.temporal.ChronoUnit -final class CirceSupportSpec extends PropSpec with PropertyChecks { +final class CirceSupportSpec extends munit.ScalaCheckSuite { implicit val arbAccessToken: Arbitrary[AccessToken] = Arbitrary { for { @@ -23,12 +22,13 @@ final class CirceSupportSpec extends PropSpec with PropertyChecks { property("encodes AccessToken correctly") { forAll { (token: AccessToken) => - assertResult( + assertEquals( + token.asJson, Json.obj( "value" -> token.value.asJson, "expiresAt" -> token.expiresAt.asJson, ), - )(token.asJson) + ) } } @@ -38,18 +38,19 @@ final class CirceSupportSpec extends PropSpec with PropertyChecks { "value" -> token.value.asJson, "expiresAt" -> token.expiresAt.asJson, ) - assertResult(json.as[AccessToken].right.get)(token) + assertEquals(token, json.as[AccessToken].right.get) } } property("encodes Login correctly") { forAll { (login: Login) => - assertResult( + assertEquals( + login.asJson, Json.obj( "username" -> login.username.asJson, "password" -> login.password.asJson, ), - )(login.asJson) + ) } } @@ -59,7 +60,7 @@ final class CirceSupportSpec extends PropSpec with PropertyChecks { "username" -> login.username.asJson, "password" -> login.password.asJson, ) - assertResult(json.as[Login].right.get)(login) + assertEquals(login, json.as[Login].right.get) } } diff --git a/toctoc/core/src/test/scala/io.buildo.toctoc/core/CatsIOSupport.scala b/toctoc/core/src/test/scala/io.buildo.toctoc/core/CatsIOSupport.scala new file mode 100644 index 00000000..2a42bb48 --- /dev/null +++ b/toctoc/core/src/test/scala/io.buildo.toctoc/core/CatsIOSupport.scala @@ -0,0 +1,16 @@ +package io.buildo.toctoc +package core + +import cats.effect.IO + +trait CatsIOSupport { self: munit.FunSuite => + + def await[A, B](test: => IO[Either[A, B]])(implicit loc: munit.Location): B = + test + .unsafeRunSync() + .fold( + error => fail(error.toString()), + b => b, + ) + +} diff --git a/toctoc/core/src/test/scala/io.buildo.toctoc/core/authentication/TokenBasedRecoveryFlowSpec.scala b/toctoc/core/src/test/scala/io.buildo.toctoc/core/authentication/TokenBasedRecoveryFlowSpec.scala index 85046758..b034dcf4 100644 --- a/toctoc/core/src/test/scala/io.buildo.toctoc/core/authentication/TokenBasedRecoveryFlowSpec.scala +++ b/toctoc/core/src/test/scala/io.buildo.toctoc/core/authentication/TokenBasedRecoveryFlowSpec.scala @@ -2,18 +2,14 @@ package io.buildo.toctoc package core package authentication -import org.scalatest._ -import org.scalatest.prop._ -import org.scalactic.TypeCheckedTripleEquals +import org.scalacheck.Prop.forAll import org.scalacheck.magnolia._ +import cats.implicits._ import cats.effect.IO import cats.data.EitherT import java.time.Duration -final class TokenBasedRecoveryFlowSpec - extends PropSpec - with PropertyChecks - with TypeCheckedTripleEquals { +final class TokenBasedRecoveryFlowSpec extends munit.ScalaCheckSuite with CatsIOSupport { import TokenBasedAuthentication._ import TokenBasedRecovery._ @@ -26,17 +22,9 @@ final class TokenBasedRecoveryFlowSpec val recoveryFlow = TokenBasedRecoveryFlow.create(loginDomain, tokenDomain, Duration.ofDays(1)) - def check[A](test: => IO[Either[A, Assertion]]): Assertion = - test - .unsafeRunSync() - .fold( - error => fail(error.toString()), - assertion => assertion, - ) - property("recovery works for existing users") { forAll { (s: UserSubject, username: String, password: String) => - check { + await { (for { registerResult <- EitherT(recoveryFlow.registerForRecovery(s)) (f, token) = registerResult @@ -46,7 +34,7 @@ final class TokenBasedRecoveryFlowSpec authenticateResult <- EitherT(f2.loginD.authenticate(Login(username, password))) (_, subject) = authenticateResult } yield { - assertResult(s.ref)(subject.ref) + assertEquals(subject.ref, s.ref) }).value } } @@ -54,23 +42,23 @@ final class TokenBasedRecoveryFlowSpec property("recovery returns InvalidCredential for non existing users") { forAll { (s: UserSubject, password: String) => - check { + await { (for { registerResult <- EitherT(recoveryFlow.registerForRecovery(s)) (f, token) = registerResult result <- EitherT(f.recoverLogin(token, password) { _ => IO(None) }) - } yield result).leftMap { - assertResult(AuthenticationError.InvalidCredential)(_) - }.swap.value + } yield result).void.leftMap { e => + assertEquals(e, AuthenticationError.InvalidCredential) + }.value } } } property("successful recovery invalidates all existing credentials for a subject") { forAll { (s: UserSubject, username: String, password: String) => - check { + await { (for { registerResult <- EitherT(recoveryFlow.registerForRecovery(s)) (f, token1) = registerResult @@ -80,9 +68,9 @@ final class TokenBasedRecoveryFlowSpec authenticateResult <- EitherT(f2.loginD.authenticate(Login(username, password))) (_, subject) = authenticateResult recoverResult <- EitherT(f.recoverLogin(token1, password)(_ => IO(Some(username)))) - } yield recoverResult).leftMap { - assertResult(AuthenticationError.InvalidCredential)(_) - }.swap.value + } yield recoverResult).void.leftMap { e => + assertEquals(e, AuthenticationError.InvalidCredential) + }.value } } } diff --git a/toctoc/docs/concepts/functional-model.md b/toctoc/docs/concepts/functional-model.md deleted file mode 100644 index f82fafba..00000000 --- a/toctoc/docs/concepts/functional-model.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -id: functional-model -title: Functional Model ---- - -A **subject** is a human or machine user agent interacting with a secured -software application. - -```plaintext -s ∈ S -``` - -A **credential** is a secret that uniquely identifies a subject. - -```plaintext -c ∈ C -``` - -An **authentication domain** is a function `Fd` from credentials `C` subjects -`S`. - -```plaintext -Fd: C ⟶ S -``` - -In particular an authentication domain can be represented by a set -`D := {(c, s) | Fd(c) = s}`, where `D ⊂ P(C x S)`. - -Any fundamental authentication operation should be expressed in the context of -authentication domains, to be able to explicitly represent side effects. - -## Operations - -### Authenticate - -The authenticate operation checks whether a given credential `c` identifies a -subject `s`. This operation could possibly modify a given authentication domain: -for example, in the OTP use case, a credential must be used only once. - -```plaintext -Fa: D x C ⟶ D x S -``` - -### Register - -The register operation adds a new association `(c, s)`. This means that the -subject `s` can be identified by the credential `c`. - -```plaintext -Fr: D x C x S ⟶ D -``` - -### Unregister - -The unregister operation removes any associations `(c, s)` for any given subject -`s`. This means that `s` will not be identifiable in the authentication domain. - -```plaintext -Fu: D x C ⟶ D -``` - -For greater flexibility we can also define a companion operation that allows to -remove a single association `(c, s)`. This means that `s` will not be -identifiable by `c` in the authentication domain. - -```plaintext -Fu': D x C ⟶ D -``` - -### Exchange - -The exchange operations allows to use multiple authentication domains and -different credential types to implement complex authentication workflows. The -Token Based Authentication, for example, involves the use of login credentials -which can generate temporary access tokens. - -```plaintext -Fx: Da x Db x Ca x Cb ⟶ Da x Db -``` diff --git a/toctoc/docs/concepts/login.md b/toctoc/docs/concepts/login.md deleted file mode 100644 index a2627709..00000000 --- a/toctoc/docs/concepts/login.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -id: login -title: Login ---- - -A common kind of credential is the couple - -```plaintext -(username, password) -``` - -where `username` uniquely identifies a subject and `password` is a secret known -**only** to the subject. - -The lifecycle of login credentials must be handled with care. - -Passwords must: - -- Not be persisted anywhere by any agent other than the subject itself. -- Be transmitted using **cryptographically secure** transports. diff --git a/toctoc/docs/concepts/token.md b/toctoc/docs/concepts/token.md deleted file mode 100644 index 2fdb6254..00000000 --- a/toctoc/docs/concepts/token.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -id: token -title: Token ---- - -A **token** is a temporary secret that uniquely identifies a subject. It is -created and shared with the subject in exchange for another kind of credential. -A token lifecycle is usually decoupled from other credential kinds, to reduce -the security risks in case the token is disclosed to an attacker. diff --git a/toctoc/docs/installation.md b/toctoc/docs/installation.md deleted file mode 100644 index ab9afedd..00000000 --- a/toctoc/docs/installation.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -id: installation -title: Installation ---- - -`toctoc` is composed by multiple modules: - -- `toctoc-core`: defines the basic abstractions -- `toctoc-slick-postgresql`: provides slick-specific implementations for - Postgres databases -- `toctoc-slick-mysql`: provides slick-specific implementations for MySql - databases - -You can cherry-pick the modules according to the needs of your project. For -example: - -```scala -val V = new { - val toctoc = "@STABLE_VERSION@" -} - -libraryDependencies ++= List( - "io.buildo" %% "toctoc-core" % V.toctoc, - "io.buildo" %% "toctoc-slick-postgresql" % V.toctoc -) -``` - -## Snapshot versions - -We publish a snapshot version on every merge on master. - -The latest snapshot version is `@VERSION@` and you can use it to try the latest -unreleased features. For example: - -```scala -val V = new { - val toctoc = "@VERSION@" -} - -libraryDependencies ++= List( - "io.buildo" %% "toctoc-core" % V.toctoc, - "io.buildo" %% "toctoc-slick-postgresql" % V.toctoc -) -``` diff --git a/toctoc/slickMySql/src/test/scala/io.buildo.toctoc/slick/authentication/test/MySqlSlickLoginAuthenticationDomainFlowSpec.scala b/toctoc/slickMySql/src/test/scala/io.buildo.toctoc/slick/authentication/test/MySqlSlickLoginAuthenticationDomainFlowSpec.scala index 7c99f52b..1c1bfcaa 100644 --- a/toctoc/slickMySql/src/test/scala/io.buildo.toctoc/slick/authentication/test/MySqlSlickLoginAuthenticationDomainFlowSpec.scala +++ b/toctoc/slickMySql/src/test/scala/io.buildo.toctoc/slick/authentication/test/MySqlSlickLoginAuthenticationDomainFlowSpec.scala @@ -4,9 +4,6 @@ package authentication package test import core.authentication.TokenBasedAuthentication._ -import org.scalatest._ -import org.scalatest.FlatSpec -import org.scalatest.concurrent.ScalaFutures import _root_.slick.jdbc.MySQLProfile.api._ import _root_.slick.jdbc.JdbcBackend.Database import io.buildo.toctoc.slick.authentication.login.MySqlSlickLoginAuthenticationDomain @@ -14,13 +11,7 @@ import io.buildo.toctoc.slick.authentication.token.MySqlSlickAccessTokenAuthenti import cats.effect.IO import java.time.Duration -class MySqlSlickLoginAuthenticationDomainFlowSpec - extends FlatSpec - with BeforeAndAfterEach - with BeforeAndAfterAll - with ScalaFutures - with EitherValues - with Matchers { +class MySqlSlickLoginAuthenticationDomainFlowSpec extends munit.FunSuite { val loginTableName = "login" val tokenTableName = "token" @@ -40,9 +31,8 @@ class MySqlSlickLoginAuthenticationDomainFlowSpec IO.fromFuture(IO(db.run(schema.create))).unsafeRunSync } - override def afterEach(): Unit = { + override def afterEach(context: AfterEach): Unit = IO.fromFuture(IO(db.run(schema.truncate))).unsafeRunSync - } override def afterAll(): Unit = { IO.fromFuture(IO(db.run(schema.drop))).unsafeRunSync @@ -55,67 +45,67 @@ class MySqlSlickLoginAuthenticationDomainFlowSpec val subject = UserSubject("test") val subject2 = UserSubject("test2") - "unregistered login credentials" should "not be accepted when exchanging for token" in { - authFlow.exchangeForTokens(login).unsafeRunSync shouldBe 'left + test("unregistered login credentials should not be accepted when exchanging for token") { + assert(authFlow.exchangeForTokens(login).unsafeRunSync.isLeft) } - "registered login credentials" should "be accepted when exchanging for token" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right - authFlow.exchangeForTokens(login).unsafeRunSync shouldBe 'right + test("registered login credentials should be accepted when exchanging for token") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) + assert(authFlow.exchangeForTokens(login).unsafeRunSync.isRight) } - "token obtained by login" should "be validated" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right - val token = authFlow.exchangeForTokens(login).unsafeRunSync.right.value - authFlow.validateToken(token).unsafeRunSync.right.value shouldBe subject + test("token obtained by login should be validated") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) + val token = authFlow.exchangeForTokens(login).unsafeRunSync.right.get + assertEquals(authFlow.validateToken(token).unsafeRunSync.right.get, subject) } - "multiple login with same values" should "not be accepted in registration" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'left + test("multiple login with same values should not be accepted in registration") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isLeft) } - "multiple login with different values" should "be accepted in registration" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right - authFlow.registerSubjectLogin(subject2, login2).unsafeRunSync shouldBe 'right - val token2 = authFlow.exchangeForTokens(login2).unsafeRunSync.right.value - authFlow.validateToken(token2).unsafeRunSync.right.value shouldBe subject2 + test("multiple login with different values should be accepted in registration") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) + assert(authFlow.registerSubjectLogin(subject2, login2).unsafeRunSync.isRight) + val token2 = authFlow.exchangeForTokens(login2).unsafeRunSync.right.get + assertEquals(authFlow.validateToken(token2).unsafeRunSync.right.get, subject2) } - "single token unregistration" should "unregister only the specific token" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right - authFlow.registerSubjectLogin(subject2, login2).unsafeRunSync shouldBe 'right - val token = authFlow.exchangeForTokens(login).unsafeRunSync.right.value - val token2 = authFlow.exchangeForTokens(login2).unsafeRunSync.right.value + test("single token unregistration should unregister only the specific token") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) + assert(authFlow.registerSubjectLogin(subject2, login2).unsafeRunSync.isRight) + val token = authFlow.exchangeForTokens(login).unsafeRunSync.right.get + val token2 = authFlow.exchangeForTokens(login2).unsafeRunSync.right.get authFlow.unregisterToken(token).unsafeRunSync - authFlow.validateToken(token).unsafeRunSync shouldBe 'left - authFlow.validateToken(token2).unsafeRunSync shouldBe 'right + assert(authFlow.validateToken(token).unsafeRunSync.isLeft) + assert(authFlow.validateToken(token2).unsafeRunSync.isRight) } - "token unregistration" should "unregister all subject's tokens" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right - authFlow.registerSubjectLogin(subject2, login2).unsafeRunSync shouldBe 'right - val token = authFlow.exchangeForTokens(login).unsafeRunSync.right.value - val token2 = authFlow.exchangeForTokens(login).unsafeRunSync.right.value - val token3 = authFlow.exchangeForTokens(login2).unsafeRunSync.right.value + test("token unregistration should unregister all subject's tokens") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) + assert(authFlow.registerSubjectLogin(subject2, login2).unsafeRunSync.isRight) + val token = authFlow.exchangeForTokens(login).unsafeRunSync.right.get + val token2 = authFlow.exchangeForTokens(login).unsafeRunSync.right.get + val token3 = authFlow.exchangeForTokens(login2).unsafeRunSync.right.get authFlow.unregisterAllSubjectTokens(subject).unsafeRunSync - authFlow.validateToken(token).unsafeRunSync shouldBe 'left - authFlow.validateToken(token2).unsafeRunSync shouldBe 'left - authFlow.validateToken(token3).unsafeRunSync shouldBe 'right + assert(authFlow.validateToken(token).unsafeRunSync.isLeft) + assert(authFlow.validateToken(token2).unsafeRunSync.isLeft) + assert(authFlow.validateToken(token3).unsafeRunSync.isRight) } - "single subject credentials unregistration" should "take effect" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right + test("single subject credentials unregistration should take effect") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) authFlow.unregisterLogin(login).unsafeRunSync - authFlow.exchangeForTokens(login).unsafeRunSync shouldBe 'left + assert(authFlow.exchangeForTokens(login).unsafeRunSync.isLeft) } - "subject credentials unregistration" should "take effect" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right - authFlow.registerSubjectLogin(subject, login3).unsafeRunSync shouldBe 'right + test("subject credentials unregistration should take effect") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) + assert(authFlow.registerSubjectLogin(subject, login3).unsafeRunSync.isRight) authFlow.unregisterAllSubjectLogins(subject).unsafeRunSync - authFlow.exchangeForTokens(login).unsafeRunSync shouldBe 'left - authFlow.exchangeForTokens(login3).unsafeRunSync shouldBe 'left + assert(authFlow.exchangeForTokens(login).unsafeRunSync.isLeft) + assert(authFlow.exchangeForTokens(login3).unsafeRunSync.isLeft) } } diff --git a/toctoc/slickPostgreSql/src/test/scala/io.buildo.toctoc/slick/authentication/test/PostgreSqlSlickLoginAuthenticationDomainFlowSpec.scala b/toctoc/slickPostgreSql/src/test/scala/io.buildo.toctoc/slick/authentication/test/PostgreSqlSlickLoginAuthenticationDomainFlowSpec.scala index b34f0b89..405ade1c 100644 --- a/toctoc/slickPostgreSql/src/test/scala/io.buildo.toctoc/slick/authentication/test/PostgreSqlSlickLoginAuthenticationDomainFlowSpec.scala +++ b/toctoc/slickPostgreSql/src/test/scala/io.buildo.toctoc/slick/authentication/test/PostgreSqlSlickLoginAuthenticationDomainFlowSpec.scala @@ -7,20 +7,13 @@ import core.authentication.TokenBasedAuthentication._ import login.PostgreSqlSlickLoginAuthenticationDomain import token.PostgreSqlSlickAccessTokenAuthenticationDomain -import org.scalatest._ -import org.scalatest.FlatSpec import _root_.slick.jdbc.PostgresProfile.api._ import _root_.slick.jdbc.JdbcBackend.Database import cats.effect.IO import java.time.Duration -class PostgreSqlSlickLoginAuthenticationDomainFlowSpec - extends FlatSpec - with BeforeAndAfterEach - with BeforeAndAfterAll - with EitherValues - with Matchers { +class PostgreSqlSlickLoginAuthenticationDomainFlowSpec extends munit.FunSuite { val schemaName = Some("public") val loginTableName = "login" @@ -43,7 +36,7 @@ class PostgreSqlSlickLoginAuthenticationDomainFlowSpec IO.fromFuture(IO(db.run(schema.create))).unsafeRunSync } - override def afterEach() = { + override def afterEach(context: AfterEach) = { IO.fromFuture(IO(db.run(schema.truncate))).unsafeRunSync } @@ -58,67 +51,67 @@ class PostgreSqlSlickLoginAuthenticationDomainFlowSpec val subject = UserSubject("test") val subject2 = UserSubject("test2") - "unregistered login credentials" should "not be accepted when exchanging for token" in { - authFlow.exchangeForTokens(login).unsafeRunSync shouldBe 'left + test("unregistered login credentials should not be accepted when exchanging for token") { + assert(authFlow.exchangeForTokens(login).unsafeRunSync.isLeft) } - "registered login credentials" should "be accepted when exchanging for token" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right - authFlow.exchangeForTokens(login).unsafeRunSync shouldBe 'right + test("registered login credentials should be accepted when exchanging for token") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) + assert(authFlow.exchangeForTokens(login).unsafeRunSync.isRight) } - "token obtained by login" should "be validated" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right - val token = authFlow.exchangeForTokens(login).unsafeRunSync.right.value - authFlow.validateToken(token).unsafeRunSync.right.value shouldBe subject + test("token obtained by login should be validated") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) + val token = authFlow.exchangeForTokens(login).unsafeRunSync.right.get + assertEquals(authFlow.validateToken(token).unsafeRunSync.right.get, subject) } - "multiple login with same values" should "not be accepted in registration" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'left + test("multiple login with same values should not be accepted in registration") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isLeft) } - "multiple login with different values" should "be accepted in registration" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right - authFlow.registerSubjectLogin(subject2, login2).unsafeRunSync shouldBe 'right - val token2 = authFlow.exchangeForTokens(login2).unsafeRunSync.right.value - authFlow.validateToken(token2).unsafeRunSync.right.value shouldBe subject2 + test("multiple login with different values should be accepted in registration") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) + assert(authFlow.registerSubjectLogin(subject2, login2).unsafeRunSync.isRight) + val token2 = authFlow.exchangeForTokens(login2).unsafeRunSync.right.get + assertEquals(authFlow.validateToken(token2).unsafeRunSync.right.get, subject2) } - "single token unregistration" should "unregister only the specific token" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right - authFlow.registerSubjectLogin(subject2, login2).unsafeRunSync shouldBe 'right - val token = authFlow.exchangeForTokens(login).unsafeRunSync.right.value - val token2 = authFlow.exchangeForTokens(login2).unsafeRunSync.right.value + test("single token unregistration should unregister only the specific token") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) + assert(authFlow.registerSubjectLogin(subject2, login2).unsafeRunSync.isRight) + val token = authFlow.exchangeForTokens(login).unsafeRunSync.right.get + val token2 = authFlow.exchangeForTokens(login2).unsafeRunSync.right.get authFlow.unregisterToken(token).unsafeRunSync - authFlow.validateToken(token).unsafeRunSync shouldBe 'left - authFlow.validateToken(token2).unsafeRunSync shouldBe 'right + assert(authFlow.validateToken(token).unsafeRunSync.isLeft) + assert(authFlow.validateToken(token2).unsafeRunSync.isRight) } - "token unregistration" should "unregister all subject's tokens" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right - authFlow.registerSubjectLogin(subject2, login2).unsafeRunSync shouldBe 'right - val token = authFlow.exchangeForTokens(login).unsafeRunSync.right.value - val token2 = authFlow.exchangeForTokens(login).unsafeRunSync.right.value - val token3 = authFlow.exchangeForTokens(login2).unsafeRunSync.right.value + test("token unregistration should unregister all subject's tokens") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) + assert(authFlow.registerSubjectLogin(subject2, login2).unsafeRunSync.isRight) + val token = authFlow.exchangeForTokens(login).unsafeRunSync.right.get + val token2 = authFlow.exchangeForTokens(login).unsafeRunSync.right.get + val token3 = authFlow.exchangeForTokens(login2).unsafeRunSync.right.get authFlow.unregisterAllSubjectTokens(subject).unsafeRunSync - authFlow.validateToken(token).unsafeRunSync shouldBe 'left - authFlow.validateToken(token2).unsafeRunSync shouldBe 'left - authFlow.validateToken(token3).unsafeRunSync shouldBe 'right + assert(authFlow.validateToken(token).unsafeRunSync.isLeft) + assert(authFlow.validateToken(token2).unsafeRunSync.isLeft) + assert(authFlow.validateToken(token3).unsafeRunSync.isRight) } - "single subject credentials unregistration" should "take effect" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right + test("single subject credentials unregistration should take effect") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) authFlow.unregisterLogin(login).unsafeRunSync - authFlow.exchangeForTokens(login).unsafeRunSync shouldBe 'left + assert(authFlow.exchangeForTokens(login).unsafeRunSync.isLeft) } - "subject credentials unregistration" should "take effect" in { - authFlow.registerSubjectLogin(subject, login).unsafeRunSync shouldBe 'right - authFlow.registerSubjectLogin(subject, login3).unsafeRunSync shouldBe 'right + test("subject credentials unregistration should take effect") { + assert(authFlow.registerSubjectLogin(subject, login).unsafeRunSync.isRight) + assert(authFlow.registerSubjectLogin(subject, login3).unsafeRunSync.isRight) authFlow.unregisterAllSubjectLogins(subject).unsafeRunSync - authFlow.exchangeForTokens(login).unsafeRunSync shouldBe 'left - authFlow.exchangeForTokens(login3).unsafeRunSync shouldBe 'left + assert(authFlow.exchangeForTokens(login).unsafeRunSync.isLeft) + assert(authFlow.exchangeForTokens(login3).unsafeRunSync.isLeft) } }