diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/Dependencies.scala b/backend/src/main/scala/com/softwaremill/bootzooka/Dependencies.scala new file mode 100644 index 000000000..82ec3fb34 --- /dev/null +++ b/backend/src/main/scala/com/softwaremill/bootzooka/Dependencies.scala @@ -0,0 +1,48 @@ +package com.softwaremill.bootzooka + +import cats.data.NonEmptyList +import cats.effect.{IO, Resource} +import com.softwaremill.bootzooka.config.Config +import com.softwaremill.bootzooka.email.EmailService +import com.softwaremill.bootzooka.email.sender.EmailSender +import com.softwaremill.bootzooka.http.{Http, HttpApi, HttpConfig} +import com.softwaremill.bootzooka.metrics.{MetricsApi, VersionApi} +import com.softwaremill.bootzooka.passwordreset.{PasswordResetApi, PasswordResetAuthToken} +import com.softwaremill.bootzooka.security.ApiKeyAuthToken +import com.softwaremill.bootzooka.user.UserApi +import com.softwaremill.bootzooka.util.{Clock, DefaultIdGenerator} +import com.softwaremill.macwire.autocats.autowire +import doobie.util.transactor.Transactor +import io.prometheus.client.CollectorRegistry +import sttp.client3.SttpBackend + +case class Dependencies(api: HttpApi, emailService: EmailService) + +object Dependencies { + def wire(config: Config, sttpBackend: Resource[IO, SttpBackend[IO, Any]], xa: Resource[IO, Transactor[IO]], clock: Clock): Resource[IO, Dependencies] = { + def buildHttpApi(http: Http, userApi: UserApi, passwordResetApi: PasswordResetApi, metricsApi: MetricsApi, versionApi: VersionApi, collectorRegistry: CollectorRegistry, cfg: HttpConfig) = + new HttpApi( + http, + userApi.endpoints concatNel passwordResetApi.endpoints, + NonEmptyList.of(metricsApi.metricsEndpoint, versionApi.versionEndpoint), + collectorRegistry, + cfg) + + autowire[Dependencies]( + config.api, + config.user, + config.passwordReset, + config.email, + DefaultIdGenerator, + clock, + CollectorRegistry.defaultRegistry, + sttpBackend, + xa, + buildHttpApi _, + new ApiKeyAuthToken(_), + new EmailService(_, _, _, _, _), + EmailSender.create _, + new PasswordResetAuthToken(_), + ) + } +} diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/InitModule.scala b/backend/src/main/scala/com/softwaremill/bootzooka/InitModule.scala deleted file mode 100644 index 83795cf88..000000000 --- a/backend/src/main/scala/com/softwaremill/bootzooka/InitModule.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.softwaremill.bootzooka - -import cats.effect.{IO, Resource} -import com.softwaremill.bootzooka.config.ConfigModule -import com.softwaremill.bootzooka.infrastructure.DB -import sttp.capabilities.WebSockets -import sttp.capabilities.fs2.Fs2Streams -import sttp.client3.SttpBackend -import sttp.client3.asynchttpclient.fs2.AsyncHttpClientFs2Backend - -/** Initialised resources needed by the application to start. - */ -trait InitModule extends ConfigModule { - lazy val db: DB = new DB(config.db) - lazy val baseSttpBackend: Resource[IO, SttpBackend[IO, Fs2Streams[IO] with WebSockets]] = - AsyncHttpClientFs2Backend.resource() -} diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/Main.scala b/backend/src/main/scala/com/softwaremill/bootzooka/Main.scala index 53e973eaa..9c2fc739b 100644 --- a/backend/src/main/scala/com/softwaremill/bootzooka/Main.scala +++ b/backend/src/main/scala/com/softwaremill/bootzooka/Main.scala @@ -1,38 +1,45 @@ package com.softwaremill.bootzooka -import cats.effect.IO import cats.effect.unsafe.implicits.global +import cats.effect.{IO, Resource} import com.softwaremill.bootzooka.config.Config +import com.softwaremill.bootzooka.infrastructure.DB import com.softwaremill.bootzooka.metrics.Metrics +import com.softwaremill.bootzooka.util.DefaultClock import com.typesafe.scalalogging.StrictLogging -import doobie.util.transactor +import sttp.capabilities.WebSockets +import sttp.capabilities.fs2.Fs2Streams import sttp.client3.SttpBackend +import sttp.client3.asynchttpclient.fs2.AsyncHttpClientFs2Backend +import sttp.client3.logging.slf4j.Slf4jLoggingBackend +import sttp.client3.prometheus.PrometheusBackend object Main extends StrictLogging { def main(args: Array[String]): Unit = { Metrics.init() Thread.setDefaultUncaughtExceptionHandler((t, e) => logger.error("Uncaught exception in thread: " + t, e)) - val initModule = new InitModule {} - initModule.logConfig() + val config = Config.read + Config.log(config) - val mainTask = initModule.db.transactorResource.use { _xa => - initModule.baseSttpBackend.use { _baseSttpBackend => - val modules = new MainModule { - override def xa: transactor.Transactor[IO] = _xa - override def baseSttpBackend: SttpBackend[IO, Any] = _baseSttpBackend - override def config: Config = initModule.config - } + lazy val sttpBackend: Resource[IO, SttpBackend[IO, Fs2Streams[IO] with WebSockets]] = + AsyncHttpClientFs2Backend + .resource[IO]() + .map(baseSttpBackend => Slf4jLoggingBackend(PrometheusBackend(baseSttpBackend), includeTiming = true)) + val xa = new DB(config.db).transactorResource + + Dependencies + .wire(config, sttpBackend, xa, DefaultClock) + .use { case Dependencies(httpApi, emailService) => /* Sequencing two tasks using the >> operator: - the first starts the background processes (such as an email sender) - the second allocates the http api resource, and never releases it (so that the http server is available as long as our application runs) */ - modules.startBackgroundProcesses >> modules.httpApi.resource.use(_ => IO.never) + emailService.startProcesses().void >> httpApi.resource.use(_ => IO.never) } - } - mainTask.unsafeRunSync() + .unsafeRunSync() } } diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/MainModule.scala b/backend/src/main/scala/com/softwaremill/bootzooka/MainModule.scala deleted file mode 100644 index 5988693c0..000000000 --- a/backend/src/main/scala/com/softwaremill/bootzooka/MainModule.scala +++ /dev/null @@ -1,35 +0,0 @@ -package com.softwaremill.bootzooka - -import cats.data.NonEmptyList -import cats.effect.IO -import com.softwaremill.bootzooka.email.EmailModule -import com.softwaremill.bootzooka.http.{Http, HttpApi} -import com.softwaremill.bootzooka.infrastructure.InfrastructureModule -import com.softwaremill.bootzooka.metrics.MetricsModule -import com.softwaremill.bootzooka.passwordreset.PasswordResetModule -import com.softwaremill.bootzooka.security.SecurityModule -import com.softwaremill.bootzooka.user.UserModule -import com.softwaremill.bootzooka.util.{Clock, DefaultClock, DefaultIdGenerator, IdGenerator, ServerEndpoints} - -/** Main application module. Depends on resources initialised in [[InitModule]]. - */ -trait MainModule - extends SecurityModule - with EmailModule - with UserModule - with PasswordResetModule - with MetricsModule - with InfrastructureModule { - - override lazy val idGenerator: IdGenerator = DefaultIdGenerator - override lazy val clock: Clock = DefaultClock - - lazy val http: Http = new Http() - - private lazy val endpoints: ServerEndpoints = userApi.endpoints concatNel passwordResetApi.endpoints - private lazy val adminEndpoints: ServerEndpoints = NonEmptyList.of(metricsApi.metricsEndpoint, versionApi.versionEndpoint) - - lazy val httpApi: HttpApi = new HttpApi(http, endpoints, adminEndpoints, collectorRegistry, config.api) - - lazy val startBackgroundProcesses: IO[Unit] = emailService.startProcesses().void -} diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/config/Config.scala b/backend/src/main/scala/com/softwaremill/bootzooka/config/Config.scala index 782285cd7..2de3798a5 100644 --- a/backend/src/main/scala/com/softwaremill/bootzooka/config/Config.scala +++ b/backend/src/main/scala/com/softwaremill/bootzooka/config/Config.scala @@ -5,7 +5,37 @@ import com.softwaremill.bootzooka.http.HttpConfig import com.softwaremill.bootzooka.infrastructure.DBConfig import com.softwaremill.bootzooka.passwordreset.PasswordResetConfig import com.softwaremill.bootzooka.user.UserConfig +import com.softwaremill.bootzooka.version.BuildInfo +import com.typesafe.scalalogging.StrictLogging +import pureconfig.ConfigSource +import pureconfig.generic.auto._ -/** Maps to the `application.conf` file. Configuration for all modules of the application. - */ +import scala.collection.immutable.TreeMap + +/** Maps to the `application.conf` file. Configuration for all modules of the application. */ case class Config(db: DBConfig, api: HttpConfig, email: EmailConfig, passwordReset: PasswordResetConfig, user: UserConfig) + +object Config extends StrictLogging { + def log(config: Config): Unit = { + val baseInfo = s""" + |Bootzooka configuration: + |----------------------- + |DB: ${config.db} + |API: ${config.api} + |Email: ${config.email} + |Password reset: ${config.passwordReset} + |User: ${config.user} + | + |Build & env info: + |----------------- + |""".stripMargin + + val info = TreeMap(BuildInfo.toMap.toSeq: _*).foldLeft(baseInfo) { case (str, (k, v)) => + str + s"$k: $v\n" + } + + logger.info(info) + } + + def read: Config = ConfigSource.default.loadOrThrow[Config] +} diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/config/ConfigModule.scala b/backend/src/main/scala/com/softwaremill/bootzooka/config/ConfigModule.scala deleted file mode 100644 index b2f27deef..000000000 --- a/backend/src/main/scala/com/softwaremill/bootzooka/config/ConfigModule.scala +++ /dev/null @@ -1,36 +0,0 @@ -package com.softwaremill.bootzooka.config - -import com.softwaremill.bootzooka.version.BuildInfo -import com.typesafe.scalalogging.StrictLogging -import pureconfig.ConfigSource -import pureconfig.generic.auto._ - -import scala.collection.immutable.TreeMap - -/** Reads and gives access to the configuration object. - */ -trait ConfigModule extends StrictLogging { - - lazy val config: Config = ConfigSource.default.loadOrThrow[Config] - - def logConfig(): Unit = { - val baseInfo = s""" - |Bootzooka configuration: - |----------------------- - |DB: ${config.db} - |API: ${config.api} - |Email: ${config.email} - |Password reset: ${config.passwordReset} - |User: ${config.user} - | - |Build & env info: - |----------------- - |""".stripMargin - - val info = TreeMap(BuildInfo.toMap.toSeq: _*).foldLeft(baseInfo) { case (str, (k, v)) => - str + s"$k: $v\n" - } - - logger.info(info) - } -} diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/email/EmailModule.scala b/backend/src/main/scala/com/softwaremill/bootzooka/email/EmailModule.scala deleted file mode 100644 index 4a1e3279e..000000000 --- a/backend/src/main/scala/com/softwaremill/bootzooka/email/EmailModule.scala +++ /dev/null @@ -1,26 +0,0 @@ -package com.softwaremill.bootzooka.email - -import cats.effect.IO -import com.softwaremill.bootzooka.email.sender.{DummyEmailSender, EmailSender, MailgunEmailSender, SmtpEmailSender} -import com.softwaremill.bootzooka.util.BaseModule -import sttp.client3.SttpBackend -import doobie.util.transactor.Transactor - -trait EmailModule extends BaseModule { - lazy val emailModel = new EmailModel - lazy val emailService = new EmailService(emailModel, idGenerator, emailSender, config.email, xa) - // the EmailService implements the EmailScheduler functionality - hence, creating an alias for this dependency - lazy val emailScheduler: EmailScheduler = emailService - lazy val emailTemplates = new EmailTemplates() - // depending on the configuration, creating the appropriate EmailSender instance - lazy val emailSender: EmailSender = if (config.email.mailgun.enabled) { - new MailgunEmailSender(config.email.mailgun, sttpBackend) - } else if (config.email.smtp.enabled) { - new SmtpEmailSender(config.email.smtp) - } else { - DummyEmailSender - } - - def xa: Transactor[IO] - def sttpBackend: SttpBackend[IO, Any] -} diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/email/sender/EmailSender.scala b/backend/src/main/scala/com/softwaremill/bootzooka/email/sender/EmailSender.scala index 5c92a3193..9e53c16f1 100644 --- a/backend/src/main/scala/com/softwaremill/bootzooka/email/sender/EmailSender.scala +++ b/backend/src/main/scala/com/softwaremill/bootzooka/email/sender/EmailSender.scala @@ -1,8 +1,19 @@ package com.softwaremill.bootzooka.email.sender import cats.effect.IO -import com.softwaremill.bootzooka.email.EmailData +import com.softwaremill.bootzooka.email.{EmailConfig, EmailData} +import sttp.client3.SttpBackend trait EmailSender { def apply(email: EmailData): IO[Unit] } + +object EmailSender { + def create(sttpBackend: SttpBackend[IO, Any], config: EmailConfig): EmailSender = if (config.mailgun.enabled) { + new MailgunEmailSender(config.mailgun, sttpBackend) + } else if (config.smtp.enabled) { + new SmtpEmailSender(config.smtp) + } else { + DummyEmailSender + } +} diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/http/HttpApi.scala b/backend/src/main/scala/com/softwaremill/bootzooka/http/HttpApi.scala index e3c192037..8aae68343 100644 --- a/backend/src/main/scala/com/softwaremill/bootzooka/http/HttpApi.scala +++ b/backend/src/main/scala/com/softwaremill/bootzooka/http/HttpApi.scala @@ -8,7 +8,7 @@ import com.typesafe.scalalogging.StrictLogging import io.prometheus.client.CollectorRegistry import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.metrics.prometheus.Prometheus -import org.http4s.server.Router +import org.http4s.server.{Router, Server} import org.http4s.server.middleware.{CORS, Metrics} import org.http4s.server.staticcontent._ import org.http4s.{HttpApp, HttpRoutes, Request, Response} @@ -40,29 +40,36 @@ class HttpApi( /** The resource describing the HTTP server; binds when the resource is allocated. */ lazy val resource: Resource[IO, org.http4s.server.Server] = { - val prometheusHttp4sMetrics = Prometheus.metricsOps[IO](collectorRegistry) - prometheusHttp4sMetrics - .map(m => Metrics[IO](m)(mainRoutes)) - .flatMap { monitoredRoutes => - val app: HttpApp[IO] = Router( - // for /api/v1 requests, first trying the API; then the docs; then, returning 404 - s"/${apiContextPath.mkString("/")}" -> { - CORS.policy.withAllowOriginAll - .withAllowCredentials(false) - .apply(monitoredRoutes <+> docsRoutes <+> respondWithNotFound) - }, - "/admin" -> adminRoutes, - // for all other requests, first trying getting existing webapp resource; - // otherwise, returning index.html; this is needed to support paths in the frontend apps (e.g. /login) - // the frontend app will handle displaying appropriate error messages - "" -> (webappRoutes <+> indexResponse()) - ).orNotFound + import com.softwaremill.macwire.autocats._ - BlazeServerBuilder[IO] - .bindHttp(config.port, config.host) - .withHttpApp(app) - .resource - } + val monitoredRoues = + Prometheus.metricsOps[IO](collectorRegistry).map(m => Metrics[IO](m)(mainRoutes)) + + def buildApp(monitoredRoutes: HttpRoutes[IO]): HttpApp[IO] = Router( + // for /api/v1 requests, first trying the API; then the docs; then, returning 404 + s"/${apiContextPath.mkString("/")}" -> { + CORS.policy + .withAllowOriginAll + .withAllowCredentials(false) + .apply(monitoredRoutes <+> docsRoutes <+> respondWithNotFound) + }, + "/admin" -> adminRoutes, + // for all other requests, first trying getting existing webapp resource; + // otherwise, returning index.html; this is needed to support paths in the frontend apps (e.g. /login) + // the frontend app will handle displaying appropriate error messages + "" -> (webappRoutes <+> indexResponse()) + ).orNotFound + + def buildServer(app: HttpApp[IO]): Resource[IO, Server] = BlazeServerBuilder[IO] + .bindHttp(config.port, config.host) + .withHttpApp(app) + .resource + + autowire[Server]( + monitoredRoues, + buildApp _, + buildServer _ + ) } private def indexResponse(): HttpRoutes[IO] = { diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/infrastructure/DB.scala b/backend/src/main/scala/com/softwaremill/bootzooka/infrastructure/DB.scala index a2f8331b9..6faa37c57 100644 --- a/backend/src/main/scala/com/softwaremill/bootzooka/infrastructure/DB.scala +++ b/backend/src/main/scala/com/softwaremill/bootzooka/infrastructure/DB.scala @@ -9,9 +9,11 @@ import org.flywaydb.core.Flyway import scala.concurrent.duration._ import Doobie._ import com.softwaremill.bootzooka.config.Sensitive +import com.softwaremill.macwire.autocats.autowire -/** Configures the database, setting up the connection pool and performing migrations. - */ +import scala.concurrent.ExecutionContext + +/** Configures the database, setting up the connection pool and performing migrations. */ class DB(_config: DBConfig) extends StrictLogging { private val config: DBConfig = { @@ -36,17 +38,17 @@ class DB(_config: DBConfig) extends StrictLogging { * * See also: https://tpolecat.github.io/doobie/docs/14-Managing-Connections.html#about-threading */ - for { - connectEC <- doobie.util.ExecutionContexts.fixedThreadPool[IO](config.connectThreadPoolSize) - xa <- HikariTransactor.newHikariTransactor[IO]( - config.driver, - config.url, - config.username, - config.password.value, - connectEC - ) - _ <- Resource.eval(connectAndMigrate(xa)) - } yield xa + def buildTransactor(ec: ExecutionContext) = HikariTransactor.newHikariTransactor[IO]( + config.driver, + config.url, + config.username, + config.password.value, + ec + ) + autowire[Transactor[IO]]( + doobie.util.ExecutionContexts.fixedThreadPool[IO](config.connectThreadPoolSize), + buildTransactor _ + ).evalTap(connectAndMigrate) } private def connectAndMigrate(xa: Transactor[IO]): IO[Unit] = { diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/infrastructure/InfrastructureModule.scala b/backend/src/main/scala/com/softwaremill/bootzooka/infrastructure/InfrastructureModule.scala deleted file mode 100644 index 374f86bb5..000000000 --- a/backend/src/main/scala/com/softwaremill/bootzooka/infrastructure/InfrastructureModule.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.softwaremill.bootzooka.infrastructure - -import cats.effect.IO -import sttp.client3.SttpBackend -import sttp.client3.prometheus.PrometheusBackend -import sttp.client3.logging.slf4j.Slf4jLoggingBackend - -trait InfrastructureModule { - implicit lazy val sttpBackend: SttpBackend[IO, Any] = Slf4jLoggingBackend(PrometheusBackend(baseSttpBackend), includeTiming = true) - - def baseSttpBackend: SttpBackend[IO, Any] -} diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/metrics/MetricsModule.scala b/backend/src/main/scala/com/softwaremill/bootzooka/metrics/MetricsModule.scala deleted file mode 100644 index ff994b179..000000000 --- a/backend/src/main/scala/com/softwaremill/bootzooka/metrics/MetricsModule.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.softwaremill.bootzooka.metrics - -import com.softwaremill.bootzooka.http.Http -import io.prometheus.client.CollectorRegistry - -trait MetricsModule { - lazy val metricsApi = new MetricsApi(http, collectorRegistry) - lazy val versionApi = new VersionApi(http) - lazy val collectorRegistry: CollectorRegistry = CollectorRegistry.defaultRegistry - - def http: Http -} diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/passwordreset/PasswordResetModule.scala b/backend/src/main/scala/com/softwaremill/bootzooka/passwordreset/PasswordResetModule.scala deleted file mode 100644 index 5c19a9877..000000000 --- a/backend/src/main/scala/com/softwaremill/bootzooka/passwordreset/PasswordResetModule.scala +++ /dev/null @@ -1,33 +0,0 @@ -package com.softwaremill.bootzooka.passwordreset - -import cats.effect.IO -import com.softwaremill.bootzooka.email.{EmailScheduler, EmailTemplates} -import com.softwaremill.bootzooka.http.Http -import com.softwaremill.bootzooka.security.Auth -import com.softwaremill.bootzooka.user.UserModel -import com.softwaremill.bootzooka.util.BaseModule -import com.softwaremill.bootzooka.infrastructure.Doobie._ - -trait PasswordResetModule extends BaseModule { - lazy val passwordResetCodeModel = new PasswordResetCodeModel - lazy val passwordResetService = - new PasswordResetService( - userModel, - passwordResetCodeModel, - emailScheduler, - emailTemplates, - passwordResetCodeAuth, - idGenerator, - config.passwordReset, - clock, - xa - ) - lazy val passwordResetApi = new PasswordResetApi(http, passwordResetService, xa) - - def userModel: UserModel - def http: Http - def passwordResetCodeAuth: Auth[PasswordResetCode] - def emailScheduler: EmailScheduler - def emailTemplates: EmailTemplates - def xa: Transactor[IO] -} diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/security/SecurityModule.scala b/backend/src/main/scala/com/softwaremill/bootzooka/security/SecurityModule.scala deleted file mode 100644 index 0b9c2f9bb..000000000 --- a/backend/src/main/scala/com/softwaremill/bootzooka/security/SecurityModule.scala +++ /dev/null @@ -1,16 +0,0 @@ -package com.softwaremill.bootzooka.security - -import cats.effect.IO -import com.softwaremill.bootzooka.passwordreset.{PasswordResetAuthToken, PasswordResetCode, PasswordResetCodeModel} -import com.softwaremill.bootzooka.util.BaseModule -import doobie.util.transactor.Transactor - -trait SecurityModule extends BaseModule { - lazy val apiKeyModel = new ApiKeyModel - lazy val apiKeyService = new ApiKeyService(apiKeyModel, idGenerator, clock) - lazy val apiKeyAuth: Auth[ApiKey] = new Auth(new ApiKeyAuthToken(apiKeyModel), xa, clock) - lazy val passwordResetCodeAuth: Auth[PasswordResetCode] = new Auth(new PasswordResetAuthToken(passwordResetCodeModel), xa, clock) - - def passwordResetCodeModel: PasswordResetCodeModel - def xa: Transactor[IO] -} diff --git a/backend/src/main/scala/com/softwaremill/bootzooka/user/UserModule.scala b/backend/src/main/scala/com/softwaremill/bootzooka/user/UserModule.scala deleted file mode 100644 index 36517b58e..000000000 --- a/backend/src/main/scala/com/softwaremill/bootzooka/user/UserModule.scala +++ /dev/null @@ -1,21 +0,0 @@ -package com.softwaremill.bootzooka.user - -import cats.effect.IO -import com.softwaremill.bootzooka.email.{EmailScheduler, EmailTemplates} -import com.softwaremill.bootzooka.http.Http -import com.softwaremill.bootzooka.security.{ApiKey, ApiKeyService, Auth} -import com.softwaremill.bootzooka.util.BaseModule -import doobie.util.transactor.Transactor - -trait UserModule extends BaseModule { - lazy val userModel = new UserModel - lazy val userApi = new UserApi(http, apiKeyAuth, userService, xa) - lazy val userService = new UserService(userModel, emailScheduler, emailTemplates, apiKeyService, idGenerator, clock, config.user) - - def http: Http - def apiKeyAuth: Auth[ApiKey] - def emailScheduler: EmailScheduler - def emailTemplates: EmailTemplates - def apiKeyService: ApiKeyService - def xa: Transactor[IO] -} diff --git a/backend/src/test/scala/com/softwaremill/bootzooka/passwordreset/PasswordResetApiTest.scala b/backend/src/test/scala/com/softwaremill/bootzooka/passwordreset/PasswordResetApiTest.scala index fa2d21999..f0ce68396 100644 --- a/backend/src/test/scala/com/softwaremill/bootzooka/passwordreset/PasswordResetApiTest.scala +++ b/backend/src/test/scala/com/softwaremill/bootzooka/passwordreset/PasswordResetApiTest.scala @@ -1,34 +1,16 @@ package com.softwaremill.bootzooka.passwordreset import cats.effect.IO -import com.softwaremill.bootzooka.MainModule -import com.softwaremill.bootzooka.config.Config import com.softwaremill.bootzooka.email.sender.DummyEmailSender -import com.softwaremill.bootzooka.infrastructure.Doobie._ import com.softwaremill.bootzooka.infrastructure.Json._ -import com.softwaremill.bootzooka.passwordreset.PasswordResetApi.{ - ForgotPassword_IN, - ForgotPassword_OUT, - PasswordReset_IN, - PasswordReset_OUT -} -import com.softwaremill.bootzooka.test.{BaseTest, Requests, TestConfig, TestEmbeddedPostgres} +import com.softwaremill.bootzooka.passwordreset.PasswordResetApi.{ForgotPassword_IN, ForgotPassword_OUT, PasswordReset_IN, PasswordReset_OUT} +import com.softwaremill.bootzooka.test.{TestDependencies, BaseTest, Requests} import org.http4s._ import org.http4s.syntax.all._ import org.scalatest.concurrent.Eventually -import sttp.client3.SttpBackend -import sttp.client3.asynchttpclient.fs2.AsyncHttpClientFs2Backend - -class PasswordResetApiTest extends BaseTest with TestEmbeddedPostgres with Eventually { - lazy val modules: MainModule = new MainModule { - override def xa: Transactor[IO] = currentDb.xa - - override lazy val baseSttpBackend: SttpBackend[IO, Any] = AsyncHttpClientFs2Backend.stub[IO] - override lazy val config: Config = TestConfig - } - - val requests = new Requests(modules) +class PasswordResetApiTest extends BaseTest with Eventually with TestDependencies { + lazy val requests = new Requests(dependencies.api) import requests._ "/passwordreset" should "reset the password" in { @@ -110,18 +92,18 @@ class PasswordResetApiTest extends BaseTest with TestEmbeddedPostgres with Event val request = Request[IO](method = POST, uri = uri"/passwordreset/forgot") .withEntity(ForgotPassword_IN(loginOrEmail)) - modules.httpApi.mainRoutes(request).unwrap + dependencies.api.mainRoutes(request).unwrap } def resetPassword(code: String, password: String): Response[IO] = { val request = Request[IO](method = POST, uri = uri"/passwordreset/reset") .withEntity(PasswordReset_IN(code, password)) - modules.httpApi.mainRoutes(request).unwrap + dependencies.api.mainRoutes(request).unwrap } def codeSentToEmail(email: String): String = { - modules.emailService.sendBatch().unwrap + dependencies.emailService.sendBatch().unwrap val emailData = DummyEmailSender .findSentEmail(email, "SoftwareMill Bootzooka password reset") @@ -132,7 +114,7 @@ class PasswordResetApiTest extends BaseTest with TestEmbeddedPostgres with Event } def codeWasNotSentToEmail(email: String): Unit = { - modules.emailService.sendBatch().unwrap + dependencies.emailService.sendBatch().unwrap val maybeEmail = DummyEmailSender.findSentEmail(email, "SoftwareMill Bootzooka password reset") maybeEmail match { diff --git a/backend/src/test/scala/com/softwaremill/bootzooka/test/HttpTestSupport.scala b/backend/src/test/scala/com/softwaremill/bootzooka/test/HttpTestSupport.scala index 0c74a392f..fcf5148b7 100644 --- a/backend/src/test/scala/com/softwaremill/bootzooka/test/HttpTestSupport.scala +++ b/backend/src/test/scala/com/softwaremill/bootzooka/test/HttpTestSupport.scala @@ -3,7 +3,6 @@ package com.softwaremill.bootzooka.test import cats.data.OptionT import cats.effect.kernel.Concurrent import cats.effect.{IO, Sync} -import com.softwaremill.bootzooka.MainModule import com.softwaremill.bootzooka.http.Error_OUT import com.softwaremill.bootzooka.infrastructure.Json._ import io.circe.{Decoder, Encoder} @@ -20,8 +19,6 @@ import scala.reflect.ClassTag trait HttpTestSupport extends Http4sDsl[IO] with Matchers { - val modules: MainModule - // in tests we are using the http4s client, hence we need http4s entity encoders/decoders to send/receive data implicit def entityEncoderFromCirce[F[_]: Sync, T: Encoder]: EntityEncoder[F, T] = { org.http4s.circe.jsonEncoderWithPrinterOf[F, T](noNullsPrinter) diff --git a/backend/src/test/scala/com/softwaremill/bootzooka/test/Requests.scala b/backend/src/test/scala/com/softwaremill/bootzooka/test/Requests.scala index afd55a60e..55c16d9ad 100644 --- a/backend/src/test/scala/com/softwaremill/bootzooka/test/Requests.scala +++ b/backend/src/test/scala/com/softwaremill/bootzooka/test/Requests.scala @@ -1,7 +1,7 @@ package com.softwaremill.bootzooka.test import cats.effect.IO -import com.softwaremill.bootzooka.MainModule +import com.softwaremill.bootzooka.http.HttpApi import com.softwaremill.bootzooka.infrastructure.Json._ import com.softwaremill.bootzooka.user.UserApi._ import org.http4s._ @@ -9,8 +9,7 @@ import org.http4s.syntax.all._ import scala.util.Random -class Requests(val modules: MainModule) extends HttpTestSupport { - +class Requests(httpApi: => HttpApi) extends HttpTestSupport { case class RegisteredUser(login: String, email: String, password: String, apiKey: String) private val random = new Random() @@ -22,7 +21,7 @@ class Requests(val modules: MainModule) extends HttpTestSupport { val request = Request[IO](method = POST, uri = uri"/user/register") .withEntity(Register_IN(login, email, password)) - modules.httpApi.mainRoutes(request).unwrap + httpApi.mainRoutes(request).unwrap } def newRegisteredUsed(): RegisteredUser = { @@ -35,26 +34,26 @@ class Requests(val modules: MainModule) extends HttpTestSupport { val request = Request[IO](method = POST, uri = uri"/user/login") .withEntity(Login_IN(loginOrEmail, password, apiKeyValidHours)) - modules.httpApi.mainRoutes(request).unwrap + httpApi.mainRoutes(request).unwrap } def getUser(apiKey: String): Response[IO] = { val request = Request[IO](method = GET, uri = uri"/user") - modules.httpApi.mainRoutes(authorizedRequest(apiKey, request)).unwrap + httpApi.mainRoutes(authorizedRequest(apiKey, request)).unwrap } def changePassword(apiKey: String, password: String, newPassword: String): Response[IO] = { val request = Request[IO](method = POST, uri = uri"/user/changepassword") .withEntity(ChangePassword_IN(password, newPassword)) - modules.httpApi.mainRoutes(authorizedRequest(apiKey, request)).unwrap + httpApi.mainRoutes(authorizedRequest(apiKey, request)).unwrap } def updateUser(apiKey: String, login: String, email: String): Response[IO] = { val request = Request[IO](method = POST, uri = uri"/user") .withEntity(UpdateUser_IN(login, email)) - modules.httpApi.mainRoutes(authorizedRequest(apiKey, request)).unwrap + httpApi.mainRoutes(authorizedRequest(apiKey, request)).unwrap } } diff --git a/backend/src/test/scala/com/softwaremill/bootzooka/test/TestDependencies.scala b/backend/src/test/scala/com/softwaremill/bootzooka/test/TestDependencies.scala new file mode 100644 index 000000000..fc4c19f73 --- /dev/null +++ b/backend/src/test/scala/com/softwaremill/bootzooka/test/TestDependencies.scala @@ -0,0 +1,25 @@ +package com.softwaremill.bootzooka.test + +import cats.effect.{IO, Resource} +import com.softwaremill.bootzooka.Dependencies +import org.scalatest.{BeforeAndAfterAll, Suite} +import sttp.client3.asynchttpclient.fs2.AsyncHttpClientFs2Backend + +trait TestDependencies extends BeforeAndAfterAll with TestEmbeddedPostgres { self: Suite with BaseTest => + var dependencies: Dependencies = _ + + override protected def beforeAll(): Unit = { + super.beforeAll() + + dependencies = { + import cats.effect.unsafe.implicits.global + + Dependencies.wire( + config = TestConfig, + sttpBackend = Resource.pure(AsyncHttpClientFs2Backend.stub[IO]), + xa = Resource.pure(currentDb.xa), + clock = testClock + ).allocated.unsafeRunSync()._1 + } + } +} diff --git a/backend/src/test/scala/com/softwaremill/bootzooka/test/package.scala b/backend/src/test/scala/com/softwaremill/bootzooka/test/package.scala index 287fcd31c..927bfe510 100644 --- a/backend/src/test/scala/com/softwaremill/bootzooka/test/package.scala +++ b/backend/src/test/scala/com/softwaremill/bootzooka/test/package.scala @@ -1,11 +1,11 @@ package com.softwaremill.bootzooka -import com.softwaremill.bootzooka.config.{Config, ConfigModule} +import com.softwaremill.bootzooka.config.Config import com.softwaremill.quicklens._ import scala.concurrent.duration._ package object test { - val DefaultConfig: Config = new ConfigModule {}.config + val DefaultConfig: Config = Config.read val TestConfig: Config = DefaultConfig.modify(_.email.emailSendInterval).setTo(100.milliseconds) } diff --git a/backend/src/test/scala/com/softwaremill/bootzooka/user/UserApiTest.scala b/backend/src/test/scala/com/softwaremill/bootzooka/user/UserApiTest.scala index 7d48adf4c..251d9e96d 100644 --- a/backend/src/test/scala/com/softwaremill/bootzooka/user/UserApiTest.scala +++ b/backend/src/test/scala/com/softwaremill/bootzooka/user/UserApiTest.scala @@ -1,31 +1,16 @@ package com.softwaremill.bootzooka.user -import cats.effect.IO -import com.softwaremill.bootzooka.MainModule -import com.softwaremill.bootzooka.config.Config import com.softwaremill.bootzooka.email.sender.DummyEmailSender -import com.softwaremill.bootzooka.infrastructure.Doobie._ import com.softwaremill.bootzooka.infrastructure.Json._ -import com.softwaremill.bootzooka.test.{BaseTest, Requests, TestConfig, TestEmbeddedPostgres} +import com.softwaremill.bootzooka.test.{BaseTest, TestDependencies, Requests} import com.softwaremill.bootzooka.user.UserApi._ -import com.softwaremill.bootzooka.util.Clock import org.http4s.Status import org.scalatest.concurrent.Eventually -import sttp.client3.SttpBackend -import sttp.client3.asynchttpclient.fs2.AsyncHttpClientFs2Backend import scala.concurrent.duration._ -class UserApiTest extends BaseTest with TestEmbeddedPostgres with Eventually { - - lazy val modules: MainModule = new MainModule { - override def xa: Transactor[IO] = currentDb.xa - override lazy val baseSttpBackend: SttpBackend[IO, Any] = AsyncHttpClientFs2Backend.stub[IO] - override lazy val config: Config = TestConfig - override lazy val clock: Clock = testClock - } - - val requests = new Requests(modules) +class UserApiTest extends BaseTest with Eventually with TestDependencies { + lazy val requests = new Requests(dependencies.api) import requests._ "/user/register" should "register" in { @@ -94,7 +79,7 @@ class UserApiTest extends BaseTest with TestEmbeddedPostgres with Eventually { val RegisteredUser(login, email, _, _) = newRegisteredUsed() // then - modules.emailService.sendBatch().unwrap + dependencies.emailService.sendBatch().unwrap DummyEmailSender.findSentEmail(email, s"registration confirmation for user $login").isDefined shouldBe true } diff --git a/build.sbt b/build.sbt index 87550c73d..35e74f941 100644 --- a/build.sbt +++ b/build.sbt @@ -17,6 +17,7 @@ val tsecVersion = "0.4.0" val sttpVersion = "3.5.0" val prometheusVersion = "0.15.0" val tapirVersion = "0.20.0-M9" +val macwireVersion = "2.5.6" val dbDependencies = Seq( "org.tpolecat" %% "doobie-core" % doobieVersion, @@ -81,6 +82,10 @@ val emailDependencies = Seq( ) val scalatest = "org.scalatest" %% "scalatest" % "3.2.11" % Test +val macwireDependencies = Seq( + "com.softwaremill.macwire" %% "macrosautocats" % macwireVersion +).map(_ % Provided) + val unitTestingStack = Seq(scalatest) val embeddedPostgres = "com.opentable.components" % "otj-pg-embedded" % "1.0.0" % Test @@ -190,7 +195,7 @@ lazy val rootProject = (project in file(".")) lazy val backend: Project = (project in file("backend")) .settings( - libraryDependencies ++= dbDependencies ++ httpDependencies ++ jsonDependencies ++ apiDocsDependencies ++ monitoringDependencies ++ dbTestingStack ++ securityDependencies ++ emailDependencies, + libraryDependencies ++= dbDependencies ++ httpDependencies ++ jsonDependencies ++ apiDocsDependencies ++ monitoringDependencies ++ dbTestingStack ++ securityDependencies ++ emailDependencies ++ macwireDependencies, Compile / mainClass := Some("com.softwaremill.bootzooka.Main") ) .enablePlugins(BuildInfoPlugin)