diff --git a/.github/workflows/decompose-frontend-test.yml b/.github/workflows/decompose-frontend-test.yml index c89a105a..3b4f21dc 100644 --- a/.github/workflows/decompose-frontend-test.yml +++ b/.github/workflows/decompose-frontend-test.yml @@ -2,6 +2,10 @@ name: Frontend Decompose Module Tests on: push: + branches: + - main + - master + - 'renovate/**' paths: - 'conduit-frontend/frontend-decompose-logic/**' - 'build-src/**' diff --git a/.github/workflows/http4k-backend-test.yml b/.github/workflows/http4k-backend-test.yml index ead46077..94cdd791 100644 --- a/.github/workflows/http4k-backend-test.yml +++ b/.github/workflows/http4k-backend-test.yml @@ -2,6 +2,10 @@ name: Http4k Backend Tests on: push: + branches: + - main + - master + - 'renovate/**' paths: - 'conduit-backend/**' - 'build-src/**' @@ -10,6 +14,31 @@ jobs: test: runs-on: ubuntu-latest + services: + postgres: + image: postgres:alpine + env: + POSTGRES_DB: test-db + POSTGRES_USER: test-user + POSTGRES_PASSWORD: test-password + options: >- + --health-cmd pg_isready + --health-interval 3s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 3s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + steps: - name: Checkout code uses: actions/checkout@v4 @@ -20,13 +49,26 @@ jobs: java-version: '21' distribution: 'temurin' + - name: Run Database Migrations + uses: red-gate/FlywayGitHubAction@main + with: + url: jdbc:postgresql://localhost:5432/test-db + user: test-user + password: test-password + locations: filesystem:./conduit-backend/db-migration/main,filesystem:./conduit-backend/db-migration/test + extraArgs: -cleanDisabled=false -clean=true + - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - name: Run Backend Tests working-directory: conduit-backend + env: + DB__URL: jdbc:postgresql://localhost:5432/test-db + DB__USER: test-user + DB__PASSWORD: test-password run: | - ./gradlew check + ./gradlew check - name: Upload build reports uses: actions/upload-artifact@v4 diff --git a/build-src/libs.versions.toml b/build-src/libs.versions.toml index 434ae52c..cde4185d 100644 --- a/build-src/libs.versions.toml +++ b/build-src/libs.versions.toml @@ -74,7 +74,7 @@ dev-backend-kotestBom = "io.kotest:kotest-bom:5.9.1" dev-backend-kotestKoin = "io.kotest.extensions:kotest-extensions-koin:1.3.0" dev-backend-mockk = "io.mockk:mockk:1.13.13" dev-backend-hoplite-yaml = "com.sksamuel.hoplite:hoplite-yaml:2.9.0" -dev-backend-sqlite = "org.xerial:sqlite-jdbc:3.47.1.0" +dev-backend-postgresql = "org.postgresql:postgresql:42.7.4" dev-backend-hikari = "com.zaxxer:HikariCP:6.2.1" dev-backend-flyway = "org.flywaydb:flyway-core:11.0.1" dev-backend-logback = "ch.qos.logback:logback-classic:1.5.12" @@ -104,4 +104,4 @@ kotlinCompose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kot compose = { id = "org.jetbrains.compose", version.ref = "compose" } ktorfit = { id = "de.jensklingenberg.ktorfit", version.ref = "ktorfit" } # a plugin used by gradle files for precompiled script plugin -buildConfig = { id = "com.github.gmazzo.buildconfig", version = "5.5.1" } \ No newline at end of file +buildConfig = { id = "com.github.gmazzo.buildconfig", version = "5.5.1" } diff --git a/conduit-backend/.run/Docker for Dev.run.xml b/conduit-backend/.run/Docker for Dev.run.xml new file mode 100644 index 00000000..564d52cd --- /dev/null +++ b/conduit-backend/.run/Docker for Dev.run.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/conduit-backend/build.gradle.kts b/conduit-backend/build.gradle.kts index 923f4fec..7b7d884e 100644 --- a/conduit-backend/build.gradle.kts +++ b/conduit-backend/build.gradle.kts @@ -37,9 +37,8 @@ dependencies { implementation("org.jetbrains.exposed:exposed-core") implementation("org.jetbrains.exposed:exposed-dao") implementation("org.jetbrains.exposed:exposed-jdbc") - implementation(libs.dev.backend.sqlite) + implementation(libs.dev.backend.postgresql) implementation(libs.dev.backend.hikari) - implementation(libs.dev.backend.flyway) implementation(libs.dev.backend.inlineLogging) implementation(libs.dev.backend.slf4j) runtimeOnly(libs.dev.backend.logback) diff --git a/conduit-backend/src/main/resources/db/migration/V1__create_tables.sql b/conduit-backend/db-migration/main/V1__create_tables.sql similarity index 82% rename from conduit-backend/src/main/resources/db/migration/V1__create_tables.sql rename to conduit-backend/db-migration/main/V1__create_tables.sql index 82dc3253..1de93e4a 100644 --- a/conduit-backend/src/main/resources/db/migration/V1__create_tables.sql +++ b/conduit-backend/db-migration/main/V1__create_tables.sql @@ -1,7 +1,7 @@ -- V1__CreateUsersTable.sql create table users ( - id integer primary key autoincrement, + id serial primary key, email varchar(255) not null unique, username varchar(255) not null unique, password varchar(255) not null, diff --git a/conduit-backend/db-migration/test/V999__insert_test_data.sql b/conduit-backend/db-migration/test/V999__insert_test_data.sql new file mode 100644 index 00000000..b0dbca70 --- /dev/null +++ b/conduit-backend/db-migration/test/V999__insert_test_data.sql @@ -0,0 +1,8 @@ +-- V999__insert_test_data.sql +INSERT INTO users (email, username, password, bio, image) +VALUES + ('john@example.com', 'john', '$2a$10$8BxPYyqz2.X5TKk2R6gGIeZ.Tkk2KmcC6yN1BxLLGCgMgMNEZpgjK', 'Hi, I''m John!', 'https://api.realworld.io/images/john.jpg'), + ('jane@example.com', 'jane', '$2a$10$8BxPYyqz2.X5TKk2R6gGIeZ.Tkk2KmcC6yN1BxLLGCgMgMNEZpgjK', 'Hi, I''m Jane!', 'https://api.realworld.io/images/jane.jpg'), + ('bob@example.com', 'bob', '$2a$10$8BxPYyqz2.X5TKk2R6gGIeZ.Tkk2KmcC6yN1BxLLGCgMgMNEZpgjK', NULL, NULL), + ('alice@example.com', 'alice', '$2a$10$8BxPYyqz2.X5TKk2R6gGIeZ.Tkk2KmcC6yN1BxLLGCgMgMNEZpgjK', 'Software developer', 'https://api.realworld.io/images/alice.jpg'), + ('test@test.com', 'test', '$2a$10$8BxPYyqz2.X5TKk2R6gGIeZ.Tkk2KmcC6yN1BxLLGCgMgMNEZpgjK', NULL, NULL); diff --git a/conduit-backend/docker/.env b/conduit-backend/docker/.env new file mode 100644 index 00000000..e9127a04 --- /dev/null +++ b/conduit-backend/docker/.env @@ -0,0 +1,5 @@ +# PostgreSQL +POSTGRES_DB=conduit-db +POSTGRES_USER=conduit-user +POSTGRES_PASSWORD=conduit-password + diff --git a/conduit-backend/docker/compose.base.yml b/conduit-backend/docker/compose.base.yml new file mode 100644 index 00000000..9cc7f1f9 --- /dev/null +++ b/conduit-backend/docker/compose.base.yml @@ -0,0 +1,52 @@ +services: + postgres: + image: postgres:alpine + container_name: conduit-postgres + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 3s + timeout: 5s + retries: 5 + + redis: + image: redis:alpine + container_name: conduit-redis + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 3s + timeout: 5s + retries: 5 + + flyway: + image: flyway/flyway:latest-alpine + container_name: conduit-flyway + profiles: ["migration"] + environment: + FLYWAY_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB} + FLYWAY_USER: ${POSTGRES_USER} + FLYWAY_PASSWORD: ${POSTGRES_PASSWORD} + FLYWAY_CONNECT_RETRIES: 10 + volumes: # consider having different migration files for different environments, see https://documentation.red-gate.com/fd/configuration-files-224003079.html + - ../db/migration:/flyway/sql + depends_on: + postgres: + condition: service_healthy + command: migrate + +volumes: + postgres_data: + name: conduit-postgres-data + redis_data: + name: conduit-redis-data + +networks: + default: + name: conduit-network diff --git a/conduit-backend/docker/compose.dev.yml b/conduit-backend/docker/compose.dev.yml new file mode 100644 index 00000000..f77e5c1e --- /dev/null +++ b/conduit-backend/docker/compose.dev.yml @@ -0,0 +1,21 @@ +# This file contains development-specific overrides +# Use with compose.base.yml like this: +# docker compose -f compose.base.yml -f compose.dev.yml up + +services: + postgres: + ports: + - "5432:5432" + + redis: + ports: + - "6379:6379" + + flyway: + volumes: + - ../db-migration/main:/flyway/sql # Regular migrations + - ../db-migration/test:/flyway/sql-test # Test migrations in separate directory + environment: + FLYWAY_LOCATIONS: filesystem:/flyway/sql,filesystem:/flyway/sql-test + FLYWAY_CLEAN_DISABLED: false + command: clean migrate \ No newline at end of file diff --git a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/Bootstrap.kt b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/Bootstrap.kt index 60e61e62..5ae014b6 100644 --- a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/Bootstrap.kt +++ b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/Bootstrap.kt @@ -2,36 +2,15 @@ package mikufan.cx.conduit.backend import mikufan.cx.conduit.backend.controller.ConduitServer import mikufan.cx.inlinelogging.KInlineLogging -import org.flywaydb.core.Flyway class Bootstrap( - private val flyway: Flyway, private val server: ConduitServer ) : Runnable { + override fun run() { - dbMigration() startServer() } - private fun dbMigration() { - log.info { "Performing DB migrations if any" } - val migrateResult = flyway.migrate() - if (migrateResult.migrationsExecuted > 0) { - if (migrateResult.success) { - log.info { "Database migration successful. Migrations applied: ${migrateResult.migrationsExecuted}" } - } else { - val failureString = migrateResult.failedMigrations.joinToString(separator = "\n ", prefix = "[\n", postfix = "\n") { - "${it.type} ${it.version} @ ${it.filepath} - ${it.description}" - } - val errorMessage = "Database migration failed: \n$failureString" - log.error { errorMessage } - throw IllegalStateException(errorMessage) - } - } else { // no migration applied - log.info { "No migration applied" } - } - } - private fun startServer() { log.info { "Starting server" } server.start() diff --git a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/config/loadConfig.kt b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/config/loadConfig.kt index 950e0706..56fadf15 100644 --- a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/config/loadConfig.kt +++ b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/config/loadConfig.kt @@ -1,11 +1,13 @@ package mikufan.cx.conduit.backend.config import com.sksamuel.hoplite.ConfigLoaderBuilder +import com.sksamuel.hoplite.addEnvironmentSource import com.sksamuel.hoplite.addResourceOrFileSource import com.sksamuel.hoplite.sources.CommandLinePropertySource fun loadConfig(args: Array): Config = ConfigLoaderBuilder.default() .addPropertySource(CommandLinePropertySource(args, prefix = "--", delimiter = "=")) // can handle empty array + .addEnvironmentSource() .addResourceOrFileSource("/application-prod.yml", optional = true, allowEmpty = true) .addResourceOrFileSource("/application.yml", allowEmpty = true) .build() diff --git a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/controller/handler/RegisterUserHandler.kt b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/controller/handler/RegisterUserHandler.kt index 15e71dad..61e071eb 100644 --- a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/controller/handler/RegisterUserHandler.kt +++ b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/controller/handler/RegisterUserHandler.kt @@ -15,7 +15,7 @@ class RegisterUserHandler( override fun invoke(request: Request): Response { val userDto = userRegistrationLen(request).user - val UserRegisterDto = userService.registerUser(userDto) - return userRspLens(UserRsp(UserRegisterDto), Response(Status.CREATED)) + val userRegisterDto = userService.registerUser(userDto) + return userRspLens(UserRsp(userRegisterDto), Response(Status.CREATED)) } } \ No newline at end of file diff --git a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/controller/routes.kt b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/controller/routes.kt index d5a48752..0a7a604a 100644 --- a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/controller/routes.kt +++ b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/controller/routes.kt @@ -12,6 +12,7 @@ import org.http4k.server.Http4kServer import org.http4k.server.JettyLoom import org.http4k.server.asServer +//// route setup //// fun conduitRoute(apiRoute: RoutingHttpHandler): RoutingHttpHandler = routes( "/healthcheck" bind GET to { Response(Status.OK).body("OK") }, diff --git a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/TransactionManager.kt b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/TransactionManager.kt index 17c44fa1..3d2dd505 100644 --- a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/TransactionManager.kt +++ b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/TransactionManager.kt @@ -5,15 +5,13 @@ import org.jetbrains.exposed.sql.Transaction import org.jetbrains.exposed.sql.transactions.transaction interface TransactionManager { - val db: Database - - fun tx(block: Transaction.() -> T): T { - return transaction(db) { - block() - } - } + fun tx(block: Transaction.() -> T): T } class TransactionManagerImpl( - override val db: Database -) : TransactionManager \ No newline at end of file + val db: Database +) : TransactionManager { + override fun tx(block: Transaction.() -> T): T = transaction(db) { + block() + } +} \ No newline at end of file diff --git a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/di.module.kt b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/di.module.kt index 4336472d..f23ab764 100644 --- a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/di.module.kt +++ b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/di.module.kt @@ -11,9 +11,8 @@ import org.koin.dsl.module */ val dbModule = module { single { creatDataSource(get().db) } - singleOf(::createFlyway) singleOf(::createExposedDb) - singleOf(::createTransactionManager) + singleOf(::createConduitTransactionManager) singleOf(::UserRepo) } \ No newline at end of file diff --git a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/models.kt b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/models.kt index 4fc472df..c6147556 100644 --- a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/models.kt +++ b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/models.kt @@ -1,5 +1,8 @@ package mikufan.cx.conduit.backend.db +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IntIdTable object Users : IntIdTable() { @@ -9,3 +12,13 @@ object Users : IntIdTable() { val bio = varchar("bio", 255).nullable() val image = varchar("image", 255).nullable() } + +class User(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Users) + + var email by Users.email + var username by Users.username + var password by Users.password + var bio by Users.bio + var image by Users.image +} \ No newline at end of file diff --git a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/repo/UserRepo.kt b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/repo/UserRepo.kt index 4232591c..72daf42a 100644 --- a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/repo/UserRepo.kt +++ b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/repo/UserRepo.kt @@ -1,44 +1,27 @@ package mikufan.cx.conduit.backend.db.repo +import mikufan.cx.conduit.backend.db.User import mikufan.cx.conduit.backend.db.Users -import mikufan.cx.conduit.common.UserDto import mikufan.cx.conduit.common.UserRegisterDto -import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.insertReturning -import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.deleteWhere class UserRepo { - fun getById(id: Int): UserDto? = Users - .selectAll().where { Users.id eq id } - .firstOrNull() - ?.toUserDto() - - fun getByEmail(email: String): UserDto? = Users - .selectAll().where { Users.email eq email } - .firstOrNull() - ?.toUserDto() - - fun getByUsername(username: String): UserDto? = Users - .selectAll().where { Users.username eq username } - .firstOrNull() - ?.toUserDto() - - fun insert(UserRegisterDto: UserRegisterDto): UserDto? = Users.insertReturning { - it[email] = UserRegisterDto.email - it[username] = UserRegisterDto.username - it[password] = UserRegisterDto.password - it[bio] = "" - it[image] = null + fun getById(id: Int): User? = User.findById(id) + + fun getByEmail(email: String): User? = User.find { Users.email eq email }.firstOrNull() + + fun getByUsername(username: String): User? = User.find { Users.username eq username }.firstOrNull() + + fun insert(UserRegisterDto: UserRegisterDto): User = User.new { + email = UserRegisterDto.email + username = UserRegisterDto.username + password = UserRegisterDto.password + bio = "" + image = null } - .firstOrNull() - ?.toUserDto() - - private fun ResultRow.toUserDto() = UserDto( - email = this[Users.email], - username = this[Users.username], - bio = this[Users.bio], - image = this[Users.image], - token = null - ) + + fun delete(id: Int): Boolean = Users.deleteWhere { Users.id eq id } > 0 + } \ No newline at end of file diff --git a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/setup.kt b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/setup.kt index 9a1f6268..f65019b1 100644 --- a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/setup.kt +++ b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/db/setup.kt @@ -3,7 +3,6 @@ package mikufan.cx.conduit.backend.db import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import mikufan.cx.conduit.backend.config.DbConfig -import org.flywaydb.core.Flyway import org.jetbrains.exposed.sql.Database import javax.sql.DataSource @@ -21,11 +20,6 @@ fun creatDataSource(dbConfig: DbConfig): DataSource { return hikariDataSource } -fun createFlyway(dataSource: DataSource): Flyway = Flyway - .configure() - .dataSource(dataSource) - .load() - fun createExposedDb(dataSource: DataSource) : Database = Database.connect(dataSource) -fun createTransactionManager(db: Database) : TransactionManager = TransactionManagerImpl(db) \ No newline at end of file +fun createConduitTransactionManager(db: Database) : TransactionManager = TransactionManagerImpl(db) diff --git a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/service/UserService.kt b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/service/UserService.kt index dfad5404..5d7d6da9 100644 --- a/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/service/UserService.kt +++ b/conduit-backend/src/main/kotlin/mikufan/cx/conduit/backend/service/UserService.kt @@ -12,17 +12,26 @@ class UserService( private val userRepo: UserRepo, ) { - fun registerUser(user: UserRegisterDto): UserDto = - txManager.tx { + fun registerUser(user: UserRegisterDto): UserDto { + val newUser = txManager.tx { val userDto = userRepo.getByUsername(user.username) ?: userRepo.getByEmail(user.email) if (userDto != null) { throw ConduitException("User already exists, username or email must be unique") } else { - val newUser = userRepo.insert(user) ?: throw ConduitException("Failed to create new user ${user.username}") - log.info { "Successfully created new user ${user.username}" } - newUser + userRepo.insert(user) } } + log.info { "Successfully created new user ${user.username}" } + + // TODO: set token + return UserDto( + email = newUser.email, + username = newUser.username, + bio = newUser.bio, + image = newUser.image, + token = null + ) + } } private val log = KInlineLogging.logger() \ No newline at end of file diff --git a/conduit-backend/src/main/resources/application.yml b/conduit-backend/src/main/resources/application.yml index 157b6ba5..b29ec731 100644 --- a/conduit-backend/src/main/resources/application.yml +++ b/conduit-backend/src/main/resources/application.yml @@ -1,9 +1,9 @@ port: 8080 # Example value db: - url: "jdbc:sqlite::memory:" # Example value -# user: # Optional -# password: # Optional - driver: "org.sqlite.JDBC" # Example value + url: "jdbc:postgresql://localhost:5432/conduit-db" # Example value + user: conduit-user + password: conduit-password + driver: "org.postgresql.Driver" cors: # allowedOrigins: # allowedMethods: diff --git a/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/db/repo/UserRepoTest.kt b/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/db/repo/UserRepoTest.kt index c6fe0210..9b197d76 100644 --- a/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/db/repo/UserRepoTest.kt +++ b/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/db/repo/UserRepoTest.kt @@ -8,34 +8,31 @@ import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import mikufan.cx.conduit.backend.db.TransactionManager import mikufan.cx.conduit.common.UserRegisterDto -import org.flywaydb.core.Flyway import org.koin.test.KoinTest import org.koin.test.inject class UserRepoTest : ShouldSpec(), KoinTest { override fun extensions(): List = listOf(KoinExtension(testDbConfig, mode = KoinLifecycleMode.Root)) - private val migration: Flyway by inject() private val txManager: TransactionManager by inject() private val userRepo: UserRepo by inject() init { context("user repo") { - migration.migrate() should("insert new user") { val UserRegisterDto = UserRegisterDto("email@email.com", "new user", "password") - val newUser = txManager.tx { - userRepo.insert(UserRegisterDto) - } + txManager.tx { + val newUser = userRepo.insert(UserRegisterDto) + newUser shouldNotBe null + newUser.apply { + email shouldBe UserRegisterDto.email + username shouldBe UserRegisterDto.username + } - newUser shouldNotBe null - newUser?.apply { - email shouldBe UserRegisterDto.email - username shouldBe UserRegisterDto.username + rollback() } } - } } } diff --git a/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/db/repo/di.module.kt b/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/db/repo/di.module.kt index ba25b0de..b8f26f38 100644 --- a/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/db/repo/di.module.kt +++ b/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/db/repo/di.module.kt @@ -1,18 +1,20 @@ package mikufan.cx.conduit.backend.db.repo -import io.mockk.mockk +import com.sksamuel.hoplite.ConfigLoaderBuilder +import com.sksamuel.hoplite.addEnvironmentSource +import com.sksamuel.hoplite.addResourceOrFileSource import mikufan.cx.conduit.backend.config.Config -import mikufan.cx.conduit.backend.config.DbConfig import mikufan.cx.conduit.backend.db.dbModule import org.koin.dsl.module val testDbConfig = module { single { - Config( - port = 0, - cors = mockk(), - db = DbConfig(url = "jdbc:sqlite::memory:", driver = "org.sqlite.JDBC") - ) + ConfigLoaderBuilder.default() + .addEnvironmentSource() + .addResourceOrFileSource("/application-test.yml", optional = true, allowEmpty = true) + .addResourceOrFileSource("/application.yml", allowEmpty = true) + .build() + .loadConfigOrThrow() } includes(dbModule) } \ No newline at end of file diff --git a/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/service/UserServiceTest.kt b/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/service/UserServiceTest.kt index f528e799..f4758cb3 100644 --- a/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/service/UserServiceTest.kt +++ b/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/service/UserServiceTest.kt @@ -6,44 +6,40 @@ import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import io.mockk.every import io.mockk.mockk -import mikufan.cx.conduit.backend.db.TransactionManager +import mikufan.cx.conduit.backend.db.User import mikufan.cx.conduit.backend.db.repo.UserRepo import mikufan.cx.conduit.backend.util.ConduitException +import mikufan.cx.conduit.backend.util.NoOpsTxManager import mikufan.cx.conduit.common.UserDto import mikufan.cx.conduit.common.UserRegisterDto -import org.jetbrains.exposed.sql.Transaction class UserServiceTest : ShouldSpec({ - // Create a mock of TransactionManager - val txManager = mockk() { - // Set up the behavior for the tx method - every { - tx(any Any?>()) - } answers { - // Call the lambda directly with a mocked Transaction - firstArg Any?>().invoke(mockk()) - } - } - - + val txManager = NoOpsTxManager context("user registration") { - val UserRegisterDto = UserRegisterDto("new user", "email@email.com", "password") + val userRegisterDto = UserRegisterDto("new user", "email@email.com", "password") should("register user successfully") { val userRepo = mockk() { every { getByEmail(any()) } returns null every { getByUsername(any()) } returns null every { insert(any()) } answers { val dto = firstArg() - UserDto(dto.email, dto.username, "", "", null) + mockk { + every { email } returns dto.email + every { username } returns dto.username + every { password } returns dto.password + every { bio } returns "" + every { image } returns "" + } } } val userService = UserService(txManager, userRepo) - val registerUser = userService.registerUser(UserRegisterDto) + val registerUser = userService.registerUser(userRegisterDto) registerUser shouldBe UserDto("new user", "email@email.com", "", "", null) } + should("throw on duplicate user") { val userRepo = mockk() { every { getByEmail(any()) } returns mockk() @@ -53,7 +49,7 @@ class UserServiceTest : ShouldSpec({ val userService = UserService(txManager, userRepo) val conduitException = shouldThrow { - userService.registerUser(UserRegisterDto) + userService.registerUser(userRegisterDto) } conduitException.message shouldContain "User already exists" diff --git a/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/util/NoOpsTxManager.kt b/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/util/NoOpsTxManager.kt new file mode 100644 index 00000000..28325844 --- /dev/null +++ b/conduit-backend/src/test/kotlin/mikufan/cx/conduit/backend/util/NoOpsTxManager.kt @@ -0,0 +1,15 @@ +package mikufan.cx.conduit.backend.util + +import io.mockk.mockk +import mikufan.cx.conduit.backend.db.TransactionManager +import org.jetbrains.exposed.sql.Transaction + +/** + * A transaction manager that does not actually perform any transactions. + * Useful for testing purposes. + * + * However, make sure the test code does not call any functions from [Transaction] or directly. + */ +object NoOpsTxManager : TransactionManager { + override fun tx(block: Transaction.() -> T): T = mockk().block() +} \ No newline at end of file diff --git a/conduit-backend/src/test/resources/application-test.yml b/conduit-backend/src/test/resources/application-test.yml new file mode 100644 index 00000000..e69de29b