From 6bf38aa94b03f56eb83c4d4e611452c617c43738 Mon Sep 17 00:00:00 2001 From: yoshinorin Date: Sat, 20 Jul 2024 17:03:24 +0900 Subject: [PATCH] perf(db): use `HikariCP` instead of `JDBC DriverManager` for manage connection --- project/Dependencies.scala | 1 + .../net/yoshinorin/qualtet/BootStrap.scala | 28 ++++---- .../net/yoshinorin/qualtet/Modules.scala | 43 ++++++------ .../db/doobie/DoobieTransactor.scala | 26 +++++++- .../qualtet/tasks/CreateAuthor.scala | 55 +++++++--------- .../qualtet/auth/AuthServiceSpec.scala | 10 +-- .../net/yoshinorin/qualtet/auth/JwtSpec.scala | 15 +++-- .../qualtet/cache/CacheServiceSpec.scala | 2 +- .../domains/archives/ArchiveServiceSpec.scala | 1 - .../domains/articles/ArticleServiceSpec.scala | 1 - .../ContentSerializingServiceSpec.scala | 6 -- .../ContentTaggingServiceSpec.scala | 5 +- .../domains/contents/ContentServiceSpec.scala | 1 - .../domains/search/SearchServiceSpec.scala | 1 - .../domains/series/SeriesServiceSpec.scala | 6 -- .../qualtet/domains/tags/TagServiceSpec.scala | 5 +- .../yoshinorin/qualtet/fixture/Fixture.scala | 66 ++++++++++++------- .../http/routes/v1/ArticleRouteSpec.scala | 3 +- .../http/routes/v1/AuthRouteSpec.scala | 3 +- .../http/routes/v1/AuthorRouteSpec.scala | 3 +- .../http/routes/v1/CacheRouteSpec.scala | 3 +- .../http/routes/v1/ContentRouteSpec.scala | 1 - .../http/routes/v1/SeriesRouteSpec.scala | 1 - .../qualtet/http/routes/v1/TagRouteSpec.scala | 1 - .../infrastructure/db/MigratorSpec.scala | 9 +-- .../qualtet/tasks/CreateAuthorSpec.scala | 9 ++- 26 files changed, 157 insertions(+), 148 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 79fdb19b..df0a664b 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -24,6 +24,7 @@ object Dependencies { "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion % "test-internal", "org.mariadb.jdbc" % "mariadb-java-client" % "3.4.0", "org.tpolecat" %% "doobie-core" % doobieVersion, + "org.tpolecat" %% "doobie-hikari" % doobieVersion, // "org.tpolecat" %% "doobie-scalatest" % doobieVersion % Test, "org.wvlet.airframe" %% "airframe-ulid" % "24.7.0", "com.github.ben-manes.caffeine" % "caffeine" % "3.1.8", diff --git a/src/main/scala/net/yoshinorin/qualtet/BootStrap.scala b/src/main/scala/net/yoshinorin/qualtet/BootStrap.scala index 04289897..9a78db80 100644 --- a/src/main/scala/net/yoshinorin/qualtet/BootStrap.scala +++ b/src/main/scala/net/yoshinorin/qualtet/BootStrap.scala @@ -5,6 +5,7 @@ import cats.effect.{ExitCode, IO, IOApp} import cats.effect.kernel.Resource import org.typelevel.log4cats.SelfAwareStructuredLogger import org.typelevel.log4cats.{LoggerFactory => Log4CatsLoggerFactory} +import org.typelevel.log4cats.slf4j.{Slf4jFactory => Log4CatsSlf4jFactory} import org.http4s.* import org.http4s.ember.server.EmberServerBuilder import org.http4s.server.Server @@ -14,7 +15,7 @@ import scala.concurrent.duration._ object BootStrap extends IOApp { - import net.yoshinorin.qualtet.Modules.log4catsLogger + given log4catsLogger: Log4CatsLoggerFactory[IO] = Log4CatsSlf4jFactory.create[IO] val logger: SelfAwareStructuredLogger[IO] = Log4CatsLoggerFactory[IO].getLoggerFromClass(this.getClass) @@ -33,17 +34,20 @@ object BootStrap extends IOApp { val host = Ipv4Address.fromString(Modules.config.http.host).getOrElse(ipv4"127.0.0.1") val port = Port.fromInt(Modules.config.http.port).getOrElse(port"9001") - (for { - _ <- logger.info(ApplicationInfo.asJson) - _ <- IO(Modules.migrator.migrate(Modules.contentTypeService)) - routes <- Modules.router.withCors.map[Kleisli[IO, Request[IO], Response[IO]]](x => x.orNotFound) - httpApp <- IO(new HttpAppBuilder(routes).build) - server <- IO( - server(host, port, httpApp) - .use(_ => IO.never) - .as(ExitCode.Success) - ) - } yield server).flatMap(identity) + Modules.transactorResource.use { tx => + val modules = new Modules(tx) + (for { + _ <- logger.info(ApplicationInfo.asJson) + _ <- IO(modules.migrator.migrate(modules.contentTypeService)) + routes <- modules.router.withCors.map[Kleisli[IO, Request[IO], Response[IO]]](x => x.orNotFound) + httpApp <- IO(new HttpAppBuilder(routes).build) + server <- IO( + server(host, port, httpApp) + .use(_ => IO.never) + .as(ExitCode.Success) + ) + } yield server).flatMap(identity) + } } } diff --git a/src/main/scala/net/yoshinorin/qualtet/Modules.scala b/src/main/scala/net/yoshinorin/qualtet/Modules.scala index 67a277dc..3ef62528 100644 --- a/src/main/scala/net/yoshinorin/qualtet/Modules.scala +++ b/src/main/scala/net/yoshinorin/qualtet/Modules.scala @@ -2,6 +2,7 @@ package net.yoshinorin.qualtet import cats.effect.IO import doobie.ConnectionIO +import doobie.util.transactor.Transactor import doobie.util.transactor.Transactor.Aux import org.typelevel.log4cats.{LoggerFactory => Log4CatsLoggerFactory} import org.typelevel.log4cats.slf4j.{Slf4jFactory => Log4CatsSlf4jFactory} @@ -45,21 +46,25 @@ import net.yoshinorin.qualtet.http.routes.v1.{ TagRoute => TagRouteV1 } import net.yoshinorin.qualtet.infrastructure.db.Migrator -import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieExecuter +import net.yoshinorin.qualtet.infrastructure.db.doobie.{DoobieExecuter, DoobieTransactor} import pdi.jwt.JwtAlgorithm import java.security.SecureRandom import java.util.concurrent.TimeUnit -import doobie.util.transactor.Transactor -import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieTransactor object Modules { - val config = ApplicationConfig.load val doobieTransactor: DoobieTransactor[Aux] = summon[DoobieTransactor[Aux]] + val transactorResource = doobieTransactor.make(config.db) +} + +class Modules(tx: Transactor[IO]) { + + val config = ApplicationConfig.load given log4catsLogger: Log4CatsLoggerFactory[IO] = Log4CatsSlf4jFactory.create[IO] - given dbContext: DoobieExecuter = new DoobieExecuter(doobieTransactor.make(config.db)) + given dbContext: DoobieExecuter = new DoobieExecuter(tx) + val migrator: Migrator = new Migrator(config.db) // NOTE: for generate JWT. They are reset when re-boot application. @@ -137,23 +142,23 @@ object Modules { feedService ) - val authProvider = new AuthProvider(Modules.authService) + val authProvider = new AuthProvider(authService) val corsProvider = new CorsProvider(Modules.config.cors) - val archiveRouteV1 = new ArchiveRouteV1(Modules.archiveService) - val articleRouteV1 = new ArticleRouteV1(Modules.articleService) - val authorRouteV1 = new AuthorRouteV1(Modules.authorService) - val authRouteV1 = new AuthRouteV1(Modules.authService) - val cacheRouteV1 = new CacheRouteV1(authProvider, Modules.cacheService) - val contentTypeRouteV1 = new ContentTypeRouteV1(Modules.contentTypeService) - val contentRouteV1 = new ContentRouteV1(authProvider, Modules.contentService) - val feedRouteV1 = new FeedRouteV1(Modules.feedService) + val archiveRouteV1 = new ArchiveRouteV1(archiveService) + val articleRouteV1 = new ArticleRouteV1(articleService) + val authorRouteV1 = new AuthorRouteV1(authorService) + val authRouteV1 = new AuthRouteV1(authService) + val cacheRouteV1 = new CacheRouteV1(authProvider, cacheService) + val contentTypeRouteV1 = new ContentTypeRouteV1(contentTypeService) + val contentRouteV1 = new ContentRouteV1(authProvider, contentService) + val feedRouteV1 = new FeedRouteV1(feedService) val homeRoute: HomeRoute = new HomeRoute() - val searchRouteV1 = new SearchRouteV1(Modules.searchService) - val seriesRouteV1 = new SeriesRouteV1(authProvider, Modules.seriesService) - val sitemapRouteV1 = new SitemapRouteV1(Modules.sitemapService) - val systemRouteV1 = new SystemRouteV1(Modules.config.http.endpoints.system) - val tagRouteV1 = new TagRouteV1(authProvider, Modules.tagService, Modules.articleService) + val searchRouteV1 = new SearchRouteV1(searchService) + val seriesRouteV1 = new SeriesRouteV1(authProvider, seriesService) + val sitemapRouteV1 = new SitemapRouteV1(sitemapService) + val systemRouteV1 = new SystemRouteV1(config.http.endpoints.system) + val tagRouteV1 = new TagRouteV1(authProvider, tagService, articleService) val router = new net.yoshinorin.qualtet.http.Router( corsProvider, diff --git a/src/main/scala/net/yoshinorin/qualtet/infrastructure/db/doobie/DoobieTransactor.scala b/src/main/scala/net/yoshinorin/qualtet/infrastructure/db/doobie/DoobieTransactor.scala index 1c81e601..226b3ffb 100644 --- a/src/main/scala/net/yoshinorin/qualtet/infrastructure/db/doobie/DoobieTransactor.scala +++ b/src/main/scala/net/yoshinorin/qualtet/infrastructure/db/doobie/DoobieTransactor.scala @@ -1,16 +1,19 @@ package net.yoshinorin.qualtet.infrastructure.db.doobie -import cats.effect.IO +import cats.effect.{IO, Resource} import doobie.* import doobie.util.transactor.Transactor.Aux +import doobie.hikari.* +import com.zaxxer.hikari.HikariConfig import net.yoshinorin.qualtet.config.DBConfig trait DoobieTransactor[F[G[_], _]] { - def make(config: DBConfig): Transactor[IO] + def make(config: DBConfig): Resource[IO, HikariTransactor[IO]] } object DoobieTransactor { + /* given DoobieTransactor: DoobieTransactor[Aux] = { new DoobieTransactor[Aux] { override def make(config: DBConfig): Transactor[IO] = { @@ -24,5 +27,24 @@ object DoobieTransactor { } } } + */ + + given DoobieTransactor: DoobieTransactor[Aux] = { + new DoobieTransactor[Aux] { + override def make(config: DBConfig): Resource[IO, HikariTransactor[IO]] = { + for { + hikariConfig <- Resource.pure { + val hConfig = new HikariConfig() + hConfig.setDriverClassName("org.mariadb.jdbc.Driver") + hConfig.setJdbcUrl(config.url.toString()) + hConfig.setUsername(config.user.toString()) + hConfig.setPassword(config.password.toString()) + hConfig + } + xa <- HikariTransactor.fromHikariConfig[IO](hikariConfig) + } yield xa + } + } + } } diff --git a/src/main/scala/net/yoshinorin/qualtet/tasks/CreateAuthor.scala b/src/main/scala/net/yoshinorin/qualtet/tasks/CreateAuthor.scala index c56b6f7b..0e90771f 100644 --- a/src/main/scala/net/yoshinorin/qualtet/tasks/CreateAuthor.scala +++ b/src/main/scala/net/yoshinorin/qualtet/tasks/CreateAuthor.scala @@ -1,53 +1,46 @@ package net.yoshinorin.qualtet.tasks -import cats.effect.IO import cats.implicits.catsSyntaxEq -import doobie.ConnectionIO -import doobie.util.transactor.Transactor.Aux +import cats.effect.{ExitCode, IO, IOApp} import org.slf4j.LoggerFactory import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder -import net.yoshinorin.qualtet.domains.authors.{Author, AuthorDisplayName, AuthorName, AuthorRepository, AuthorService, BCryptPassword} -import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieExecuter -import net.yoshinorin.qualtet.Modules.* +import net.yoshinorin.qualtet.domains.authors.{Author, AuthorDisplayName, AuthorName, BCryptPassword} +import net.yoshinorin.qualtet.Modules import net.yoshinorin.qualtet.syntax.* import net.yoshinorin.qualtet.domains.authors.ResponseAuthor import cats.effect.unsafe.implicits.global -import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieTransactor -object CreateAuthor { +object CreateAuthor extends IOApp { private val logger = LoggerFactory.getLogger(this.getClass) - val doobieTransactor: DoobieTransactor[Aux] = summon[DoobieTransactor[Aux]] - - given dbContext: DoobieExecuter = new DoobieExecuter(doobieTransactor.make(config.db)) - val authorRepository: AuthorRepository[ConnectionIO] = summon[AuthorRepository[ConnectionIO]] - val authorService = new AuthorService(authorRepository) - - def main(args: Array[String]): Unit = { + def run(args: List[String]): IO[ExitCode] = { if (args.length =!= 3) { throw new IllegalArgumentException("args must be three length.") } // https://docs.spring.io/spring-security/site/docs/current/reference/html5/#authentication-password-storage-bcrypt val bcryptPasswordEncoder = new BCryptPasswordEncoder() - (for { - author <- authorService.create( - Author(name = AuthorName(args(0)), displayName = AuthorDisplayName(args(1)), password = BCryptPassword(bcryptPasswordEncoder.encode(args(2)))) - ) - } yield author) - .handleErrorWith { e => - IO.pure(e) - } - .unsafeRunSync() match { - case a: ResponseAuthor => - logger.info(s"author created: ${a.asJson}") - logger.info("shutting down...") - case e: Exception => - logger.error(e.getMessage) - case _ => - logger.error("unknown error") + + Modules.transactorResource.use { tx => + val modules = new Modules(tx) + (for { + author <- modules.authorService.create( + Author(name = AuthorName(args(0)), displayName = AuthorDisplayName(args(1)), password = BCryptPassword(bcryptPasswordEncoder.encode(args(2)))) + ) + _ <- IO(logger.info(s"author created: ${author.asJson}")) + _ <- IO(println(s"author created: ${author.asJson}")) + } yield author) + .handleErrorWith { e => + e match + case e: Exception => + IO(logger.error(e.getMessage)) + case _ => + IO(logger.error("unknown error")) + } + .unsafeRunSync() // FIXME + IO(ExitCode.Success) } } } diff --git a/src/test/scala/net/yoshinorin/qualtet/auth/AuthServiceSpec.scala b/src/test/scala/net/yoshinorin/qualtet/auth/AuthServiceSpec.scala index 6045b4ea..d61766c7 100644 --- a/src/test/scala/net/yoshinorin/qualtet/auth/AuthServiceSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/auth/AuthServiceSpec.scala @@ -3,27 +3,23 @@ package net.yoshinorin.qualtet.auth import cats.effect.IO import net.yoshinorin.qualtet.domains.authors.ResponseAuthor import net.yoshinorin.qualtet.domains.errors.{NotFound, Unauthorized} -import net.yoshinorin.qualtet.Modules.* import net.yoshinorin.qualtet.Modules import net.yoshinorin.qualtet.fixture.Fixture.* -import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieExecuter import org.scalatest.wordspec.AnyWordSpec import cats.effect.unsafe.implicits.global // testOnly net.yoshinorin.qualtet.auth.AuthServiceSpec class AuthServiceSpec extends AnyWordSpec { - val tx = doobieTransactor.make(Modules.config.db) - given dbContext: DoobieExecuter = new DoobieExecuter(tx) - + val mod = Modules(fixtureTx) val a: ResponseAuthor = authorService.findByName(author.name).unsafeRunSync().get val a2: ResponseAuthor = authorService.findByName(author2.name).unsafeRunSync().get "AuthService" should { "generate token" in { - val token = authService.generateToken(RequestToken(a.id, "pass")).unsafeRunSync().token - assert(jwtInstance.decode[IO](token).unsafeRunSync().isRight) + val token = mod.authService.generateToken(RequestToken(a.id, "pass")).unsafeRunSync().token + assert(mod.jwtInstance.decode[IO](token).unsafeRunSync().isRight) } "find an author from JWT string" in { diff --git a/src/test/scala/net/yoshinorin/qualtet/auth/JwtSpec.scala b/src/test/scala/net/yoshinorin/qualtet/auth/JwtSpec.scala index 20d8b153..96d9d57e 100644 --- a/src/test/scala/net/yoshinorin/qualtet/auth/JwtSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/auth/JwtSpec.scala @@ -3,13 +3,14 @@ package net.yoshinorin.qualtet.auth import cats.effect.IO import net.yoshinorin.qualtet.domains.authors.{Author, AuthorDisplayName, AuthorId, AuthorName} import net.yoshinorin.qualtet.domains.errors.Unauthorized -import net.yoshinorin.qualtet.Modules.* -import net.yoshinorin.qualtet.fixture.Fixture.validBCryptPassword +import net.yoshinorin.qualtet.fixture.Fixture.{fixtureTx, validBCryptPassword} +import net.yoshinorin.qualtet.Modules +import net.yoshinorin.qualtet.syntax.* import net.yoshinorin.qualtet.validator.Validator + import org.scalatest.wordspec.AnyWordSpec import pdi.jwt.exceptions.JwtValidationException import wvlet.airframe.ulid.ULID -import net.yoshinorin.qualtet.syntax.* import cats.effect.unsafe.implicits.global @@ -18,6 +19,8 @@ import java.time.Instant // testOnly net.yoshinorin.qualtet.auth.JwtSpec class JwtSpec extends AnyWordSpec { + val mod = Modules(fixtureTx) + val config = Modules.config val jc: JwtClaim = JwtClaim( iss = config.jwt.iss, aud = config.jwt.aud, @@ -30,7 +33,7 @@ class JwtSpec extends AnyWordSpec { "Jwt" should { "encode and decode" in { val id = ULID.newULIDString.toLower - val jwtString = jwtInstance.encode( + val jwtString = mod.jwtInstance.encode( Author( id = AuthorId(id), name = AuthorName("Jhon"), @@ -39,7 +42,7 @@ class JwtSpec extends AnyWordSpec { ) ) - jwtInstance.decode[IO](jwtString).unsafeRunSync() match { + mod.jwtInstance.decode[IO](jwtString).unsafeRunSync() match { case Right(j) => { assert(j.iss === config.jwt.iss) assert(j.aud === config.jwt.aud) @@ -55,7 +58,7 @@ class JwtSpec extends AnyWordSpec { val ioInstance = implicitly[cats.Monad[IO]] "throw exception caused by not signed JSON" in { - val maybeJwtClaims = jwtInstance.decode[IO]( + val maybeJwtClaims = mod.jwtInstance.decode[IO]( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" ) assert(maybeJwtClaims.unsafeRunSync().left.getOrElse("").isInstanceOf[JwtValidationException]) diff --git a/src/test/scala/net/yoshinorin/qualtet/cache/CacheServiceSpec.scala b/src/test/scala/net/yoshinorin/qualtet/cache/CacheServiceSpec.scala index d4628ec4..ef0c9878 100644 --- a/src/test/scala/net/yoshinorin/qualtet/cache/CacheServiceSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/cache/CacheServiceSpec.scala @@ -1,6 +1,6 @@ package net.yoshinorin.qualtet.cache -import net.yoshinorin.qualtet.Modules.* +import net.yoshinorin.qualtet.fixture.Fixture.* import org.scalatest.wordspec.AnyWordSpec import cats.effect.unsafe.implicits.global diff --git a/src/test/scala/net/yoshinorin/qualtet/domains/archives/ArchiveServiceSpec.scala b/src/test/scala/net/yoshinorin/qualtet/domains/archives/ArchiveServiceSpec.scala index 776b082f..53ad4b46 100644 --- a/src/test/scala/net/yoshinorin/qualtet/domains/archives/ArchiveServiceSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/domains/archives/ArchiveServiceSpec.scala @@ -1,6 +1,5 @@ package net.yoshinorin.qualtet.domains.archives -import net.yoshinorin.qualtet.Modules.* import net.yoshinorin.qualtet.fixture.Fixture.* import org.scalatest.wordspec.AnyWordSpec import org.scalatest.BeforeAndAfterAll diff --git a/src/test/scala/net/yoshinorin/qualtet/domains/articles/ArticleServiceSpec.scala b/src/test/scala/net/yoshinorin/qualtet/domains/articles/ArticleServiceSpec.scala index d4eacc9b..83060245 100644 --- a/src/test/scala/net/yoshinorin/qualtet/domains/articles/ArticleServiceSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/domains/articles/ArticleServiceSpec.scala @@ -3,7 +3,6 @@ package net.yoshinorin.qualtet.domains.articles import net.yoshinorin.qualtet.domains.contents.{Path, RequestContent} import net.yoshinorin.qualtet.domains.robots.Attributes import net.yoshinorin.qualtet.domains.tags.TagName -import net.yoshinorin.qualtet.Modules.* import net.yoshinorin.qualtet.fixture.Fixture.* import net.yoshinorin.qualtet.http.ArticlesQueryParameter import org.scalatest.wordspec.AnyWordSpec diff --git a/src/test/scala/net/yoshinorin/qualtet/domains/contentSerializing/ContentSerializingServiceSpec.scala b/src/test/scala/net/yoshinorin/qualtet/domains/contentSerializing/ContentSerializingServiceSpec.scala index 96954141..d0dc769f 100644 --- a/src/test/scala/net/yoshinorin/qualtet/domains/contentSerializing/ContentSerializingServiceSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/domains/contentSerializing/ContentSerializingServiceSpec.scala @@ -4,9 +4,6 @@ import net.yoshinorin.qualtet.domains.contents.{Path, RequestContent} import net.yoshinorin.qualtet.domains.robots.Attributes import net.yoshinorin.qualtet.domains.series.* import net.yoshinorin.qualtet.fixture.Fixture.* -import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieExecuter -import net.yoshinorin.qualtet.Modules -import net.yoshinorin.qualtet.Modules.* import org.scalatest.wordspec.AnyWordSpec import org.scalatest.BeforeAndAfterAll @@ -16,9 +13,6 @@ import org.scalatest.Ignore @Ignore // TODO: write testcode when implement delete feature class ContentSerializingServiceSpec extends AnyWordSpec with BeforeAndAfterAll { - val tx = doobieTransactor.make(Modules.config.db) - given dbContext: DoobieExecuter = new DoobieExecuter(tx) - val requestSeries: RequestSeries = RequestSeries( title = "Content Serializing Service Spec", name = SeriesName("content-serializing-service-spec"), diff --git a/src/test/scala/net/yoshinorin/qualtet/domains/contentTaggings/ContentTaggingServiceSpec.scala b/src/test/scala/net/yoshinorin/qualtet/domains/contentTaggings/ContentTaggingServiceSpec.scala index 630f15a8..8c246588 100644 --- a/src/test/scala/net/yoshinorin/qualtet/domains/contentTaggings/ContentTaggingServiceSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/domains/contentTaggings/ContentTaggingServiceSpec.scala @@ -4,8 +4,6 @@ import net.yoshinorin.qualtet.domains.contents.{Path, RequestContent} import net.yoshinorin.qualtet.domains.robots.Attributes import net.yoshinorin.qualtet.fixture.Fixture.* import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieExecuter -import net.yoshinorin.qualtet.Modules -import net.yoshinorin.qualtet.Modules.* import org.scalatest.wordspec.AnyWordSpec import org.scalatest.BeforeAndAfterAll @@ -14,8 +12,7 @@ import cats.effect.unsafe.implicits.global // testOnly net.yoshinorin.qualtet.domains.contentTaggings.ContentTaggingServiceSpec class ContentTaggingServiceSpec extends AnyWordSpec with BeforeAndAfterAll { - val tx = doobieTransactor.make(Modules.config.db) - given dbContext: DoobieExecuter = new DoobieExecuter(tx) + given dbContext: DoobieExecuter = new DoobieExecuter(fixtureTx) val requestContents: List[RequestContent] = { List(1, 2) diff --git a/src/test/scala/net/yoshinorin/qualtet/domains/contents/ContentServiceSpec.scala b/src/test/scala/net/yoshinorin/qualtet/domains/contents/ContentServiceSpec.scala index 145c63a8..b3c82280 100644 --- a/src/test/scala/net/yoshinorin/qualtet/domains/contents/ContentServiceSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/domains/contents/ContentServiceSpec.scala @@ -4,7 +4,6 @@ import net.yoshinorin.qualtet.domains.authors.AuthorName import net.yoshinorin.qualtet.domains.errors.{NotFound, UnprocessableEntity} import net.yoshinorin.qualtet.domains.series.SeriesName import net.yoshinorin.qualtet.domains.robots.Attributes -import net.yoshinorin.qualtet.Modules.* import net.yoshinorin.qualtet.fixture.Fixture.* import org.scalatest.wordspec.AnyWordSpec import net.yoshinorin.qualtet.domains.externalResources.ExternalResources diff --git a/src/test/scala/net/yoshinorin/qualtet/domains/search/SearchServiceSpec.scala b/src/test/scala/net/yoshinorin/qualtet/domains/search/SearchServiceSpec.scala index ac728ac1..ac90b84a 100644 --- a/src/test/scala/net/yoshinorin/qualtet/domains/search/SearchServiceSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/domains/search/SearchServiceSpec.scala @@ -3,7 +3,6 @@ package net.yoshinorin.qualtet.domains.search import net.yoshinorin.qualtet.domains.contents.{Path, RequestContent} import net.yoshinorin.qualtet.domains.robots.Attributes import net.yoshinorin.qualtet.fixture.Fixture.* -import net.yoshinorin.qualtet.Modules.* import net.yoshinorin.qualtet.http.ProblemDetailsError import org.scalatest.wordspec.AnyWordSpec import org.scalatest.BeforeAndAfterAll diff --git a/src/test/scala/net/yoshinorin/qualtet/domains/series/SeriesServiceSpec.scala b/src/test/scala/net/yoshinorin/qualtet/domains/series/SeriesServiceSpec.scala index 23d4a1ea..69b2217e 100644 --- a/src/test/scala/net/yoshinorin/qualtet/domains/series/SeriesServiceSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/domains/series/SeriesServiceSpec.scala @@ -3,9 +3,6 @@ package net.yoshinorin.qualtet.domains.tags import net.yoshinorin.qualtet.domains.contents.Path import net.yoshinorin.qualtet.domains.series.{RequestSeries, SeriesName} import net.yoshinorin.qualtet.fixture.Fixture.* -import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieExecuter -import net.yoshinorin.qualtet.Modules -import net.yoshinorin.qualtet.Modules.* import org.scalatest.wordspec.AnyWordSpec import org.scalatest.BeforeAndAfterAll @@ -14,9 +11,6 @@ import cats.effect.unsafe.implicits.global // testOnly net.yoshinorin.qualtet.domains.SeriesServiceSpec class SeriesServiceSpec extends AnyWordSpec with BeforeAndAfterAll { - val tx = doobieTransactor.make(Modules.config.db) - given dbContext: DoobieExecuter = new DoobieExecuter(tx) - val requestSeries: List[RequestSeries] = List( RequestSeries( title = "Series Service Spec", diff --git a/src/test/scala/net/yoshinorin/qualtet/domains/tags/TagServiceSpec.scala b/src/test/scala/net/yoshinorin/qualtet/domains/tags/TagServiceSpec.scala index 3d6a5c68..32c06d6d 100644 --- a/src/test/scala/net/yoshinorin/qualtet/domains/tags/TagServiceSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/domains/tags/TagServiceSpec.scala @@ -4,8 +4,6 @@ import net.yoshinorin.qualtet.domains.contents.Path import net.yoshinorin.qualtet.domains.errors.NotFound import net.yoshinorin.qualtet.fixture.Fixture.* import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieExecuter -import net.yoshinorin.qualtet.Modules -import net.yoshinorin.qualtet.Modules.* import org.scalatest.wordspec.AnyWordSpec import org.scalatest.BeforeAndAfterAll @@ -14,8 +12,7 @@ import cats.effect.unsafe.implicits.global // testOnly net.yoshinorin.qualtet.domains.TagServiceSpec class TagServiceSpec extends AnyWordSpec with BeforeAndAfterAll { - val tx = doobieTransactor.make(Modules.config.db) - given dbContext: DoobieExecuter = new DoobieExecuter(tx) + given dbContext: DoobieExecuter = new DoobieExecuter(fixtureTx) val requestContents = makeRequestContents(10, "tagService") diff --git a/src/test/scala/net/yoshinorin/qualtet/fixture/Fixture.scala b/src/test/scala/net/yoshinorin/qualtet/fixture/Fixture.scala index f9b87564..bfa600cb 100644 --- a/src/test/scala/net/yoshinorin/qualtet/fixture/Fixture.scala +++ b/src/test/scala/net/yoshinorin/qualtet/fixture/Fixture.scala @@ -3,6 +3,7 @@ package net.yoshinorin.qualtet.fixture import cats.Monad import cats.effect.IO import doobie.ConnectionIO +import doobie.util.transactor.Transactor import org.http4s.Uri import org.http4s.Response import org.typelevel.log4cats.{LoggerFactory => Log4CatsLoggerFactory} @@ -56,15 +57,34 @@ object Fixture { val p: String = Modules.config.http.port.toString() val host = Uri.unsafeFromString(s"http://${h}:${p}") - val tx = Modules.doobieTransactor.make(Modules.config.db) - given dbContext: DoobieExecuter = new DoobieExecuter(tx) + val fixtureTx = Transactor.fromDriverManager[IO]( + driver = "org.mariadb.jdbc.Driver", + url = Modules.config.db.url, + user = Modules.config.db.user, + password = Modules.config.db.password, + logHandler = None + ) + private val modules = Modules(fixtureTx) given log4catsLogger: Log4CatsLoggerFactory[IO] = Log4CatsSlf4jFactory.create[IO] + given dbContext: DoobieExecuter = new DoobieExecuter(fixtureTx) + + val migrator = modules.migrator + val articleService = modules.articleService + val archiveService = modules.archiveService + val authorService = modules.authorService + val authService = modules.authService + val cacheService = modules.cacheService + val contentService = modules.contentService + val contentTaggingService = modules.contentTaggingService + val tagService = modules.tagService + val seriesService = modules.seriesService + val searchService = modules.searchService // TODO: from config for cache options val contentTypeCaffeinCache: CaffeineCache[String, ContentType] = Caffeine.newBuilder().expireAfterAccess(5, TimeUnit.SECONDS).build[String, ContentType] val contentTypeCache = new CacheModule[String, ContentType](contentTypeCaffeinCache) - val contentTypeService = new ContentTypeService(Modules.contentTypeRepository, contentTypeCache) + val contentTypeService = new ContentTypeService(modules.contentTypeRepository, contentTypeCache) val sitemapRepository: SitemapsRepository[ConnectionIO] = summon[SitemapsRepository[ConnectionIO]] val sitemapCaffeinCache: CaffeineCache[String, Seq[Url]] = @@ -75,25 +95,25 @@ object Fixture { val feedCaffeinCache: CaffeineCache[String, ResponseArticleWithCount] = Caffeine.newBuilder().expireAfterAccess(5, TimeUnit.SECONDS).build[String, ResponseArticleWithCount] val feedCache: CacheModule[String, ResponseArticleWithCount] = new CacheModule[String, ResponseArticleWithCount](feedCaffeinCache) - val feedService = new FeedService(feedCache, Modules.articleService) - - val authProvider = new AuthProvider(Modules.authService) - val corsProvider = new CorsProvider(Modules.config.cors) - - val archiveRouteV1 = new ArchiveRouteV1(Modules.archiveService) - val articleRouteV1 = new ArticleRouteV1(Modules.articleService) - val authorRouteV1 = new AuthorRouteV1(Modules.authorService) - val authRouteV1 = new AuthRouteV1(Modules.authService) - val cacheRouteV1 = new CacheRouteV1(authProvider, Modules.cacheService) - val contentTypeRouteV1 = new ContentTypeRouteV1(Modules.contentTypeService) - val contentRouteV1 = new ContentRouteV1(authProvider, Modules.contentService) - val feedRouteV1 = new FeedRouteV1(Modules.feedService) + val feedService = new FeedService(feedCache, modules.articleService) + + val authProvider = new AuthProvider(modules.authService) + val corsProvider = new CorsProvider(modules.config.cors) + + val archiveRouteV1 = new ArchiveRouteV1(modules.archiveService) + val articleRouteV1 = new ArticleRouteV1(modules.articleService) + val authorRouteV1 = new AuthorRouteV1(modules.authorService) + val authRouteV1 = new AuthRouteV1(modules.authService) + val cacheRouteV1 = new CacheRouteV1(authProvider, modules.cacheService) + val contentTypeRouteV1 = new ContentTypeRouteV1(modules.contentTypeService) + val contentRouteV1 = new ContentRouteV1(authProvider, modules.contentService) + val feedRouteV1 = new FeedRouteV1(modules.feedService) val homeRoute: HomeRoute = new HomeRoute() - val searchRouteV1 = new SearchRouteV1(Modules.searchService) - val seriesRouteV1 = new SeriesRouteV1(authProvider, Modules.seriesService) - val sitemapRouteV1 = new SitemapRouteV1(Modules.sitemapService) - val systemRouteV1 = new SystemRouteV1(Modules.config.http.endpoints.system) - val tagRouteV1 = new TagRouteV1(authProvider, Modules.tagService, Modules.articleService) + val searchRouteV1 = new SearchRouteV1(modules.searchService) + val seriesRouteV1 = new SeriesRouteV1(authProvider, modules.seriesService) + val sitemapRouteV1 = new SitemapRouteV1(modules.sitemapService) + val systemRouteV1 = new SystemRouteV1(modules.config.http.endpoints.system) + val tagRouteV1 = new TagRouteV1(authProvider, modules.tagService, modules.articleService) val router = new net.yoshinorin.qualtet.http.Router( corsProvider, @@ -201,13 +221,13 @@ object Fixture { def createContents(requestContents: List[RequestContent]) = { requestContents.foreach { rc => - Modules.contentService.createContentFromRequest(AuthorName(author.name.value), rc).unsafeRunSync() + modules.contentService.createContentFromRequest(AuthorName(author.name.value), rc).unsafeRunSync() } } def createSeries(requestSeries: List[RequestSeries]) = { requestSeries.foreach { rs => - Modules.seriesService.create(rs).unsafeRunSync() + modules.seriesService.create(rs).unsafeRunSync() } } diff --git a/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/ArticleRouteSpec.scala b/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/ArticleRouteSpec.scala index 0886dad1..2f9ebaaf 100644 --- a/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/ArticleRouteSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/ArticleRouteSpec.scala @@ -11,8 +11,7 @@ import net.yoshinorin.qualtet.domains.authors.AuthorName import net.yoshinorin.qualtet.domains.contents.{Path, RequestContent} import net.yoshinorin.qualtet.domains.robots.Attributes import net.yoshinorin.qualtet.http.ResponseProblemDetails -import net.yoshinorin.qualtet.Modules.* -import net.yoshinorin.qualtet.fixture.Fixture.{author, router, unsafeDecode} +import net.yoshinorin.qualtet.fixture.Fixture.{author, contentService, router, unsafeDecode} import org.scalatest.wordspec.AnyWordSpec import cats.effect.unsafe.implicits.global diff --git a/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/AuthRouteSpec.scala b/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/AuthRouteSpec.scala index 86d3d8f8..9de54140 100644 --- a/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/AuthRouteSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/AuthRouteSpec.scala @@ -9,8 +9,7 @@ import org.http4s.implicits.* import net.yoshinorin.qualtet.domains.authors.ResponseAuthor import net.yoshinorin.qualtet.auth.ResponseToken import net.yoshinorin.qualtet.http.ResponseProblemDetails -import net.yoshinorin.qualtet.Modules.* -import net.yoshinorin.qualtet.fixture.Fixture.{author, router, unsafeDecode} +import net.yoshinorin.qualtet.fixture.Fixture.{author, authorService, router, unsafeDecode} import org.scalatest.wordspec.AnyWordSpec import cats.effect.unsafe.implicits.global diff --git a/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/AuthorRouteSpec.scala b/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/AuthorRouteSpec.scala index b6731a4a..21578735 100644 --- a/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/AuthorRouteSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/AuthorRouteSpec.scala @@ -8,8 +8,7 @@ import org.http4s.headers.`Content-Type` import org.http4s.implicits.* import net.yoshinorin.qualtet.domains.authors.{AuthorName, ResponseAuthor} import net.yoshinorin.qualtet.http.ResponseProblemDetails -import net.yoshinorin.qualtet.fixture.Fixture.{author, author2, router, unsafeDecode} -import net.yoshinorin.qualtet.Modules.* +import net.yoshinorin.qualtet.fixture.Fixture.{author, author2, authorService, router, unsafeDecode} import org.scalatest.wordspec.AnyWordSpec import cats.effect.unsafe.implicits.global diff --git a/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/CacheRouteSpec.scala b/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/CacheRouteSpec.scala index f2ed1e3e..37137457 100644 --- a/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/CacheRouteSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/CacheRouteSpec.scala @@ -8,8 +8,7 @@ import org.http4s.implicits.* import org.typelevel.ci.* import org.scalatest.wordspec.AnyWordSpec import net.yoshinorin.qualtet.domains.authors.ResponseAuthor -import net.yoshinorin.qualtet.Modules.* -import net.yoshinorin.qualtet.fixture.Fixture.{author, expiredToken, nonExistsUserToken, router} +import net.yoshinorin.qualtet.fixture.Fixture.{authService, author, authorService, expiredToken, nonExistsUserToken, router} import net.yoshinorin.qualtet.auth.RequestToken import cats.effect.unsafe.implicits.global diff --git a/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/ContentRouteSpec.scala b/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/ContentRouteSpec.scala index 583314d8..784730b4 100644 --- a/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/ContentRouteSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/ContentRouteSpec.scala @@ -14,7 +14,6 @@ import net.yoshinorin.qualtet.domains.robots.Attributes import net.yoshinorin.qualtet.http.ResponseProblemDetails import net.yoshinorin.qualtet.fixture.Fixture.* import net.yoshinorin.qualtet.fixture.Fixture.{router => fixtureRouter} -import net.yoshinorin.qualtet.Modules.* import org.scalatest.wordspec.AnyWordSpec import java.time.Instant diff --git a/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/SeriesRouteSpec.scala b/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/SeriesRouteSpec.scala index a0d07c53..796c8ff2 100644 --- a/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/SeriesRouteSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/SeriesRouteSpec.scala @@ -13,7 +13,6 @@ import net.yoshinorin.qualtet.domains.series.{RequestSeries, Series, SeriesName} import net.yoshinorin.qualtet.http.ResponseProblemDetails import net.yoshinorin.qualtet.fixture.Fixture.* import net.yoshinorin.qualtet.fixture.Fixture.{authProvider => fixtureAuthProvider} -import net.yoshinorin.qualtet.Modules.* import org.scalatest.wordspec.AnyWordSpec import org.scalatest.BeforeAndAfterAll diff --git a/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/TagRouteSpec.scala b/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/TagRouteSpec.scala index f18a0060..084dc50d 100644 --- a/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/TagRouteSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/http/routes/v1/TagRouteSpec.scala @@ -14,7 +14,6 @@ import net.yoshinorin.qualtet.domains.tags.{ResponseTag, TagId} import net.yoshinorin.qualtet.http.ResponseProblemDetails import net.yoshinorin.qualtet.fixture.Fixture.* import net.yoshinorin.qualtet.fixture.Fixture.{authProvider => fixtureAuthProvider} -import net.yoshinorin.qualtet.Modules.* import org.scalatest.wordspec.AnyWordSpec import org.scalatest.BeforeAndAfterAll diff --git a/src/test/scala/net/yoshinorin/qualtet/infrastructure/db/MigratorSpec.scala b/src/test/scala/net/yoshinorin/qualtet/infrastructure/db/MigratorSpec.scala index ff5fa50d..a7c560e3 100644 --- a/src/test/scala/net/yoshinorin/qualtet/infrastructure/db/MigratorSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/infrastructure/db/MigratorSpec.scala @@ -1,8 +1,6 @@ package net.yoshinorin.qualtet.infrastructure.db -import net.yoshinorin.qualtet.fixture.Fixture.contentTypeService -import net.yoshinorin.qualtet.Modules -import net.yoshinorin.qualtet.infrastructure.db.doobie.DoobieExecuter +import net.yoshinorin.qualtet.fixture.Fixture.{contentTypeService, migrator} import org.scalatest.wordspec.AnyWordSpec import cats.effect.unsafe.implicits.global @@ -10,14 +8,11 @@ import cats.effect.unsafe.implicits.global // testOnly net.yoshinorin.qualtet.infrastructure.db.MigratorSpec class MigratorSpec extends AnyWordSpec { - val tx = Modules.doobieTransactor.make(Modules.config.db) - given dbContext: DoobieExecuter = new DoobieExecuter(tx) - "Migrator" should { "migrate" in { - Modules.migrator.migrate(contentTypeService).unsafeRunSync() + migrator.migrate(contentTypeService).unsafeRunSync() val result = (for { a <- contentTypeService.findByName("article") diff --git a/src/test/scala/net/yoshinorin/qualtet/tasks/CreateAuthorSpec.scala b/src/test/scala/net/yoshinorin/qualtet/tasks/CreateAuthorSpec.scala index e9a87492..be9c8e79 100644 --- a/src/test/scala/net/yoshinorin/qualtet/tasks/CreateAuthorSpec.scala +++ b/src/test/scala/net/yoshinorin/qualtet/tasks/CreateAuthorSpec.scala @@ -1,8 +1,7 @@ package net.yoshinorin.qualtet.tasks import net.yoshinorin.qualtet.domains.authors.AuthorName -import net.yoshinorin.qualtet.fixture.Fixture.{author, author2} -import net.yoshinorin.qualtet.Modules.* +import net.yoshinorin.qualtet.fixture.Fixture.{author, author2, authorService} import org.scalatest.wordspec.AnyWordSpec import cats.effect.unsafe.implicits.global @@ -12,14 +11,14 @@ class CreateAuthorSpec extends AnyWordSpec { "CreateAuthor" should { "create author" in { - CreateAuthor.main(Array(author.name.value, author.displayName.value, "pass")) + CreateAuthor.run(List(author.name.value, author.displayName.value, "pass")).unsafeRunSync() // TODO: Need assertion. Sometimes return `None` after change 9817e9fc7a57cb1e8eac1c168a715f9486ed0dc7 // val a = authorService.findByName(AuthorName(author.name.value)).unsafeRunSync() // assert(a.get.name.value === author.name.value) // NOTE: just for create test data - CreateAuthor.main(Array(author2.name.value, author2.displayName.value, "pass")) + CreateAuthor.run(List(author2.name.value, author2.displayName.value, "pass")).unsafeRunSync() // NOTE: avoid test failure. This is a just test data no need assert. // assert(a2.get.name.value === author2.name.value) @@ -28,7 +27,7 @@ class CreateAuthorSpec extends AnyWordSpec { "not be create author" in { assertThrows[IllegalArgumentException] { - CreateAuthor.main(Array("testUser2", "tu")) + CreateAuthor.run(List("testUser2", "tu")).unsafeRunSync() } val a = authorService.findByName(AuthorName("testUser2")).unsafeRunSync()