From 5cfabe7c29f33d6c7903eb0eea27add8aca098c7 Mon Sep 17 00:00:00 2001 From: psuzn Date: Fri, 7 Jun 2024 13:33:37 +0545 Subject: [PATCH 1/4] dependencies upgrade --- backend/build.gradle.kts | 93 ++++---- .../kotlin/me/sujanpoudel/playdeals/Conf.kt | 8 +- .../me/sujanpoudel/playdeals/DIConfigurer.kt | 16 +- .../sujanpoudel/playdeals/FlywayVerticle.kt | 1 - .../kotlin/me/sujanpoudel/playdeals/Main.kt | 30 +-- .../me/sujanpoudel/playdeals/MainVerticle.kt | 11 +- .../sujanpoudel/playdeals/api/ApiVerticle.kt | 7 +- .../me/sujanpoudel/playdeals/api/ForexRate.kt | 25 +-- .../me/sujanpoudel/playdeals/api/Health.kt | 43 ++-- .../me/sujanpoudel/playdeals/api/deals/Api.kt | 47 ++-- .../playdeals/api/deals/GetDealsContext.kt | 3 +- .../playdeals/api/deals/NewDealContext.kt | 3 +- .../me/sujanpoudel/playdeals/common/Conf.kt | 115 +++++----- .../me/sujanpoudel/playdeals/common/Enums.kt | 12 +- .../sujanpoudel/playdeals/common/Metrices.kt | 12 +- .../sujanpoudel/playdeals/common/Routing.kt | 20 +- .../playdeals/domain/AndroidAppDetail.kt | 35 +-- .../sujanpoudel/playdeals/domain/ForexRate.kt | 4 +- .../sujanpoudel/playdeals/domain/NewDeal.kt | 35 +-- .../playdeals/domain/entities/DealEntity.kt | 24 ++- .../playdeals/domain/entities/Mappers.kt | 4 +- .../jobs/AndroidAppDetailScrapper.kt | 105 ++++----- .../jobs/AndroidAppExpiryCheckScheduler.kt | 37 ++-- .../playdeals/jobs/BackgroundJobsVerticle.kt | 3 +- .../playdeals/jobs/CoJobRequestHandler.kt | 7 +- .../playdeals/jobs/DealSummarizer.kt | 77 +++---- .../playdeals/jobs/ForexFetcher.kt | 85 ++++---- .../playdeals/jobs/RedditPostsScrapper.kt | 83 +++---- .../playdeals/repositories/DealRepository.kt | 5 +- .../repositories/KeyValuesRepository.kt | 5 +- .../caching/CachingDealRepository.kt | 8 +- .../persistent/PersistentDealRepository.kt | 33 +-- .../PersistentKeyValuesRepository.kt | 24 ++- .../playdeals/services/MessagingService.kt | 77 +++---- .../playdeals/usecases/DBHealthUseCase.kt | 20 +- .../playdeals/usecases/GetDealsUseCase.kt | 3 +- .../playdeals/usecases/GetForexUseCase.kt | 1 - .../playdeals/usecases/NewDealUseCase.kt | 3 +- .../playdeals/usecases/UseCaseEngine.kt | 21 +- .../sujanpoudel/playdeals/IntegrationTest.kt | 105 ++++----- .../me/sujanpoudel/playdeals/MainTest.kt | 87 ++++---- .../playdeals/api/deals/GetDealsApiTest.kt | 203 +++++++++--------- .../playdeals/api/deals/NewDealApiTest.kt | 126 ++++++----- .../playdeals/api/forex/GetForexApiTest.kt | 86 ++++---- .../playdeals/api/health/DBCleanupTest.kt | 25 +-- .../playdeals/api/health/HealthTest.kt | 31 +-- .../repositories/CachingDealRepositoryTest.kt | 137 ++++++------ .../PersistentDealRepositoryTest.kt | 166 +++++++------- .../PersistentKeyValueRepositoryTest.kt | 58 ++--- build.gradle.kts | 40 +--- gradle/libs.versions.toml | 88 ++++++++ gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 8 + 53 files changed, 1250 insertions(+), 1057 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 55a095c..bc36f82 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -2,8 +2,9 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { application - id("com.github.johnrengelman.shadow") version Versions.SHADOW - id("com.google.cloud.tools.jib") version "3.3.2" + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.jib) + alias(libs.plugins.shadow) } group = Artifact.GROUP @@ -37,51 +38,50 @@ jib { dependencies { implementation(kotlin("stdlib")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.COROUTINES}") - - implementation(platform("io.vertx:vertx-stack-depchain:${Versions.VERTX}")) - "io.vertx".let { vertx -> - implementation("$vertx:vertx-core") - implementation("$vertx:vertx-web-graphql") - implementation("$vertx:vertx-auth-jwt") - implementation("$vertx:vertx-sql-client-templates") - implementation("$vertx:vertx-web") - implementation("$vertx:vertx-pg-client") - implementation("$vertx:vertx-lang-kotlin-coroutines") - implementation("$vertx:vertx-lang-kotlin") - implementation("$vertx:vertx-health-check") - implementation("$vertx:vertx-web-client") - - testImplementation("$vertx:vertx-junit5") + implementation(libs.kotlinx.coroutines.core) + + implementation(platform(libs.vertx.depchain)) + + with(libs.vertx) { + implementation(core) + implementation(web) + implementation(pgClient) + implementation(coroutines) + implementation(kotlin) + implementation(healthCheck) + implementation(webClient) + + testImplementation(junit5) } - implementation("com.michael-bull.kotlin-result:kotlin-result:${Versions.KOTLIN_RESULT}") + implementation(libs.kotlinResult) - implementation("org.flywaydb:flyway-core:${Versions.FLYWAY}") - implementation("org.postgresql:postgresql:${Versions.POSTGRES}") - implementation("com.ongres.scram:client:${Versions.ONGRESS_SCARM}") + implementation(libs.flyway.core) + implementation(libs.flyway.postgresql) + implementation(libs.postgresql) + implementation(libs.scramOngressClient) - implementation("org.slf4j:slf4j-api:${Versions.SLF4J}") - implementation("org.slf4j:slf4j-simple:${Versions.SLF4J}") - implementation("io.github.microutils:kotlin-logging-jvm:${Versions.JVM_LOGGER}") + implementation(libs.slf4j.api) + implementation(libs.slf4j.simpe) + implementation(libs.kotlinLoggingJvm) - implementation("com.fasterxml.jackson.core:jackson-databind:${Versions.JACKSON}") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:${Versions.JACKSON}") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${Versions.JACKSON}") + implementation(libs.jackson.databind) + implementation(libs.jackson.moduleKotlin) + implementation(libs.jackson.datatype.jsr310) - implementation("org.kodein.di:kodein-di:${Versions.KODEIN_DI}") + implementation(libs.kodein) + implementation(libs.jobrunr) + implementation(libs.jobrunr.kotlin) + implementation(libs.firebaseAdmin) - implementation("org.jobrunr:jobrunr:${Versions.JOB_RUNNER}") - implementation("org.jobrunr:jobrunr-kotlin-1.8-support:${Versions.JOB_RUNNER}") - implementation("com.google.firebase:firebase-admin:9.2.0") + testImplementation(libs.junit.jupiter) + testImplementation(libs.kotest.assertions.core) + testImplementation(libs.mockk) - testImplementation("org.junit.jupiter:junit-jupiter:${Versions.JUNIT_JUPITER}") - testImplementation("io.kotest:kotest-assertions-core:${Versions.KO_TEST}") - testImplementation("io.mockk:mockk:${Versions.MOCKK}") - with("org.testcontainers") { - testImplementation("$this:testcontainers:${Versions.TEST_CONTAINERS}") - testImplementation("$this:junit-jupiter:${Versions.TEST_CONTAINERS}") - testImplementation("$this:postgresql:${Versions.TEST_CONTAINERS}") + with(libs.testcontainers) { + testImplementation(this) + testImplementation(junit) + testImplementation(postgresql) } } @@ -95,13 +95,14 @@ tasks.withType { } tasks.withType { - args = listOf( - "run", - mainVerticleName, - "--redeploy=$watchForChange", - "--launcher-class=$launcherClassName", - "--on-redeploy=$doOnChange" - ) + args = + listOf( + "run", + mainVerticleName, + "--redeploy=$watchForChange", + "--launcher-class=$launcherClassName", + "--on-redeploy=$doOnChange", + ) } tasks.test { diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/Conf.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/Conf.kt index b745318..0c9f381 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/Conf.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/Conf.kt @@ -3,7 +3,7 @@ package me.sujanpoudel.playdeals enum class Environment { PRODUCTION, DEVELOPMENT, - TEST + TEST, } data class Conf( @@ -12,7 +12,7 @@ data class Conf( val environment: Environment, val backgroundTask: BackgroundTask, val firebaseAuthCredential: String, - val forexApiKey: String + val forexApiKey: String, ) { data class DB( val host: String, @@ -20,13 +20,13 @@ data class Conf( val name: String, val username: String, val password: String, - val poolSize: Int + val poolSize: Int, ) data class BackgroundTask( val dashboardEnabled: Boolean, val dashboardUserName: String, - val dashboardPassword: String + val dashboardPassword: String, ) data class Api(val port: Int, val cors: String) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt index 8e36e27..61b634f 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt @@ -52,7 +52,7 @@ inline fun DI.get(tag: String? = null) = direct.instance(ta fun configureDI( vertx: Vertx, - conf: Conf + conf: Conf, ) = DI { bindSingleton { conf } @@ -67,7 +67,7 @@ fun configureDI( MainVerticle( apiVerticle = instance(), backgroundJobsVerticle = instance(), - flywayVerticle = instance() + flywayVerticle = instance(), ) } @@ -114,7 +114,7 @@ fun configureDI( setURL("jdbc:postgresql://${conf.db.host}:${conf.db.port}/${conf.db.name}?currentSchema=job_runr") user = conf.db.username password = conf.db.password - } + }, ) } @@ -125,15 +125,15 @@ fun configureDI( conf.backgroundTask.dashboardEnabled, JobRunrDashboardWebServerConfiguration .usingStandardDashboardConfiguration() - .andBasicAuthentication(conf.backgroundTask.dashboardUserName, conf.backgroundTask.dashboardPassword) + .andBasicAuthentication(conf.backgroundTask.dashboardUserName, conf.backgroundTask.dashboardPassword), ) .useJobActivator(instance()) .useBackgroundJobServer( BackgroundJobServerConfiguration.usingStandardBackgroundJobServerConfiguration() .andDeleteSucceededJobsAfter(Duration.ofMinutes(10)) .andPermanentlyDeleteDeletedJobsAfter(Duration.ofMinutes(10)) - .andWorkerCount(1) - .andPollIntervalInSeconds(10) + .andWorkerCount(2) + .andPollIntervalInSeconds(10), ) .initialize() } @@ -157,13 +157,13 @@ fun configureDI( AndroidAppExpiryCheckScheduler( repository = instance(), requestScheduler = instance(), - storageProvider = instance() + storageProvider = instance(), ) } bindSingleton { ForexFetcher( di = di, - conf = instance() + conf = instance(), ) } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/FlywayVerticle.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/FlywayVerticle.kt index 6d290d7..b1be991 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/FlywayVerticle.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/FlywayVerticle.kt @@ -4,7 +4,6 @@ import io.vertx.kotlin.coroutines.CoroutineVerticle import org.flywaydb.core.Flyway class FlywayVerticle(private val flyway: Flyway) : CoroutineVerticle() { - override suspend fun start() { flyway.migrate() } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/Main.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/Main.kt index 8f09984..2bf4f45 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/Main.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/Main.kt @@ -3,7 +3,7 @@ package me.sujanpoudel.playdeals import com.fasterxml.jackson.databind.ObjectMapper import com.github.michaelbull.result.getOrThrow import io.vertx.core.Vertx -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import kotlinx.coroutines.runBlocking import me.sujanpoudel.playdeals.common.BootstrapException import me.sujanpoudel.playdeals.common.buildConf @@ -12,20 +12,22 @@ import org.kodein.di.instance import kotlin.system.exitProcess private val vertx = Vertx.vertx() -val configuration = buildConf(System.getenv()).getOrThrow { - (it as BootstrapException).violations.forEach(::println) - exitProcess(-1) -} +val configuration = + buildConf(System.getenv()).getOrThrow { + (it as BootstrapException).violations.forEach(::println) + exitProcess(-1) + } val primaryDI = configureDI(vertx, configuration) -fun main(): Unit = runBlocking { - primaryDI.direct.instance() +fun main(): Unit = + runBlocking { + primaryDI.direct.instance() - vertx.deployVerticle(primaryDI.direct.instance()) - .onSuccess { logger.infoNotify("Deployed MainVerticle : $it") } - .onFailure { - logger.error(it) { "Error deploying main verticle" } - vertx.close() - }.await() -} + vertx.deployVerticle(primaryDI.direct.instance()) + .onSuccess { logger.infoNotify("Deployed MainVerticle : $it") } + .onFailure { + logger.error(it) { "Error deploying main verticle" } + vertx.close() + }.coAwait() + } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/MainVerticle.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/MainVerticle.kt index 7fb7237..7f0542b 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/MainVerticle.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/MainVerticle.kt @@ -1,19 +1,18 @@ package me.sujanpoudel.playdeals import io.vertx.kotlin.coroutines.CoroutineVerticle -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.api.ApiVerticle import me.sujanpoudel.playdeals.jobs.BackgroundJobsVerticle class MainVerticle( private val apiVerticle: ApiVerticle, private val flywayVerticle: FlywayVerticle, - private val backgroundJobsVerticle: BackgroundJobsVerticle + private val backgroundJobsVerticle: BackgroundJobsVerticle, ) : CoroutineVerticle() { - override suspend fun start() { - vertx.deployVerticle(flywayVerticle).await() - vertx.deployVerticle(backgroundJobsVerticle).await() - vertx.deployVerticle(apiVerticle).await() + vertx.deployVerticle(flywayVerticle).coAwait() + vertx.deployVerticle(backgroundJobsVerticle).coAwait() + vertx.deployVerticle(apiVerticle).coAwait() } } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ApiVerticle.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ApiVerticle.kt index 36e6a9a..4faa7ad 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ApiVerticle.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ApiVerticle.kt @@ -3,7 +3,7 @@ package me.sujanpoudel.playdeals.api import io.vertx.ext.web.Router import io.vertx.ext.web.handler.CorsHandler import io.vertx.kotlin.coroutines.CoroutineVerticle -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.Conf import me.sujanpoudel.playdeals.api.deals.appDealsApi import me.sujanpoudel.playdeals.logger @@ -11,9 +11,8 @@ import org.kodein.di.DirectDI import org.kodein.di.instance class ApiVerticle( - private val di: DirectDI + private val di: DirectDI, ) : CoroutineVerticle() { - override suspend fun start() { val config = di.instance() val router = Router.router(vertx) @@ -26,7 +25,7 @@ class ApiVerticle( vertx.createHttpServer() .requestHandler(router) .listen(config.api.port) - .await() + .coAwait() logger.info("API server running at : ${config.api.port}") } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt index 91bf83f..4648e24 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt @@ -10,16 +10,17 @@ import org.kodein.di.instance fun forexRateApi( di: DirectDI, - vertx: io.vertx.core.Vertx -): Router = Router.router(vertx).apply { - get() - .coHandler { ctx -> - ctx.executeUseCase( - useCase = di.instance(), - toContext = { }, - toInput = { } - ) { - ctx.json(jsonResponse(data = it)) + vertx: io.vertx.core.Vertx, +): Router = + Router.router(vertx).apply { + get() + .coHandler { ctx -> + ctx.executeUseCase( + useCase = di.instance(), + toContext = { }, + toInput = { }, + ) { + ctx.json(jsonResponse(data = it)) + } } - } -} + } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/Health.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/Health.kt index 4136cc4..765fc1d 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/Health.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/Health.kt @@ -14,31 +14,32 @@ import org.kodein.di.instance fun healthApi( di: DirectDI, - vertx: Vertx -): Router = Router.router(vertx).apply { - val dbHealthChecker = di.instance() + vertx: Vertx, +): Router = + Router.router(vertx).apply { + val dbHealthChecker = di.instance() - val livenessHandler = HealthCheckHandler.create(vertx) - val readinessHandler = HealthCheckHandler.create(vertx) + val livenessHandler = HealthCheckHandler.create(vertx) + val readinessHandler = HealthCheckHandler.create(vertx) - livenessHandler.register("status") { promise -> - promise.complete(Status.OK()) - } + livenessHandler.register("status") { promise -> + promise.complete(Status.OK()) + } - readinessHandler.register("status") { promise -> - promise.complete(Status.OK()) - } + readinessHandler.register("status") { promise -> + promise.complete(Status.OK()) + } - readinessHandler.register("postgres") { promise -> - CoroutineScope(Dispatchers.IO).launch(vertx.dispatcher()) { - if (dbHealthChecker.execute(Unit)) { - promise.complete(Status.OK()) - } else { - promise.complete(Status.KO()) + readinessHandler.register("postgres") { promise -> + CoroutineScope(Dispatchers.IO).launch(vertx.dispatcher()) { + if (dbHealthChecker.execute(Unit)) { + promise.complete(Status.OK()) + } else { + promise.complete(Status.KO()) + } } } - } - get("/liveness").handler(livenessHandler) - get("/readiness").handler(readinessHandler) -} + get("/liveness").handler(livenessHandler) + get("/readiness").handler(readinessHandler) + } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/Api.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/Api.kt index 196a0e3..1d44008 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/Api.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/Api.kt @@ -2,7 +2,7 @@ package me.sujanpoudel.playdeals.api.deals import io.vertx.core.Vertx import io.vertx.ext.web.Router -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.ContentTypes import me.sujanpoudel.playdeals.common.coHandler import me.sujanpoudel.playdeals.common.jsonResponse @@ -14,28 +14,29 @@ import org.kodein.di.instance fun appDealsApi( di: DirectDI, - vertx: Vertx -): Router = Router.router(vertx).apply { - get() - .coHandler { ctx -> - ctx.executeUseCase( - useCase = di.instance(), - toContext = { GetDealsContext(ctx.request().params()) }, - toInput = { GetDealsUseCase.Input(it.skip, it.take) } - ) { - ctx.json(jsonResponse(data = it)) + vertx: Vertx, +): Router = + Router.router(vertx).apply { + get() + .coHandler { ctx -> + ctx.executeUseCase( + useCase = di.instance(), + toContext = { GetDealsContext(ctx.request().params()) }, + toInput = { GetDealsUseCase.Input(it.skip, it.take) }, + ) { + ctx.json(jsonResponse(data = it)) + } } - } - post() - .consumes(ContentTypes.JSON) - .coHandler { ctx -> - ctx.executeUseCase( - useCase = di.instance(), - toContext = { NewDealContext(ctx.request().body().await().toJsonObject()) }, - toInput = { it.packageName } - ) { - ctx.json(jsonResponse("App added for queue")) + post() + .consumes(ContentTypes.JSON) + .coHandler { ctx -> + ctx.executeUseCase( + useCase = di.instance(), + toContext = { NewDealContext(ctx.request().body().coAwait().toJsonObject()) }, + toInput = { it.packageName }, + ) { + ctx.json(jsonResponse("App added for queue")) + } } - } -} + } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsContext.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsContext.kt index 65d5f52..1ebb283 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsContext.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsContext.kt @@ -5,9 +5,8 @@ import me.sujanpoudel.playdeals.exceptions.ClientErrorException import me.sujanpoudel.playdeals.usecases.Validated class GetDealsContext( - private val param: MultiMap + private val param: MultiMap, ) : Validated { - val skip by lazy { param.get("skip")?.toIntOrNull() ?: 0 } val take by lazy { param.get("take")?.toIntOrNull() ?: 10 } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealContext.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealContext.kt index 0442060..cff8c9c 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealContext.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealContext.kt @@ -6,9 +6,8 @@ import me.sujanpoudel.playdeals.exceptions.ClientErrorException import me.sujanpoudel.playdeals.usecases.Validated class NewDealContext( - private val request: JsonObject + private val request: JsonObject, ) : Validated { - private val packageNameField = "packageName" val packageName: String by lazy { request.getString(packageNameField) } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt index d0279ab..100d678 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt @@ -9,67 +9,72 @@ import java.util.Base64 class BootstrapException(val violations: List) : RuntimeException() -fun buildConf(envs: Map) = com.github.michaelbull.result.runCatching { - val violations = mutableListOf() +fun buildConf(envs: Map) = + com.github.michaelbull.result.runCatching { + val violations = mutableListOf() - @Suppress("UNCHECKED_CAST") - fun env( - envVarName: String, - default: String? = null, - converter: (String) -> T? = { it as? T } - ): T? = ( - envs[envVarName] ?: default ?: run { - violations += "No '$envVarName' env var defined!".also { logger.error { it } } - null - } - )?.let(converter) ?: run { - violations += "Invalid '$envVarName'" - null - } + @Suppress("UNCHECKED_CAST") + fun env( + envVarName: String, + default: String? = null, + converter: (String) -> T? = { it as? T }, + ): T? = + ( + envs[envVarName] ?: default ?: run { + violations += "No '$envVarName' env var defined!".also { logger.error { it } } + null + } + )?.let(converter) ?: run { + violations += "Invalid '$envVarName'" + null + } - val environment = env("ENV", Environment.PRODUCTION.name) { it.asEnumOrNull() } + val environment = env("ENV", Environment.PRODUCTION.name) { it.asEnumOrNull() } - val appPort = env("APP_PORT", "8888") { it.toIntOrNull() } - val cors = env("CORS", ".*.") + val appPort = env("APP_PORT", "8888") { it.toIntOrNull() } + val cors = env("CORS", ".*.") - val dbPort = env("DB_PORT", "5432") { it.toIntOrNull() } - val dbName = env("DB_NAME", "play_deals") - val dbPoolSize = env("DB_POOL_SIZE", "5") { it.toIntOrNull() } - val dbHost = env("DB_HOST") - val dbUsername = env("DB_USERNAME") - val dbPassword = env("DB_PASSWORD", "password") + val dbPort = env("DB_PORT", "5432") { it.toIntOrNull() } + val dbName = env("DB_NAME", "play_deals") + val dbPoolSize = env("DB_POOL_SIZE", "5") { it.toIntOrNull() } + val dbHost = env("DB_HOST") + val dbUsername = env("DB_USERNAME") + val dbPassword = env("DB_PASSWORD", "password") - val dashboardEnabled = env("DASHBOARD", "true") { it.toBooleanStrictOrNull() } - val dashboardUser = env("DASHBOARD_USER", "admin") - val dashboardPassword = env("DASHBOARD_PASS", "admin") + val dashboardEnabled = env("DASHBOARD", "true") { it.toBooleanStrictOrNull() } + val dashboardUser = env("DASHBOARD_USER", "admin") + val dashboardPassword = env("DASHBOARD_PASS", "admin") - val firebaseAuthCredential = env("FIREBASE_ADMIN_AUTH_CREDENTIALS") { - Base64.getDecoder().decode(it).decodeToString() - } + val firebaseAuthCredential = + env("FIREBASE_ADMIN_AUTH_CREDENTIALS") { + Base64.getDecoder().decode(it).decodeToString() + } - val forexApiKey = env("FOREX_API_KEY") + val forexApiKey = env("FOREX_API_KEY") - if (violations.isNotEmpty()) { - throw BootstrapException(violations) - } else { - Conf( - api = Conf.Api(appPort!!, cors = cors!!), - environment = environment!!, - db = Conf.DB( - host = dbHost!!, - port = dbPort!!, - name = dbName!!, - username = dbUsername!!, - password = dbPassword!!, - poolSize = dbPoolSize!! - ), - backgroundTask = Conf.BackgroundTask( - dashboardEnabled = dashboardEnabled!!, - dashboardUserName = dashboardUser!!, - dashboardPassword = dashboardPassword!! - ), - firebaseAuthCredential = firebaseAuthCredential!!, - forexApiKey = forexApiKey!! - ) + if (violations.isNotEmpty()) { + throw BootstrapException(violations) + } else { + Conf( + api = Conf.Api(appPort!!, cors = cors!!), + environment = environment!!, + db = + Conf.DB( + host = dbHost!!, + port = dbPort!!, + name = dbName!!, + username = dbUsername!!, + password = dbPassword!!, + poolSize = dbPoolSize!!, + ), + backgroundTask = + Conf.BackgroundTask( + dashboardEnabled = dashboardEnabled!!, + dashboardUserName = dashboardUser!!, + dashboardPassword = dashboardPassword!!, + ), + firebaseAuthCredential = firebaseAuthCredential!!, + forexApiKey = forexApiKey!!, + ) + } } -} diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Enums.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Enums.kt index a6ba8e8..da3680d 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Enums.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Enums.kt @@ -1,8 +1,10 @@ package me.sujanpoudel.playdeals.common inline fun > String.asEnum() = enumValueOf(this) -inline fun > String.asEnumOrNull() = try { - asEnum() -} catch (e: Exception) { - null -} + +inline fun > String.asEnumOrNull() = + try { + asEnum() + } catch (e: Exception) { + null + } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Metrices.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Metrices.kt index 52a3dd5..1730c63 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Metrices.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Metrices.kt @@ -4,10 +4,14 @@ import me.sujanpoudel.playdeals.logger import kotlin.time.DurationUnit import kotlin.time.measureTimedValue -inline fun loggingExecutionTime(message: String, action: () -> T): T { - val timedValue = measureTimedValue { - action.invoke() - } +inline fun loggingExecutionTime( + message: String, + action: () -> T, +): T { + val timedValue = + measureTimedValue { + action.invoke() + } logger.info("$message (took ${timedValue.duration.toString(DurationUnit.MILLISECONDS)})") return timedValue.value } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Routing.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Routing.kt index 48c81d1..c3f8f81 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Routing.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Routing.kt @@ -29,19 +29,21 @@ fun HttpServerResponse.contentType(value: String): HttpServerResponse = putHeade fun jsonResponse( message: String = "Success", - data: T? = null -): JsonObject = jsonObjectOf( - "message" to message, - "data" to data -) + data: T? = null, +): JsonObject = + jsonObjectOf( + "message" to message, + "data" to data, + ) const val UNKNOWN_ERROR_MESSAGE = "Something went wrong" fun RoutingContext.handleExceptions(exception: Throwable) { - val (message, statusCode) = when (exception) { - is ClientErrorException -> exception.message to exception.statusCode - else -> UNKNOWN_ERROR_MESSAGE to 500 - } + val (message, statusCode) = + when (exception) { + is ClientErrorException -> exception.message to exception.statusCode + else -> UNKNOWN_ERROR_MESSAGE to 500 + } this.response() .setStatusCode(statusCode) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/AndroidAppDetail.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/AndroidAppDetail.kt index 57e86f9..9a7b196 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/AndroidAppDetail.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/AndroidAppDetail.kt @@ -16,22 +16,23 @@ data class AndroidAppDetail( val downloads: String, val rating: String, val offerExpiresIn: OffsetDateTime?, - val source: String + val source: String, ) -fun AndroidAppDetail.asNewDeal() = NewDeal( - id = id, - name = name, - icon = icon, - images = images, - normalPrice = normalPrice, - currentPrice = currentPrice!!, - currency = currency, - storeUrl = storeUrl, - category = category, - downloads = downloads, - rating = rating, - offerExpiresIn = offerExpiresIn!!, - type = DealType.ANDROID_APP, - source = source -) +fun AndroidAppDetail.asNewDeal() = + NewDeal( + id = id, + name = name, + icon = icon, + images = images, + normalPrice = normalPrice, + currentPrice = currentPrice!!, + currency = currency, + storeUrl = storeUrl, + category = category, + downloads = downloads, + rating = rating, + offerExpiresIn = offerExpiresIn!!, + type = DealType.ANDROID_APP, + source = source, + ) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/ForexRate.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/ForexRate.kt index c85f694..eed6663 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/ForexRate.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/ForexRate.kt @@ -5,12 +5,12 @@ import java.time.OffsetDateTime // Rates are USD based data class ForexRate( val timestamp: OffsetDateTime, - val rates: List + val rates: List, ) data class ConversionRate( val currency: String, val symbol: String, val name: String, - val rate: Float + val rate: Float, ) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/NewDeal.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/NewDeal.kt index 5ecc8b2..a2b82dd 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/NewDeal.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/NewDeal.kt @@ -17,23 +17,24 @@ data class NewDeal( val rating: String, val offerExpiresIn: OffsetDateTime, val type: DealType, - val source: String + val source: String, ) val NewDeal.insertValues - get() = arrayOf( - id, - name, - icon, - images.toTypedArray(), - normalPrice, - currentPrice, - currency, - storeUrl, - category, - downloads, - rating, - offerExpiresIn, - type.toString(), - source - ) + get() = + arrayOf( + id, + name, + icon, + images.toTypedArray(), + normalPrice, + currentPrice, + currency, + storeUrl, + category, + downloads, + rating, + offerExpiresIn, + type.toString(), + source, + ) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/DealEntity.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/DealEntity.kt index e4f4625..4db6a33 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/DealEntity.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/DealEntity.kt @@ -7,7 +7,7 @@ enum class DealType { ANDROID_APP, IOS_APP, DESKTOP_APP, - OTHER + OTHER, } data class DealEntity( @@ -26,23 +26,25 @@ data class DealEntity( val type: DealType, val source: String, val createdAt: OffsetDateTime, - val updatedAt: OffsetDateTime + val updatedAt: OffsetDateTime, ) -private fun String.asCurrencySymbol() = when (this) { - "USD" -> "$" - else -> this -} +private fun String.asCurrencySymbol() = + when (this) { + "USD" -> "$" + else -> this + } private fun Float.formatAsPrice(): String { val int = toInt() val decimal = ((this - int) * 100).roundToInt() - val formattedDecimal = if (decimal < 10) { - "${decimal}0" - } else { - "$decimal" - } + val formattedDecimal = + if (decimal < 10) { + "${decimal}0" + } else { + "$decimal" + } return "$int.$formattedDecimal" } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/Mappers.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/Mappers.kt index 8e1b44b..82f1c91 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/Mappers.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/Mappers.kt @@ -20,11 +20,11 @@ fun Row.asAppDeal(): DealEntity { offerExpiresIn = get("offer_expires_in"), type = getString("type").asEnum(), source = get("source"), - createdAt = get("created_at"), - updatedAt = get("updated_at") + updatedAt = get("updated_at"), ) } fun Row?.valueOrNull() = this?.getString("value") + fun Row.value(): String = getString("value") diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppDetailScrapper.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppDetailScrapper.kt index d4bb8c0..3484232 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppDetailScrapper.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppDetailScrapper.kt @@ -7,7 +7,7 @@ import io.vertx.core.json.JsonObject import io.vertx.ext.web.client.WebClient import io.vertx.ext.web.client.WebClientOptions import io.vertx.kotlin.core.json.jsonObjectOf -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.Constants import me.sujanpoudel.playdeals.common.SIMPLE_NAME import me.sujanpoudel.playdeals.common.loggingExecutionTime @@ -38,13 +38,12 @@ enum class Value(val root: String, vararg val path: Int) { CURRENCY("ds:5", 1, 2, 57, 0, 0, 0, 0, 1, 0, 1), GENRE("ds:5", 1, 2, 79, 0, 0, 0), SCREENSHOTS_LIST("ds:5", 1, 2, 78, 0), - SCREENSHOTS_URL("", 3, 2); + SCREENSHOTS_URL("", 3, 2), } class AppDetailScrapper( - override val di: DI + override val di: DI, ) : CoJobRequestHandler(), DIAware { - private val jobsVerticle by instance() private val repository by instance() private val messagingService by instance() @@ -52,56 +51,61 @@ class AppDetailScrapper( WebClient.create(jobsVerticle.vertx, WebClientOptions().setDefaultHost("play.google.com")) } - override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest ${jobRequest.packageName}" - ) { - val packageName = jobRequest.packageName + override suspend fun handleRequest(jobRequest: Request): Unit = + loggingExecutionTime( + "$SIMPLE_NAME:: handleRequest ${jobRequest.packageName}", + ) { + val packageName = jobRequest.packageName - val app = loggingExecutionTime("$SIMPLE_NAME:: scrapping app details $packageName") { - getAppDetail(packageName) - } + val app = + loggingExecutionTime("$SIMPLE_NAME:: scrapping app details $packageName") { + getAppDetail(packageName) + } - when { - app.normalPrice == 0f -> { - logger.infoNotify("App $packageName(${app.name}) doesn't have any price") - repository.delete(packageName) - } + when { + app.normalPrice == 0f -> { + logger.infoNotify("App $packageName(${app.name}) doesn't have any price") + repository.delete(packageName) + } - app.normalPrice == app.currentPrice -> { - logger.infoNotify("App $packageName(${app.name}) deals has been expired") - repository.delete(packageName) - } + app.normalPrice == app.currentPrice -> { + logger.infoNotify("App $packageName(${app.name}) deals has been expired") + repository.delete(packageName) + } - (app.currentPrice ?: 0f) < app.normalPrice -> { - logger.info("Found deal for $packageName(${app.name}) ${app.currentPrice} ${app.currency}(${app.normalPrice} ${app.currency})") - repository.upsert(app.asNewDeal()).also { - messagingService.sendMessageForNewDeal(it) + (app.currentPrice ?: 0f) < app.normalPrice -> { + logger.info("Found deal for $packageName(${app.name}) ${app.currentPrice} ${app.currency}(${app.normalPrice} ${app.currency})") + repository.upsert(app.asNewDeal()).also { + messagingService.sendMessageForNewDeal(it) + } } } } - } private suspend fun getAppDetail(packageName: String): AndroidAppDetail { - val response = webClient.get("/store/apps/details?id=$packageName&hl=en&gl=us") - .send() - .await() + val response = + webClient.get("/store/apps/details?id=$packageName&hl=en&gl=us") + .send() + .coAwait() val body = response.bodyAsString() - val mapper = ObjectMapper().apply { - configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true) - configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true) - } + val mapper = + ObjectMapper().apply { + configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true) + configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true) + } - val matches = INIT_DATA_PATTERN.matcher(body).let { - val snippets = mutableListOf() - while (it.find()) { - snippets.add(it.group(1)) + val matches = + INIT_DATA_PATTERN.matcher(body).let { + val snippets = mutableListOf() + while (it.find()) { + snippets.add(it.group(1)) + } + snippets + }.map { + io.vertx.core.json.Json.decodeValue(mapper.readTree(it).toPrettyString()) as JsonObject } - snippets - }.map { - io.vertx.core.json.Json.decodeValue(mapper.readTree(it).toPrettyString()) as JsonObject - } val combined = jsonObjectOf() @@ -116,9 +120,10 @@ class AppDetailScrapper( id = packageName, name = combined.getValue(Value.TITLE), icon = combined.getValue(Value.ICON), - images = (combined.getValue(Value.SCREENSHOTS_LIST) as JsonArray).mapNotNull { - getValue(it as JsonArray, Value.SCREENSHOTS_URL.path.toTypedArray()) as? String - }, + images = + (combined.getValue(Value.SCREENSHOTS_LIST) as JsonArray).mapNotNull { + getValue(it as JsonArray, Value.SCREENSHOTS_URL.path.toTypedArray()) as? String + }, normalPrice = normalPrice, currency = combined.getValue(Value.CURRENCY) as String, currentPrice = currentPrice, @@ -126,10 +131,11 @@ class AppDetailScrapper( downloads = combined.getValue(Value.INSTALLS), storeUrl = "https://play.google.com/store/apps/details?id=$packageName", category = combined.getValue(Value.GENRE) as String, - offerExpiresIn = combined.getValueOrNull(Value.OFFER_END_TIME)?.let { - OffsetDateTime.ofInstant(Instant.ofEpochSecond(it.toLong()), ZoneOffset.UTC) - }, - source = Constants.DealSources.APP_DEAL_SUBREDDIT + offerExpiresIn = + combined.getValueOrNull(Value.OFFER_END_TIME)?.let { + OffsetDateTime.ofInstant(Instant.ofEpochSecond(it.toLong()), ZoneOffset.UTC) + }, + source = Constants.DealSources.APP_DEAL_SUBREDDIT, ) } @@ -147,7 +153,10 @@ class AppDetailScrapper( return getValue(getJsonArray(value.root), value.path.toTypedArray()) as T } - fun getValue(jsonObject: JsonArray, path: Array): Any { + fun getValue( + jsonObject: JsonArray, + path: Array, + ): Any { return if (path.size == 1) { jsonObject.getValue(path.first()) } else { diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppExpiryCheckScheduler.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppExpiryCheckScheduler.kt index c533145..c374dda 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppExpiryCheckScheduler.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppExpiryCheckScheduler.kt @@ -17,32 +17,35 @@ import java.util.UUID class AndroidAppExpiryCheckScheduler( private val repository: DealRepository, private val requestScheduler: JobRequestScheduler, - private val storageProvider: StorageProvider + private val storageProvider: StorageProvider, ) : CoJobRequestHandler() { + override suspend fun handleRequest(jobRequest: Request): Unit = + loggingExecutionTime( + "$SIMPLE_NAME:: handleRequest", + ) { + val apps = + repository.getPotentiallyExpiredDeals().stream() + .map { AppDetailScrapper.Request(it.id) } - override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest" - ) { - val apps = repository.getPotentiallyExpiredDeals().stream() - .map { AppDetailScrapper.Request(it.id) } + requestScheduler.enqueue(apps) - requestScheduler.enqueue(apps) - - val lastUpdatedTime = Instant.now().minus(1, ChronoUnit.HOURS) - val jobs = storageProvider.deleteJobsPermanently(StateName.FAILED, lastUpdatedTime) - logger.info("deleted FAILED `$jobs`") - } + val lastUpdatedTime = Instant.now().minus(1, ChronoUnit.HOURS) + val jobs = storageProvider.deleteJobsPermanently(StateName.FAILED, lastUpdatedTime) + logger.info("deleted FAILED `$jobs`") + } class Request private constructor() : JobRequest { override fun getJobRequestHandler() = AndroidAppExpiryCheckScheduler::class.java companion object { private val JOB_ID: UUID = UUID.nameUUIDFromBytes("AppExpiryCheckScheduler".toByteArray()) - operator fun invoke(): RecurringJobBuilder = RecurringJobBuilder.aRecurringJob() - .withJobRequest(Request()) - .withName("App Expiry Checker") - .withId(JOB_ID.toString()) - .withDuration(Duration.ofHours(6)) + + operator fun invoke(): RecurringJobBuilder = + RecurringJobBuilder.aRecurringJob() + .withJobRequest(Request()) + .withName("App Expiry Checker") + .withId(JOB_ID.toString()) + .withDuration(Duration.ofHours(6)) } } } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/BackgroundJobsVerticle.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/BackgroundJobsVerticle.kt index 2b46299..4eb1205 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/BackgroundJobsVerticle.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/BackgroundJobsVerticle.kt @@ -11,9 +11,8 @@ import org.kodein.di.direct import org.kodein.di.instance class BackgroundJobsVerticle( - override val di: DI + override val di: DI, ) : CoroutineVerticle(), DIAware { - private val jobRequestScheduler by instance() private val keyValuesRepository by instance() diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/CoJobRequestHandler.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/CoJobRequestHandler.kt index ac0608b..0175e3e 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/CoJobRequestHandler.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/CoJobRequestHandler.kt @@ -5,9 +5,10 @@ import org.jobrunr.jobs.lambdas.JobRequest import org.jobrunr.jobs.lambdas.JobRequestHandler abstract class CoJobRequestHandler : JobRequestHandler { - override fun run(jobRequest: T): Unit = runBlocking { - handleRequest(jobRequest) - } + override fun run(jobRequest: T): Unit = + runBlocking { + handleRequest(jobRequest) + } abstract suspend fun handleRequest(jobRequest: T) } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/DealSummarizer.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/DealSummarizer.kt index 89bd828..ac9b57e 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/DealSummarizer.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/DealSummarizer.kt @@ -20,57 +20,62 @@ import java.time.OffsetDateTime import java.util.UUID class DealSummarizer( - override val di: DI + override val di: DI, ) : CoJobRequestHandler(), DIAware { - private val dealRepository by instance() private val keyValueRepository by instance() private val messagingService by instance() - override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest" - ) { - val lastTimestamp = keyValueRepository.get(LAST_SUMMARY_TIMESTAMP)?.let(OffsetDateTime::parse) - ?: OffsetDateTime.now() + override suspend fun handleRequest(jobRequest: Request): Unit = + loggingExecutionTime( + "$SIMPLE_NAME:: handleRequest", + ) { + val lastTimestamp = + keyValueRepository.get(LAST_SUMMARY_TIMESTAMP)?.let(OffsetDateTime::parse) + ?: OffsetDateTime.now() - val deals = dealRepository.getNewDeals(lastTimestamp) + val deals = dealRepository.getNewDeals(lastTimestamp) - if (deals.isNotEmpty()) { - val maxCount = 6 - val dealsDescription = deals - .take(maxCount) - .mapIndexed { index, deal -> - "${index + 1}. ${deal.name} was ${deal.formattedNormalPrice()} is now ${deal.formattedCurrentPrice()}" - }.joinToString("\n") + if (deals.isNotEmpty()) { + val maxCount = 6 + val dealsDescription = + deals + .take(maxCount) + .mapIndexed { index, deal -> + "${index + 1}. ${deal.name} was ${deal.formattedNormalPrice()} is now ${deal.formattedCurrentPrice()}" + }.joinToString("\n") - messagingService.sendMessageToTopic( - topic = Constants.PushNotificationTopic.DEALS_SUMMARY, - title = "New ${deals.size} app deals are found since yesterday", - body = if (deals.size > maxCount) { - "$dealsDescription\n\n +${deals.size - maxCount} more..." - } else { - dealsDescription - } - ) - } else { - logger.infoNotify("$SIMPLE_NAME:: haven't got any deals since $lastTimestamp") - } + messagingService.sendMessageToTopic( + topic = Constants.PushNotificationTopic.DEALS_SUMMARY, + title = "New ${deals.size} app deals are found since yesterday", + body = + if (deals.size > maxCount) { + "$dealsDescription\n\n +${deals.size - maxCount} more..." + } else { + dealsDescription + }, + ) + } else { + logger.infoNotify("$SIMPLE_NAME:: haven't got any deals since $lastTimestamp") + } - keyValueRepository.set(LAST_SUMMARY_TIMESTAMP, OffsetDateTime.now().toString()) - } + keyValueRepository.set(LAST_SUMMARY_TIMESTAMP, OffsetDateTime.now().toString()) + } class Request private constructor() : JobRequest { override fun getJobRequestHandler() = DealSummarizer::class.java companion object { private val JOB_ID: UUID = UUID.nameUUIDFromBytes("deal-summarizer".toByteArray()) - operator fun invoke(): RecurringJobBuilder = RecurringJobBuilder.aRecurringJob() - .withJobRequest(Request()) - .withCron(Cron.daily(16)) - .withAmountOfRetries(2) - .withLabels("Deal Summarizer") - .withName("Deal Summarizer") - .withId(JOB_ID.toString()) + + operator fun invoke(): RecurringJobBuilder = + RecurringJobBuilder.aRecurringJob() + .withJobRequest(Request()) + .withCron(Cron.daily(16)) + .withAmountOfRetries(2) + .withLabels("Deal Summarizer") + .withName("Deal Summarizer") + .withId(JOB_ID.toString()) } } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt index 6bc2757..66e47d4 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt @@ -5,7 +5,7 @@ import com.google.gson.reflect.TypeToken import io.vertx.core.json.Json import io.vertx.ext.web.client.WebClient import io.vertx.ext.web.client.WebClientOptions -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.Conf import me.sujanpoudel.playdeals.common.SIMPLE_NAME import me.sujanpoudel.playdeals.common.loggingExecutionTime @@ -26,66 +26,68 @@ import java.util.UUID data class Currency( val name: String, - val symbol: String + val symbol: String, ) -fun loadCurrencies(): HashMap { +fun loadCurrencies(): HashMap { return Gson().fromJson( Thread.currentThread().contextClassLoader.getResource("currencies.json")?.readText() ?: "{}", - object : TypeToken>() {} + object : TypeToken>() {}, ) } class ForexFetcher( override val di: DI, - private val conf: Conf + private val conf: Conf, ) : CoJobRequestHandler(), DIAware { - private val backgroundJobsVerticle by instance() private val webClient by lazy { WebClient.create( backgroundJobsVerticle.vertx, - WebClientOptions().setSsl(false).setDefaultHost("api.exchangeratesapi.io") + WebClientOptions().setSsl(false).setDefaultHost("api.exchangeratesapi.io"), ) } private val repository by instance() - override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest" - ) { - val rates = getForexRates() - logger.info("got ${rates.rates.size} forex rate") - repository.saveForexRate(rates) - } + override suspend fun handleRequest(jobRequest: Request): Unit = + loggingExecutionTime( + "$SIMPLE_NAME:: handleRequest", + ) { + val rates = getForexRates() + logger.info("got ${rates.rates.size} forex rate") + repository.saveForexRate(rates) + } private suspend fun getForexRates(): ForexRate { val currencies = loadCurrencies() - val response = webClient.get("/v1/latest?access_key=${conf.forexApiKey}&format=1&base=EUR") - .send() - .await() - .bodyAsString() - .let { - Json.decodeValue(it) as io.vertx.core.json.JsonObject - } + val response = + webClient.get("/v1/latest?access_key=${conf.forexApiKey}&format=1&base=EUR") + .send() + .coAwait() + .bodyAsString() + .let { + Json.decodeValue(it) as io.vertx.core.json.JsonObject + } val epochSeconds = response.getLong("timestamp") val usdRate = response.getJsonObject("rates").getNumber("USD").toFloat() return ForexRate( timestamp = OffsetDateTime.ofInstant(java.time.Instant.ofEpochSecond(epochSeconds), ZoneOffset.UTC), - rates = response.getJsonObject("rates").map { - val currency = currencies[it.key] - - ConversionRate( - currency = it.key, - symbol = currency?.symbol ?: "$", - name = currency?.name ?: it.key, - rate = (it.value as Number).toFloat() / usdRate - ) - } + rates = + response.getJsonObject("rates").map { + val currency = currencies[it.key] + + ConversionRate( + currency = it.key, + symbol = currency?.symbol ?: "$", + name = currency?.name ?: it.key, + rate = (it.value as Number).toFloat() / usdRate, + ) + }, ) } @@ -94,12 +96,14 @@ class ForexFetcher( companion object { private val JOB_ID: UUID = UUID.nameUUIDFromBytes("ForexFetch".toByteArray()) - operator fun invoke(): RecurringJobBuilder = RecurringJobBuilder.aRecurringJob() - .withJobRequest(Request()) - .withName("ForexFetch") - .withId(JOB_ID.toString()) - .withDuration(Duration.ofDays(1)) - .withAmountOfRetries(3) + + operator fun invoke(): RecurringJobBuilder = + RecurringJobBuilder.aRecurringJob() + .withJobRequest(Request()) + .withName("ForexFetch") + .withId(JOB_ID.toString()) + .withDuration(Duration.ofDays(1)) + .withAmountOfRetries(3) fun immediate(): JobRequest = Request() } @@ -108,9 +112,10 @@ class ForexFetcher( private const val KEY_FOREX_RATE = "FOREX_RATE" -suspend fun KeyValuesRepository.getForexRate(): ForexRate? = get(KEY_FOREX_RATE)?.let { - Json.decodeValue(it, ForexRate::class.java) -} +suspend fun KeyValuesRepository.getForexRate(): ForexRate? = + get(KEY_FOREX_RATE)?.let { + Json.decodeValue(it, ForexRate::class.java) + } suspend fun KeyValuesRepository.saveForexRate(forexRate: ForexRate) = set(KEY_FOREX_RATE, Json.encode(forexRate)) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/RedditPostsScrapper.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/RedditPostsScrapper.kt index a9fab58..e0e5634 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/RedditPostsScrapper.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/RedditPostsScrapper.kt @@ -4,7 +4,7 @@ import io.vertx.core.json.JsonObject import io.vertx.ext.web.client.WebClient import io.vertx.ext.web.client.WebClientOptions import io.vertx.kotlin.core.json.jsonArrayOf -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.common.SIMPLE_NAME import me.sujanpoudel.playdeals.common.loggingExecutionTime import me.sujanpoudel.playdeals.infoNotify @@ -25,52 +25,54 @@ import java.util.UUID data class RedditPost( val id: String, val content: String, - val createdAt: OffsetDateTime + val createdAt: OffsetDateTime, ) class RedditPostsScrapper( - override val di: DI + override val di: DI, ) : CoJobRequestHandler(), DIAware { - private val verticle by instance() private val keyValueRepository by instance() private val webClient by lazy { WebClient.create( verticle.vertx, - WebClientOptions().setDefaultHost("www.reddit.com") + WebClientOptions().setDefaultHost("www.reddit.com"), ) } private val jobRequestScheduler by instance() - override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest" - ) { - val lastPostTime = keyValueRepository.get(LAST_REDDIT_POST_TIME)?.let(OffsetDateTime::parse) - - val posts = loggingExecutionTime( - "$SIMPLE_NAME:: Fetched reddit post, last created post was at : '$lastPostTime'" + override suspend fun handleRequest(jobRequest: Request): Unit = + loggingExecutionTime( + "$SIMPLE_NAME:: handleRequest", ) { - getLatestRedditPosts(lastPostTime ?: OffsetDateTime.MIN) - } + val lastPostTime = keyValueRepository.get(LAST_REDDIT_POST_TIME)?.let(OffsetDateTime::parse) - val appIds = posts.flatMap { post -> - PLAY_CONSOLE_REGX.findAll(post.content).toList().mapNotNull { - it.groupValues.lastOrNull() - } - }.distinct() + val posts = + loggingExecutionTime( + "$SIMPLE_NAME:: Fetched reddit post, last created post was at : '$lastPostTime'", + ) { + getLatestRedditPosts(lastPostTime ?: OffsetDateTime.MIN) + } - logger.infoNotify("$SIMPLE_NAME:: got ${posts.size} new posts (${appIds.size} Links)") + val appIds = + posts.flatMap { post -> + PLAY_CONSOLE_REGX.findAll(post.content).toList().mapNotNull { + it.groupValues.lastOrNull() + } + }.distinct() - appIds.forEach { packageName -> - val id = UUID.nameUUIDFromBytes(packageName.toByteArray()) - jobRequestScheduler.enqueue(id, AppDetailScrapper.Request(packageName)) - } + logger.infoNotify("$SIMPLE_NAME:: got ${posts.size} new posts (${appIds.size} Links)") - posts.firstOrNull()?.let { - logger.info("$SIMPLE_NAME:: Last reddit post was at ${it.createdAt} with id ${it.id}") - keyValueRepository.set(LAST_REDDIT_POST_TIME, it.createdAt.toString()) + appIds.forEach { packageName -> + val id = UUID.nameUUIDFromBytes(packageName.toByteArray()) + jobRequestScheduler.enqueue(id, AppDetailScrapper.Request(packageName)) + } + + posts.firstOrNull()?.let { + logger.info("$SIMPLE_NAME:: Last reddit post was at ${it.createdAt} with id ${it.id}") + keyValueRepository.set(LAST_REDDIT_POST_TIME, it.createdAt.toString()) + } } - } private suspend fun getLatestRedditPosts(lastPostTime: OffsetDateTime): List { val path = "/r/googleplaydeals/new.json?limit=100" @@ -93,13 +95,14 @@ class RedditPostsScrapper( RedditPost( id = data.getString("name"), content = data.getString("selftext").trim().ifBlank { data.getString("url") }, - createdAt = data.getDouble("created").toLong().let { - OffsetDateTime.ofInstant(Instant.ofEpochSecond(it), ZoneOffset.UTC) - } + createdAt = + data.getDouble("created").toLong().let { + OffsetDateTime.ofInstant(Instant.ofEpochSecond(it), ZoneOffset.UTC) + }, ) } } - .await() + .coAwait() .filter { it.createdAt > lastPostTime } @@ -110,13 +113,15 @@ class RedditPostsScrapper( companion object { private val JOB_ID: UUID = UUID.nameUUIDFromBytes("Reddit Posts".toByteArray()) - operator fun invoke(): RecurringJobBuilder = RecurringJobBuilder.aRecurringJob() - .withJobRequest(Request()) - .withAmountOfRetries(2) - .withLabels("Reddit") - .withName("Reddit Post Scrap") - .withId(JOB_ID.toString()) - .withDuration(Duration.ofHours(1)) + + operator fun invoke(): RecurringJobBuilder = + RecurringJobBuilder.aRecurringJob() + .withJobRequest(Request()) + .withAmountOfRetries(2) + .withLabels("Reddit") + .withName("Reddit Post Scrap") + .withId(JOB_ID.toString()) + .withDuration(Duration.ofHours(1)) } } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/DealRepository.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/DealRepository.kt index 979bb3c..0ef9251 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/DealRepository.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/DealRepository.kt @@ -5,7 +5,10 @@ import me.sujanpoudel.playdeals.domain.entities.DealEntity import java.time.OffsetDateTime interface DealRepository { - suspend fun getAll(skip: Int, take: Int): List + suspend fun getAll( + skip: Int, + take: Int, + ): List suspend fun upsert(appDeal: NewDeal): DealEntity diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/KeyValuesRepository.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/KeyValuesRepository.kt index 9e02f25..28592bf 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/KeyValuesRepository.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/KeyValuesRepository.kt @@ -1,7 +1,10 @@ package me.sujanpoudel.playdeals.repositories interface KeyValuesRepository { - suspend fun set(key: String, value: String): String + suspend fun set( + key: String, + value: String, + ): String suspend fun get(key: String): String? diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/caching/CachingDealRepository.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/caching/CachingDealRepository.kt index 7aa049c..f30463b 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/caching/CachingDealRepository.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/caching/CachingDealRepository.kt @@ -6,9 +6,8 @@ import me.sujanpoudel.playdeals.logger import me.sujanpoudel.playdeals.repositories.DealRepository class CachingDealRepository( - private val delegate: DealRepository + private val delegate: DealRepository, ) : DealRepository by delegate { - private val cache by lazy { HashMap(0, 0.8f) } @@ -28,7 +27,10 @@ class CachingDealRepository( } } - override suspend fun getAll(skip: Int, take: Int): List { + override suspend fun getAll( + skip: Int, + take: Int, + ): List { initialize() return if (cacheInitialized) { cache.values.toList().drop(skip).take(take) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentDealRepository.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentDealRepository.kt index e5b3602..45fb7fd 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentDealRepository.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentDealRepository.kt @@ -1,6 +1,6 @@ package me.sujanpoudel.playdeals.repositories.persistent -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.sqlclient.SqlClient import me.sujanpoudel.playdeals.common.exec import me.sujanpoudel.playdeals.domain.NewDeal @@ -11,15 +11,18 @@ import me.sujanpoudel.playdeals.repositories.DealRepository import java.time.OffsetDateTime class PersistentDealRepository( - private val sqlClient: SqlClient + private val sqlClient: SqlClient, ) : DealRepository { - override suspend fun getAll(skip: Int, take: Int): List { + override suspend fun getAll( + skip: Int, + take: Int, + ): List { return sqlClient.preparedQuery( """ SELECT * FROM "deal" ORDER BY created_at DESC OFFSET $1 LIMIT $2 - """.trimIndent() + """.trimIndent(), ).exec(skip, take) - .await() + .coAwait() .map { it.asAppDeal() } } @@ -43,9 +46,9 @@ class PersistentDealRepository( type = $13, source = $14 RETURNING * - """.trimIndent() + """.trimIndent(), ).exec(*appDeal.insertValues) - .await() + .coAwait() .first() .asAppDeal() } @@ -54,9 +57,9 @@ class PersistentDealRepository( return sqlClient.preparedQuery( """ DELETE from "deal" where id=$1 RETURNING * - """.trimIndent() + """.trimIndent(), ).exec(id) - .await() + .coAwait() .firstOrNull()?.asAppDeal() } @@ -64,9 +67,9 @@ class PersistentDealRepository( return sqlClient.preparedQuery( """ SELECT * FROM "deal" where offer_expires_in < current_timestamp - """.trimIndent() + """.trimIndent(), ).exec() - .await() + .coAwait() .map { it.asAppDeal() } } @@ -74,9 +77,9 @@ class PersistentDealRepository( return sqlClient.preparedQuery( """ SELECT * FROM "deal" where created_at > $1 - """.trimIndent() + """.trimIndent(), ).exec(since) - .await() + .coAwait() .map { it.asAppDeal() } } @@ -84,9 +87,9 @@ class PersistentDealRepository( return sqlClient.preparedQuery( """ SELECT * FROM "deal" where id = $1 - """.trimIndent() + """.trimIndent(), ).exec(packageName) - .await() + .coAwait() .firstOrNull()?.asAppDeal() } } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentKeyValuesRepository.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentKeyValuesRepository.kt index 2cd7aaa..b643049 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentKeyValuesRepository.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentKeyValuesRepository.kt @@ -1,6 +1,6 @@ package me.sujanpoudel.playdeals.repositories.persistent -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.sqlclient.SqlClient import me.sujanpoudel.playdeals.common.exec import me.sujanpoudel.playdeals.domain.entities.value @@ -8,18 +8,20 @@ import me.sujanpoudel.playdeals.domain.entities.valueOrNull import me.sujanpoudel.playdeals.repositories.KeyValuesRepository class PersistentKeyValuesRepository( - private val sqlClient: SqlClient + private val sqlClient: SqlClient, ) : KeyValuesRepository { - - override suspend fun set(key: String, value: String): String { + override suspend fun set( + key: String, + value: String, + ): String { return sqlClient.preparedQuery( """ INSERT INTO "key_value_store" VALUES ($1,$2) ON CONFLICT(key) DO UPDATE SET value = $2 RETURNING * - """.trimIndent() + """.trimIndent(), ).exec(key, value) - .await() + .coAwait() .first() .value() } @@ -28,9 +30,9 @@ class PersistentKeyValuesRepository( return sqlClient.preparedQuery( """ SELECT * FROM "key_value_store" WHERE key = $1 - """.trimIndent() + """.trimIndent(), ).exec(key) - .await() + .coAwait() .firstOrNull() .valueOrNull() } @@ -38,9 +40,9 @@ class PersistentKeyValuesRepository( override suspend fun delete(key: String) { sqlClient.preparedQuery( """ - DELETE FROM "key_value_store" WHERE key = $1 - """.trimIndent() + DELETE FROM "key_value_store" WHERE key = $1 + """.trimIndent(), ).exec(key) - .await() + .coAwait() } } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/services/MessagingService.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/services/MessagingService.kt index 7d29003..e16c197 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/services/MessagingService.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/services/MessagingService.kt @@ -28,55 +28,58 @@ suspend fun ApiFuture.awaitIgnoring() { class MessagingService( private val firebaseMessaging: FirebaseMessaging, - private val environment: Environment + private val environment: Environment, ) { - private fun String.asTopic() = if (environment == Environment.PRODUCTION) this else "$this-dev" suspend fun sendMessageToTopic( topic: String, title: String, body: String, - imageUrl: String? = null + imageUrl: String? = null, ) { - val message = Message.builder() - .setTopic(topic.asTopic()) - .setNotification( - Notification.builder() - .setTitle(title) - .setBody(body) - .setImage(imageUrl) - .build() - ).setAndroidConfig( - AndroidConfig.builder() - .setCollapseKey(topic) - .setNotification( - AndroidNotification.builder() - .setPriority(AndroidNotification.Priority.HIGH) - .setChannelId(topic) - .build() - ) - .build() - ).build() + val message = + Message.builder() + .setTopic(topic.asTopic()) + .setNotification( + Notification.builder() + .setTitle(title) + .setBody(body) + .setImage(imageUrl) + .build(), + ).setAndroidConfig( + AndroidConfig.builder() + .setCollapseKey(topic) + .setNotification( + AndroidNotification.builder() + .setPriority(AndroidNotification.Priority.HIGH) + .setChannelId(topic) + .build(), + ) + .build(), + ).build() firebaseMessaging.sendAsync(message) .awaitIgnoring() } } -suspend inline fun MessagingService.sendMessageForNewDeal(deal: DealEntity) = sendMessageToTopic( - topic = if (deal.currentPrice == 0f) { - Constants.PushNotificationTopic.NEW_FREE_DEAL - } else { - Constants.PushNotificationTopic.NEW_DISCOUNT_DEAL - }, - title = "New deal found", - body = "${deal.name} was ${deal.formattedNormalPrice()} is now ${deal.formattedCurrentPrice()}", - imageUrl = deal.icon -) +suspend inline fun MessagingService.sendMessageForNewDeal(deal: DealEntity) = + sendMessageToTopic( + topic = + if (deal.currentPrice == 0f) { + Constants.PushNotificationTopic.NEW_FREE_DEAL + } else { + Constants.PushNotificationTopic.NEW_DISCOUNT_DEAL + }, + title = "New deal found", + body = "${deal.name} was ${deal.formattedNormalPrice()} is now ${deal.formattedCurrentPrice()}", + imageUrl = deal.icon, + ) -suspend inline fun MessagingService.sendMaintenanceLog(message: String) = sendMessageToTopic( - topic = Constants.PushNotificationTopic.DEV_LOG, - title = "Maintenance Log", - body = message -) +suspend inline fun MessagingService.sendMaintenanceLog(message: String) = + sendMessageToTopic( + topic = Constants.PushNotificationTopic.DEV_LOG, + title = "Maintenance Log", + body = message, + ) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/DBHealthUseCase.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/DBHealthUseCase.kt index 94c908c..d3e5fc1 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/DBHealthUseCase.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/DBHealthUseCase.kt @@ -1,21 +1,21 @@ package me.sujanpoudel.playdeals.usecases -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.sqlclient.SqlClient import org.kodein.di.DI import org.kodein.di.instance class DBHealthUseCase( - di: DI + di: DI, ) : UseCase { - private val sqlClient by di.instance() - override suspend fun doExecute(input: Unit): Boolean = runCatching { - sqlClient.preparedQuery("""SELECT 1""") - .execute() - .await() - }.map { rs -> - rs.count() == 1 - }.getOrDefault(false) + override suspend fun doExecute(input: Unit): Boolean = + runCatching { + sqlClient.preparedQuery("""SELECT 1""") + .execute() + .coAwait() + }.map { rs -> + rs.count() == 1 + }.getOrDefault(false) } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetDealsUseCase.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetDealsUseCase.kt index 8e403fd..c3a7a9e 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetDealsUseCase.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetDealsUseCase.kt @@ -6,9 +6,8 @@ import org.kodein.di.DI import org.kodein.di.instance class GetDealsUseCase( - di: DI + di: DI, ) : UseCase> { - private val appDealsRepository by di.instance() class Input(val skip: Int, val take: Int) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetForexUseCase.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetForexUseCase.kt index a9ddfbe..dbe407e 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetForexUseCase.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetForexUseCase.kt @@ -7,7 +7,6 @@ import org.kodein.di.DI import org.kodein.di.instance class GetForexUseCase(di: DI) : UseCase { - private val appDealsRepository by di.instance() override suspend fun doExecute(input: Unit): ForexRate? { diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/NewDealUseCase.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/NewDealUseCase.kt index 6c36df3..5270e87 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/NewDealUseCase.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/NewDealUseCase.kt @@ -8,9 +8,8 @@ import org.kodein.di.instance import java.util.UUID class NewDealUseCase( - di: DI + di: DI, ) : UseCase { - private val jobRequestScheduler by di.instance() private val dealsRepository by di.instance() diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/UseCaseEngine.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/UseCaseEngine.kt index eba5669..877db18 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/UseCaseEngine.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/UseCaseEngine.kt @@ -19,6 +19,7 @@ interface UseCase { } suspend fun validate(input: Input) {} + suspend fun doExecute(input: Input): Output } @@ -27,10 +28,16 @@ suspend fun RoutingContext.executeUseCase( toContext: suspend () -> Request, toInput: (Request) -> Input, onError: (Throwable) -> Unit = this::handleExceptions, - onSuccess: (Output) -> Unit -): Result = runCatching { toContext.invoke() } - .andThen { runCatching { (it as? Validated)?.validate(); it } } - .andThen { runCatching { toInput.invoke(it) } } - .andThen { runCatching { useCase.execute(it) } } - .onSuccess(onSuccess) - .onFailure(onError) + onSuccess: (Output) -> Unit, +): Result = + runCatching { toContext.invoke() } + .andThen { + runCatching { + (it as? Validated)?.validate() + it + } + } + .andThen { runCatching { toInput.invoke(it) } } + .andThen { runCatching { useCase.execute(it) } } + .onSuccess(onSuccess) + .onFailure(onError) diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt index adad98c..0919d55 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt @@ -5,7 +5,7 @@ import io.vertx.core.Vertx import io.vertx.ext.web.client.WebClient import io.vertx.ext.web.client.WebClientOptions import io.vertx.junit5.VertxExtension -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.kotlin.coroutines.dispatcher import io.vertx.sqlclient.SqlClient import kotlinx.coroutines.runBlocking @@ -36,45 +36,50 @@ abstract class IntegrationTest(private val vertx: Vertx) { WebClientOptions() .apply { defaultPort = conf.api.port - } + }, ) } - private val conf = Conf( - api = Conf.Api(8888, cors = ".*."), - environment = Environment.TEST, - db = Conf.DB( - host = postgresqlContainer.host, - port = postgresqlContainer.firstMappedPort, - name = DB_NAME, - username = DB_USERNAME, - password = DB_PASSWORD, - 3 - ), - backgroundTask = Conf.BackgroundTask( - false, - "admin", - "admin" - ), - firebaseAuthCredential = "", - forexApiKey = "" - ) + private val conf = + Conf( + api = Conf.Api(8888, cors = ".*."), + environment = Environment.TEST, + db = + Conf.DB( + host = postgresqlContainer.host, + port = postgresqlContainer.firstMappedPort, + name = DB_NAME, + username = DB_USERNAME, + password = DB_PASSWORD, + 3, + ), + backgroundTask = + Conf.BackgroundTask( + false, + "admin", + "admin", + ), + firebaseAuthCredential = "", + forexApiKey = "", + ) var di = configureDI(vertx, conf) - protected fun runTest(block: suspend () -> Unit): Unit = runBlocking(vertx.dispatcher()) { - di.direct.instance() - try { - block() - } catch (e: Exception) { - e.printStackTrace() - throw e + protected fun runTest(block: suspend () -> Unit): Unit = + runBlocking(vertx.dispatcher()) { + di.direct.instance() + try { + block() + } catch (e: Exception) { + e.printStackTrace() + throw e + } } - } - private fun deployVerticle(): String = runBlocking(vertx.dispatcher()) { - vertx.deployVerticle(di.direct.instance()).await() - } + private fun deployVerticle(): String = + runBlocking(vertx.dispatcher()) { + vertx.deployVerticle(di.direct.instance()).coAwait() + } @BeforeEach fun assignDeploymentId() { @@ -89,33 +94,35 @@ abstract class IntegrationTest(private val vertx: Vertx) { .query(CLEAN_UP_DB_QUERY).execute() .onSuccess { log.info { "Successfully cleaned up dh" } } .onFailure { log.error(it) { "Could not cleanup db" } } - .await() + .coAwait() } @AfterEach - fun undeployVerticle() = runBlocking(vertx.dispatcher()) { - vertx.undeploy(deploymentId).await() - log.info { "un-deployed deployment id $deploymentId" } - } + fun undeployVerticle() = + runBlocking(vertx.dispatcher()) { + vertx.undeploy(deploymentId).coAwait() + log.info { "un-deployed deployment id $deploymentId" } + } companion object { - @JvmStatic - protected val CLEAN_UP_DB_QUERY = """ + protected val CLEAN_UP_DB_QUERY = + """ DELETE FROM "deal" WHERE True; DELETE FROM "key_value_store" WHERE True; - """.trimIndent() + """.trimIndent() @Container @JvmStatic - val postgresqlContainer: PostgreSQLContainer = PostgreSQLContainer( - DockerImageName.parse(System.getenv("POSTGRES_IMAGE") ?: "postgres:14") - .asCompatibleSubstituteFor("postgres") - ) - .apply { - withDatabaseName(DB_NAME) - withUsername(DB_USERNAME) - withPassword(DB_PASSWORD) - } + val postgresqlContainer: PostgreSQLContainer = + PostgreSQLContainer( + DockerImageName.parse(System.getenv("POSTGRES_IMAGE") ?: "postgres:14") + .asCompatibleSubstituteFor("postgres"), + ) + .apply { + withDatabaseName(DB_NAME) + withUsername(DB_USERNAME) + withPassword(DB_PASSWORD) + } } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/MainTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/MainTest.kt index 0c8e1ee..d8d07dc 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/MainTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/MainTest.kt @@ -12,25 +12,23 @@ import org.junit.jupiter.api.Test class MainTest { @Test fun `Should return a proper conf with all values from env`() { - val env = mutableMapOf( - "ENV" to "DEVELOPMENT", - "APP_PORT" to "123", - "CORS" to "*.example.com", - - "DASHBOARD" to "true", - "DASHBOARD_USER" to "user", - "DASHBOARD_PASS" to "admin", - - "DB_HOST" to "localhost1", - "DB_USERNAME" to "u", - "DB_PASSWORD" to "p", - "DB_POOL_SIZE" to "8", - "DB_PORT" to "3333", - "DB_NAME" to "db-name", - - "FIREBASE_ADMIN_AUTH_CREDENTIALS" to "dGVzdF9jcmVk", - "FOREX_API_KEY" to "forex_key" - ) + val env = + mutableMapOf( + "ENV" to "DEVELOPMENT", + "APP_PORT" to "123", + "CORS" to "*.example.com", + "DASHBOARD" to "true", + "DASHBOARD_USER" to "user", + "DASHBOARD_PASS" to "admin", + "DB_HOST" to "localhost1", + "DB_USERNAME" to "u", + "DB_PASSWORD" to "p", + "DB_POOL_SIZE" to "8", + "DB_PORT" to "3333", + "DB_NAME" to "db-name", + "FIREBASE_ADMIN_AUTH_CREDENTIALS" to "dGVzdF9jcmVk", + "FOREX_API_KEY" to "forex_key", + ) val conf = buildConf(env).unwrap() @@ -55,14 +53,15 @@ class MainTest { @Test fun `Should return a proper conf with some defaults being taken`() { - val env = mutableMapOf( - "DB_PORT" to "3333", - "DB_HOST" to "localhost", - "DB_USERNAME" to "u", - "DB_PASSWORD" to "p", - "FIREBASE_ADMIN_AUTH_CREDENTIALS" to "dGVzdF9jcmVk", - "FOREX_API_KEY" to "forex_key" - ) + val env = + mutableMapOf( + "DB_PORT" to "3333", + "DB_HOST" to "localhost", + "DB_USERNAME" to "u", + "DB_PASSWORD" to "p", + "FIREBASE_ADMIN_AUTH_CREDENTIALS" to "dGVzdF9jcmVk", + "FOREX_API_KEY" to "forex_key", + ) val conf = buildConf(env).unwrap() @@ -87,24 +86,26 @@ class MainTest { @Test fun `Should return all violations`() { - val env = mutableMapOf( - "APP_PORT" to "BAD_APP_PORT", - "ENV" to "prod" - ) + val env = + mutableMapOf( + "APP_PORT" to "BAD_APP_PORT", + "ENV" to "prod", + ) val violations = ((buildConf(env).unwrapError()) as BootstrapException).violations violations.shouldNotBeEmpty() - violations shouldContainExactlyInAnyOrder listOf( - "Invalid 'ENV'", - "Invalid 'APP_PORT'", - "No 'DB_HOST' env var defined!", - "Invalid 'DB_HOST'", - "No 'DB_USERNAME' env var defined!", - "Invalid 'DB_USERNAME'", - "No 'FIREBASE_ADMIN_AUTH_CREDENTIALS' env var defined!", - "Invalid 'FIREBASE_ADMIN_AUTH_CREDENTIALS'", - "No 'FOREX_API_KEY' env var defined!", - "Invalid 'FOREX_API_KEY'" - ) + violations shouldContainExactlyInAnyOrder + listOf( + "Invalid 'ENV'", + "Invalid 'APP_PORT'", + "No 'DB_HOST' env var defined!", + "Invalid 'DB_HOST'", + "No 'DB_USERNAME' env var defined!", + "Invalid 'DB_USERNAME'", + "No 'FIREBASE_ADMIN_AUTH_CREDENTIALS' env var defined!", + "Invalid 'FIREBASE_ADMIN_AUTH_CREDENTIALS'", + "No 'FOREX_API_KEY' env var defined!", + "Invalid 'FOREX_API_KEY'", + ) } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsApiTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsApiTest.kt index 73f05dd..c9df63b 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsApiTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsApiTest.kt @@ -5,7 +5,7 @@ import com.fasterxml.jackson.module.kotlin.readValue import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.shouldBe import io.vertx.core.Vertx -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.Constants import me.sujanpoudel.playdeals.IntegrationTest import me.sujanpoudel.playdeals.api.ApiResponse @@ -18,119 +18,128 @@ import me.sujanpoudel.playdeals.repositories.persistent.PersistentDealRepository import org.junit.jupiter.api.Test import java.time.OffsetDateTime -private val newDeal = NewDeal( - id = "id", - name = "name", - icon = "icon", - images = listOf("img0", "img1"), - normalPrice = 12f, - currentPrice = 12f, - currency = "USD", - storeUrl = "store_url", - category = "unknown", - downloads = "12+", - rating = "12", - offerExpiresIn = OffsetDateTime.now(), - type = DealType.ANDROID_APP, - source = Constants.DealSources.APP_DEAL_SUBREDDIT -) +private val newDeal = + NewDeal( + id = "id", + name = "name", + icon = "icon", + images = listOf("img0", "img1"), + normalPrice = 12f, + currentPrice = 12f, + currency = "USD", + storeUrl = "store_url", + category = "unknown", + downloads = "12+", + rating = "12", + offerExpiresIn = OffsetDateTime.now(), + type = DealType.ANDROID_APP, + source = Constants.DealSources.APP_DEAL_SUBREDDIT, + ) class GetDealsApiTest(vertx: Vertx) : IntegrationTest(vertx) { @Test - fun `should send error if skip param is less than 0`() = runTest { - val response = httpClient.get("/api/deals/?skip=-1") - .send() - .await() + fun `should send error if skip param is less than 0`() = + runTest { + val response = + httpClient.get("/api/deals/?skip=-1") + .send() + .coAwait() - val responseBody = response.bodyAsJsonObject() + val responseBody = response.bodyAsJsonObject() - response.statusCode() shouldBe 400 - responseBody.getString("message") shouldBe "skip Can't be less than 0" - } + response.statusCode() shouldBe 400 + responseBody.getString("message") shouldBe "skip Can't be less than 0" + } @Test - fun `should send error if take param is less than 1`() = runTest { - val response = httpClient.get("/api/deals/?take=0") - .send() - .await() + fun `should send error if take param is less than 1`() = + runTest { + val response = + httpClient.get("/api/deals/?take=0") + .send() + .coAwait() - val responseBody = response.bodyAsJsonObject() + val responseBody = response.bodyAsJsonObject() - response.statusCode() shouldBe 400 - responseBody.getString("message") shouldBe "take Can't be less than 1" - } + response.statusCode() shouldBe 400 + responseBody.getString("message") shouldBe "take Can't be less than 1" + } @Test - fun `should return app deals`() = runTest { - val repository = di.get() + fun `should return app deals`() = + runTest { + val repository = di.get() - val app0 = repository.upsert(newDeal) - val app1 = repository.upsert(newDeal.copy(id = "id1")) + val app0 = repository.upsert(newDeal) + val app1 = repository.upsert(newDeal.copy(id = "id1")) - val response = httpClient.get("/api/deals/") - .send() - .await() + val response = + httpClient.get("/api/deals/") + .send() + .coAwait() - val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) + val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) - response.statusCode() shouldBe 200 - deals.data!!.size shouldBe 2 - deals.data.shouldContainAll(listOf(app0, app1)) - } + response.statusCode() shouldBe 200 + deals.data!!.size shouldBe 2 + deals.data.shouldContainAll(listOf(app0, app1)) + } @Test - fun `should correctly handle skip parameter`() = runTest { - val repository = di.get() - - repository.upsert(newDeal) - repository.upsert(newDeal.copy(id = "id1")) - repository.upsert(newDeal.copy(id = "id2")) - repository.upsert(newDeal.copy(id = "id3")) - - httpClient.get("/api/deals?skip=1") - .send() - .await().also { response -> - val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) - - response.statusCode() shouldBe 200 - deals.data!!.size shouldBe 3 - } - - httpClient.get("/api/deals?skip=3") - .send() - .await().also { response -> - val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) - - response.statusCode() shouldBe 200 - deals.data!!.size shouldBe 1 - } - } + fun `should correctly handle skip parameter`() = + runTest { + val repository = di.get() + + repository.upsert(newDeal) + repository.upsert(newDeal.copy(id = "id1")) + repository.upsert(newDeal.copy(id = "id2")) + repository.upsert(newDeal.copy(id = "id3")) + + httpClient.get("/api/deals?skip=1") + .send() + .coAwait().also { response -> + val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) + + response.statusCode() shouldBe 200 + deals.data!!.size shouldBe 3 + } + + httpClient.get("/api/deals?skip=3") + .send() + .coAwait().also { response -> + val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) + + response.statusCode() shouldBe 200 + deals.data!!.size shouldBe 1 + } + } @Test - fun `should correctly handle take parameter`() = runTest { - val repository = di.get() - - repository.upsert(newDeal) - repository.upsert(newDeal.copy(id = "id1")) - repository.upsert(newDeal.copy(id = "id2")) - repository.upsert(newDeal.copy(id = "id3")) - - httpClient.get("/api/deals?take=2") - .send() - .await().also { response -> - val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) - - response.statusCode() shouldBe 200 - deals.data!!.size shouldBe 2 - } - - httpClient.get("/api/deals?take=1") - .send() - .await().also { response -> - val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) - - response.statusCode() shouldBe 200 - deals.data!!.size shouldBe 1 - } - } + fun `should correctly handle take parameter`() = + runTest { + val repository = di.get() + + repository.upsert(newDeal) + repository.upsert(newDeal.copy(id = "id1")) + repository.upsert(newDeal.copy(id = "id2")) + repository.upsert(newDeal.copy(id = "id3")) + + httpClient.get("/api/deals?take=2") + .send() + .coAwait().also { response -> + val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) + + response.statusCode() shouldBe 200 + deals.data!!.size shouldBe 2 + } + + httpClient.get("/api/deals?take=1") + .send() + .coAwait().also { response -> + val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) + + response.statusCode() shouldBe 200 + deals.data!!.size shouldBe 1 + } + } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealApiTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealApiTest.kt index 860369d..1923317 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealApiTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealApiTest.kt @@ -3,7 +3,7 @@ package me.sujanpoudel.playdeals.api.deals import io.kotest.matchers.shouldBe import io.vertx.core.Vertx import io.vertx.kotlin.core.json.jsonObjectOf -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.Constants import me.sujanpoudel.playdeals.IntegrationTest import me.sujanpoudel.playdeals.domain.NewDeal @@ -17,78 +17,86 @@ import java.time.OffsetDateTime import java.util.UUID class NewDealApiTest(vertx: Vertx) : IntegrationTest(vertx) { - @Test - fun `should send error response if packageName is null`() = runTest { - val response = httpClient.post("/api/deals") - .sendJson(jsonObjectOf()) - .await() + fun `should send error response if packageName is null`() = + runTest { + val response = + httpClient.post("/api/deals") + .sendJson(jsonObjectOf()) + .coAwait() - val responseBody = response.bodyAsJsonObject() + val responseBody = response.bodyAsJsonObject() - response.statusCode() shouldBe 400 - responseBody.getString("message") shouldBe "packageName is required" - } + response.statusCode() shouldBe 400 + responseBody.getString("message") shouldBe "packageName is required" + } @Test - fun `should send error response if packageName is invalid`() = runTest { - val response = httpClient.post("/api/deals") - .sendJson(jsonObjectOf("packageName" to "11111")) - .await() + fun `should send error response if packageName is invalid`() = + runTest { + val response = + httpClient.post("/api/deals") + .sendJson(jsonObjectOf("packageName" to "11111")) + .coAwait() - val responseBody = response.bodyAsJsonObject() + val responseBody = response.bodyAsJsonObject() - response.statusCode() shouldBe 400 - responseBody.getString("message") shouldBe "Invalid value for packageName" - } + response.statusCode() shouldBe 400 + responseBody.getString("message") shouldBe "Invalid value for packageName" + } @Test - fun `should enqueue a app detail scrap request on success`() = runTest { - val storageProvider = di.get() + fun `should enqueue a app detail scrap request on success`() = + runTest { + val storageProvider = di.get() - val packageName = "com.example.app" + val packageName = "com.example.app" - val response = httpClient.post("/api/deals") - .sendJson(jsonObjectOf("packageName" to packageName)) - .await() + val response = + httpClient.post("/api/deals") + .sendJson(jsonObjectOf("packageName" to packageName)) + .coAwait() - val job = storageProvider.getJobById(UUID.nameUUIDFromBytes(packageName.encodeToByteArray())) + val job = storageProvider.getJobById(UUID.nameUUIDFromBytes(packageName.encodeToByteArray())) - job.state shouldBe StateName.ENQUEUED + job.state shouldBe StateName.ENQUEUED - response.statusCode() shouldBe 200 - } + response.statusCode() shouldBe 200 + } @Test - fun `should should 200 if the app already exists`() = runTest { - di.get() - val repository = di.get() - - val packageName = "com.example.app" - - val newDeal = NewDeal( - id = packageName, - name = "name", - icon = "icon", - images = listOf("img0", "img1"), - normalPrice = 12f, - currentPrice = 12f, - currency = "USD", - storeUrl = "store_url", - category = "unknown", - downloads = "12+", - rating = "12", - offerExpiresIn = OffsetDateTime.now(), - type = DealType.ANDROID_APP, - source = Constants.DealSources.APP_DEAL_SUBREDDIT - ) - - repository.upsert(newDeal) - - val response = httpClient.post("/api/deals") - .sendJson(jsonObjectOf("packageName" to packageName)) - .await() - - response.statusCode() shouldBe 200 - } + fun `should should 200 if the app already exists`() = + runTest { + di.get() + val repository = di.get() + + val packageName = "com.example.app" + + val newDeal = + NewDeal( + id = packageName, + name = "name", + icon = "icon", + images = listOf("img0", "img1"), + normalPrice = 12f, + currentPrice = 12f, + currency = "USD", + storeUrl = "store_url", + category = "unknown", + downloads = "12+", + rating = "12", + offerExpiresIn = OffsetDateTime.now(), + type = DealType.ANDROID_APP, + source = Constants.DealSources.APP_DEAL_SUBREDDIT, + ) + + repository.upsert(newDeal) + + val response = + httpClient.post("/api/deals") + .sendJson(jsonObjectOf("packageName" to packageName)) + .coAwait() + + response.statusCode() shouldBe 200 + } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt index 1f1fbc4..a7745aa 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt @@ -3,7 +3,7 @@ package me.sujanpoudel.playdeals.api.forex import io.kotest.matchers.shouldBe import io.vertx.core.Vertx import io.vertx.core.json.JsonObject -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.IntegrationTest import me.sujanpoudel.playdeals.domain.ConversionRate import me.sujanpoudel.playdeals.domain.ForexRate @@ -16,59 +16,65 @@ import java.time.OffsetDateTime import java.time.ZoneOffset class GetForexApiTest(vertx: Vertx) : IntegrationTest(vertx) { - @Test - fun `Key value repo should properly store the forex rate`() = runTest { - val repository = di.get() + fun `Key value repo should properly store the forex rate`() = + runTest { + val repository = di.get() - val forexRate = ForexRate( - timestamp = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC), - rates = listOf(ConversionRate("USD", "$", "US Dollar", 1.1f)) - ) - repository.saveForexRate(forexRate) + val forexRate = + ForexRate( + timestamp = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC), + rates = listOf(ConversionRate("USD", "$", "US Dollar", 1.1f)), + ) + repository.saveForexRate(forexRate) - val savedForexRate = repository.getForexRate() + val savedForexRate = repository.getForexRate() - savedForexRate shouldBe forexRate - } + savedForexRate shouldBe forexRate + } @Test - fun `should return forex if there is data`() = runTest { - val repository = di.get() + fun `should return forex if there is data`() = + runTest { + val repository = di.get() - val forexRate = ForexRate( - timestamp = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC), - rates = listOf(ConversionRate("USD", "$", "US Dollar", 1.1f)) - ) + val forexRate = + ForexRate( + timestamp = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC), + rates = listOf(ConversionRate("USD", "$", "US Dollar", 1.1f)), + ) - repository.saveForexRate(forexRate) + repository.saveForexRate(forexRate) - val response = httpClient.get("/api/forex") - .send() - .await() - .bodyAsJsonObject() + val response = + httpClient.get("/api/forex") + .send() + .coAwait() + .bodyAsJsonObject() - response.getJsonObject("data").also { data -> - OffsetDateTime.parse(data.getString("timestamp")) shouldBe forexRate.timestamp - data.getJsonArray("rates").also { rates -> - rates.size() shouldBe 1 - (rates.first() as JsonObject).also { rate -> - rate.getString("currency") shouldBe "USD" - rate.getString("symbol") shouldBe "$" - rate.getString("name") shouldBe "US Dollar" - rate.getFloat("rate") shouldBe 1.1f + response.getJsonObject("data").also { data -> + OffsetDateTime.parse(data.getString("timestamp")) shouldBe forexRate.timestamp + data.getJsonArray("rates").also { rates -> + rates.size() shouldBe 1 + (rates.first() as JsonObject).also { rate -> + rate.getString("currency") shouldBe "USD" + rate.getString("symbol") shouldBe "$" + rate.getString("name") shouldBe "US Dollar" + rate.getFloat("rate") shouldBe 1.1f + } } } } - } @Test - fun `should return null if there is no data`() = runTest { - val response = httpClient.get("/api/forex") - .send() - .await() - .bodyAsJsonObject() + fun `should return null if there is no data`() = + runTest { + val response = + httpClient.get("/api/forex") + .send() + .coAwait() + .bodyAsJsonObject() - response.getJsonObject("data") shouldBe null - } + response.getJsonObject("data") shouldBe null + } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/DBCleanupTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/DBCleanupTest.kt index 5d56e23..135f6e8 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/DBCleanupTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/DBCleanupTest.kt @@ -2,26 +2,27 @@ package me.sujanpoudel.playdeals.api.health import io.kotest.matchers.shouldBe import io.vertx.core.Vertx -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.sqlclient.SqlClient import me.sujanpoudel.playdeals.IntegrationTest import me.sujanpoudel.playdeals.get import org.junit.jupiter.api.Test class DBCleanupTest(vertx: Vertx) : IntegrationTest(vertx) { - @Test - fun `Does cleanup`() = runTest { - val sqlClient = di.get() + fun `Does cleanup`() = + runTest { + val sqlClient = di.get() - sqlClient - .query(CLEAN_UP_DB_QUERY).execute() - .onFailure { it.printStackTrace() } + sqlClient + .query(CLEAN_UP_DB_QUERY).execute() + .onFailure { it.printStackTrace() } - val totalDeals = sqlClient.preparedQuery("""select count(*) from deal """) - .execute() - .await().first().getInteger(0) + val totalDeals = + sqlClient.preparedQuery("""select count(*) from deal """) + .execute() + .coAwait().first().getInteger(0) - totalDeals shouldBe 0 - } + totalDeals shouldBe 0 + } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/HealthTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/HealthTest.kt index 6ec411d..8141c81 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/HealthTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/HealthTest.kt @@ -2,28 +2,29 @@ package me.sujanpoudel.playdeals.api.health import io.kotest.matchers.shouldBe import io.vertx.core.Vertx -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import me.sujanpoudel.playdeals.IntegrationTest import org.junit.jupiter.api.Test class HealthTest(vertx: Vertx) : IntegrationTest(vertx) { - @Test - fun `GET liveness should return 200`() = runTest { - val response = httpClient.get(8888, "localhost", "/health/liveness").send().await() + fun `GET liveness should return 200`() = + runTest { + val response = httpClient.get(8888, "localhost", "/health/liveness").send().coAwait() - response.statusCode() shouldBe 200 - val responseJson = response.bodyAsJsonObject() - responseJson.getString("status") shouldBe "UP" - } + response.statusCode() shouldBe 200 + val responseJson = response.bodyAsJsonObject() + responseJson.getString("status") shouldBe "UP" + } @Test - fun `GET readiness should return 200`() = runTest { - val response = httpClient.get(8888, "localhost", "/health/readiness").send().await() + fun `GET readiness should return 200`() = + runTest { + val response = httpClient.get(8888, "localhost", "/health/readiness").send().coAwait() - val responseJson = response.bodyAsJsonObject() - response.statusCode() shouldBe 200 - responseJson.getString("status") shouldBe "UP" - responseJson.getString("outcome") shouldBe "UP" - } + val responseJson = response.bodyAsJsonObject() + response.statusCode() shouldBe 200 + responseJson.getString("status") shouldBe "UP" + responseJson.getString("outcome") shouldBe "UP" + } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/CachingDealRepositoryTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/CachingDealRepositoryTest.kt index 8977660..9eecc7d 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/CachingDealRepositoryTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/CachingDealRepositoryTest.kt @@ -17,7 +17,6 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class CachingDealRepositoryTest(vertx: Vertx) : IntegrationTest(vertx) { - private lateinit var persistentDealRepository: PersistentDealRepository private lateinit var repository: CachingDealRepository @@ -30,104 +29,108 @@ class CachingDealRepositoryTest(vertx: Vertx) : IntegrationTest(vertx) { } @Test - fun `should delegate new deal call`() = runTest { - val dealEntity = mockk() - val newDeal = mockk() + fun `should delegate new deal call`() = + runTest { + val dealEntity = mockk() + val newDeal = mockk() - coEvery { persistentDealRepository.upsert(any()) } returns dealEntity + coEvery { persistentDealRepository.upsert(any()) } returns dealEntity - repository.upsert(newDeal) shouldBe dealEntity + repository.upsert(newDeal) shouldBe dealEntity - coVerify(exactly = 1) { - persistentDealRepository.upsert(newDeal) + coVerify(exactly = 1) { + persistentDealRepository.upsert(newDeal) + } } - } @Test - fun `should cache newly added deal`() = runTest { - val dealEntity = mockk() - val newDeal = mockk() + fun `should cache newly added deal`() = + runTest { + val dealEntity = mockk() + val newDeal = mockk() - every { newDeal.id } returns "1" - every { dealEntity.id } returns newDeal.id - coEvery { persistentDealRepository.upsert(any()) } returns dealEntity - coEvery { persistentDealRepository.getAll(any(), any()) } returns emptyList() + every { newDeal.id } returns "1" + every { dealEntity.id } returns newDeal.id + coEvery { persistentDealRepository.upsert(any()) } returns dealEntity + coEvery { persistentDealRepository.getAll(any(), any()) } returns emptyList() - repository.upsert(newDeal) + repository.upsert(newDeal) - repository.getAll(0, Int.MAX_VALUE).shouldContainExactly(dealEntity) + repository.getAll(0, Int.MAX_VALUE).shouldContainExactly(dealEntity) - clearAllMocks() + clearAllMocks() - coVerify(exactly = 0) { - persistentDealRepository.getAll(any(), any()) + coVerify(exactly = 0) { + persistentDealRepository.getAll(any(), any()) + } } - } @Test - fun `should remove entry when deal is deleted`() = runTest { - val entity1 = mockk() - val entity2 = mockk() + fun `should remove entry when deal is deleted`() = + runTest { + val entity1 = mockk() + val entity2 = mockk() - val newDeal1 = mockk() - val newDeal2 = mockk() + val newDeal1 = mockk() + val newDeal2 = mockk() - every { newDeal1.id } returns "1" - every { newDeal2.id } returns "2" + every { newDeal1.id } returns "1" + every { newDeal2.id } returns "2" - every { entity1.id } returns newDeal1.id - every { entity2.id } returns newDeal2.id + every { entity1.id } returns newDeal1.id + every { entity2.id } returns newDeal2.id - coEvery { persistentDealRepository.upsert(newDeal1) } returns entity1 - coEvery { persistentDealRepository.upsert(newDeal2) } returns entity2 + coEvery { persistentDealRepository.upsert(newDeal1) } returns entity1 + coEvery { persistentDealRepository.upsert(newDeal2) } returns entity2 - coEvery { persistentDealRepository.delete("1") } returns entity1 - coEvery { persistentDealRepository.delete("2") } returns entity2 + coEvery { persistentDealRepository.delete("1") } returns entity1 + coEvery { persistentDealRepository.delete("2") } returns entity2 - coEvery { persistentDealRepository.getAll(any(), any()) } returns emptyList() + coEvery { persistentDealRepository.getAll(any(), any()) } returns emptyList() - repository.upsert(newDeal1) - repository.upsert(newDeal2) + repository.upsert(newDeal1) + repository.upsert(newDeal2) - repository.getAll(0, Int.MAX_VALUE).shouldContainExactly(entity1, entity2) + repository.getAll(0, Int.MAX_VALUE).shouldContainExactly(entity1, entity2) - repository.delete(entity2.id) + repository.delete(entity2.id) - repository.getAll(0, Int.MAX_VALUE).shouldContainExactly(entity1) - } + repository.getAll(0, Int.MAX_VALUE).shouldContainExactly(entity1) + } @Test - fun `should respect skip and take`() = runTest { - val entity1 = mockk() - val entity2 = mockk() - val entity3 = mockk() + fun `should respect skip and take`() = + runTest { + val entity1 = mockk() + val entity2 = mockk() + val entity3 = mockk() - val newDeal1 = mockk() - val newDeal2 = mockk() - val newDeal3 = mockk() + val newDeal1 = mockk() + val newDeal2 = mockk() + val newDeal3 = mockk() - every { newDeal1.id } returns "1" - every { newDeal2.id } returns "2" - every { newDeal3.id } returns "3" + every { newDeal1.id } returns "1" + every { newDeal2.id } returns "2" + every { newDeal3.id } returns "3" - every { entity1.id } returns newDeal1.id - every { entity2.id } returns newDeal2.id - every { entity3.id } returns newDeal3.id + every { entity1.id } returns newDeal1.id + every { entity2.id } returns newDeal2.id + every { entity3.id } returns newDeal3.id - coEvery { persistentDealRepository.upsert(newDeal1) } returns entity1 - coEvery { persistentDealRepository.upsert(newDeal2) } returns entity2 - coEvery { persistentDealRepository.upsert(newDeal3) } returns entity3 + coEvery { persistentDealRepository.upsert(newDeal1) } returns entity1 + coEvery { persistentDealRepository.upsert(newDeal2) } returns entity2 + coEvery { persistentDealRepository.upsert(newDeal3) } returns entity3 - coEvery { persistentDealRepository.getAll(any(), any()) } returns emptyList() + coEvery { persistentDealRepository.getAll(any(), any()) } returns emptyList() - repository.upsert(newDeal1) - repository.upsert(newDeal2) - repository.upsert(newDeal3) + repository.upsert(newDeal1) + repository.upsert(newDeal2) + repository.upsert(newDeal3) - println(repository.getAll(0, Int.MAX_VALUE)) + println(repository.getAll(0, Int.MAX_VALUE)) - repository.getAll(0, 1).shouldContainExactly(entity1) - repository.getAll(1, 1).shouldContainExactly(entity2) - repository.getAll(2, 1).shouldContainExactly(entity3) - } + repository.getAll(0, 1).shouldContainExactly(entity1) + repository.getAll(1, 1).shouldContainExactly(entity2) + repository.getAll(2, 1).shouldContainExactly(entity3) + } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentDealRepositoryTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentDealRepositoryTest.kt index 255d385..ad2f0be 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentDealRepositoryTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentDealRepositoryTest.kt @@ -5,7 +5,7 @@ import io.kotest.matchers.collections.shouldContainInOrder import io.kotest.matchers.equality.shouldBeEqualToComparingFields import io.kotest.matchers.shouldBe import io.vertx.core.Vertx -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.sqlclient.SqlClient import me.sujanpoudel.playdeals.Constants import me.sujanpoudel.playdeals.IntegrationTest @@ -19,120 +19,130 @@ import org.junit.jupiter.api.Test import java.time.OffsetDateTime class PersistentDealRepositoryTest(vertx: Vertx) : IntegrationTest(vertx) { - private val repository by lazy { di.get() } private val sqlClient by lazy { di.get() } - private val newDeal = NewDeal( - id = "id", - name = "name", - icon = "icon", - images = listOf("img0", "img1"), - normalPrice = 12f, - currentPrice = 12f, - currency = "USD", - storeUrl = "store_url", - category = "unknown", - downloads = "12+", - rating = "12", - offerExpiresIn = OffsetDateTime.now(), - type = DealType.ANDROID_APP, - source = Constants.DealSources.APP_DEAL_SUBREDDIT - ) + private val newDeal = + NewDeal( + id = "id", + name = "name", + icon = "icon", + images = listOf("img0", "img1"), + normalPrice = 12f, + currentPrice = 12f, + currency = "USD", + storeUrl = "store_url", + category = "unknown", + downloads = "12+", + rating = "12", + offerExpiresIn = OffsetDateTime.now(), + type = DealType.ANDROID_APP, + source = Constants.DealSources.APP_DEAL_SUBREDDIT, + ) @Test - fun `should create new app deal in db`() = runTest { - val appDeal = repository.upsert(newDeal) + fun `should create new app deal in db`() = + runTest { + val appDeal = repository.upsert(newDeal) - val appDealFromDb = sqlClient.preparedQuery(""" SELECT * from "deal" where id=$1""") - .exec(newDeal.id) - .await() - .first() - .asAppDeal() + val appDealFromDb = + sqlClient.preparedQuery(""" SELECT * from "deal" where id=$1""") + .exec(newDeal.id) + .coAwait() + .first() + .asAppDeal() - appDeal.shouldBeEqualToComparingFields(appDealFromDb) - } + appDeal.shouldBeEqualToComparingFields(appDealFromDb) + } @Test - fun `should perform update if item with id already exists`() = runTest { - repository.upsert(newDeal) + fun `should perform update if item with id already exists`() = + runTest { + repository.upsert(newDeal) - repository.upsert(newDeal.copy(name = "Updated Name")) + repository.upsert(newDeal.copy(name = "Updated Name")) - val appDealFromDb = sqlClient.preparedQuery(""" SELECT * from "deal" where id=$1""") - .exec(newDeal.id) - .await() - .first() - .asAppDeal() + val appDealFromDb = + sqlClient.preparedQuery(""" SELECT * from "deal" where id=$1""") + .exec(newDeal.id) + .coAwait() + .first() + .asAppDeal() - appDealFromDb.name.shouldBe("Updated Name") - } + appDealFromDb.name.shouldBe("Updated Name") + } @Test - fun `should delete app deal in db`() = runTest { - repository.upsert(newDeal) - repository.delete(newDeal.id) + fun `should delete app deal in db`() = + runTest { + repository.upsert(newDeal) + repository.delete(newDeal.id) - sqlClient.preparedQuery("""SELECT * from "deal" where id=$1""") - .exec(newDeal.id) - .await() - .rowCount() shouldBe 0 - } + sqlClient.preparedQuery("""SELECT * from "deal" where id=$1""") + .exec(newDeal.id) + .coAwait() + .rowCount() shouldBe 0 + } @Test - fun `should be able to get all app deals from db`() = runTest { - val deal0 = repository.upsert(newDeal) - val deal1 = repository.upsert(newDeal.copy(id = "id_1")) + fun `should be able to get all app deals from db`() = + runTest { + val deal0 = repository.upsert(newDeal) + val deal1 = repository.upsert(newDeal.copy(id = "id_1")) - val appDeal = repository.getAll(0, 100) + val appDeal = repository.getAll(0, 100) - appDeal shouldContainAll listOf(deal0, deal1) - } + appDeal shouldContainAll listOf(deal0, deal1) + } @Test - fun `should be able to get all app deals from db in order`() = runTest { - val deal0 = repository.upsert(newDeal) - val deal1 = repository.upsert(newDeal.copy(id = "id_1")) + fun `should be able to get all app deals from db in order`() = + runTest { + val deal0 = repository.upsert(newDeal) + val deal1 = repository.upsert(newDeal.copy(id = "id_1")) - val appDeal = repository.getAll(0, 100) + val appDeal = repository.getAll(0, 100) - appDeal shouldContainInOrder listOf(deal1, deal0) - } + appDeal shouldContainInOrder listOf(deal1, deal0) + } @Test - fun `should get deals added after a time`() = runTest { - val deal0 = repository.upsert(newDeal) + fun `should get deals added after a time`() = + runTest { + val deal0 = repository.upsert(newDeal) - val now = OffsetDateTime.now() + val now = OffsetDateTime.now() - val deal1 = repository.upsert(newDeal.copy(id = "id_1")) - val deal2 = repository.upsert(newDeal.copy(id = "id_2")) + val deal1 = repository.upsert(newDeal.copy(id = "id_1")) + val deal2 = repository.upsert(newDeal.copy(id = "id_2")) - repository.upsert(newDeal.copy(id = "id_2")) + repository.upsert(newDeal.copy(id = "id_2")) - val count = repository.getNewDeals(now) + val count = repository.getNewDeals(now) - count.shouldContainAll(deal1, deal2) - } + count.shouldContainAll(deal1, deal2) + } @Test - fun `getDealByPackageName should return correct deal by packageName`() = runTest { - val deal0 = repository.upsert(newDeal) + fun `getDealByPackageName should return correct deal by packageName`() = + runTest { + val deal0 = repository.upsert(newDeal) - repository.upsert(newDeal.copy(id = "id_1")) - repository.upsert(newDeal.copy(id = "id_2")) + repository.upsert(newDeal.copy(id = "id_1")) + repository.upsert(newDeal.copy(id = "id_2")) - val deal01 = repository.getDealByPackageName(deal0.id) + val deal01 = repository.getDealByPackageName(deal0.id) - deal0 shouldBe deal01 - } + deal0 shouldBe deal01 + } @Test - fun `getDealByPackageName should return null when there is no deal`() = runTest { - val deal0 = repository.upsert(newDeal) + fun `getDealByPackageName should return null when there is no deal`() = + runTest { + val deal0 = repository.upsert(newDeal) - val deal1 = repository.getDealByPackageName("id_3") + val deal1 = repository.getDealByPackageName("id_3") - deal1 shouldBe null - } + deal1 shouldBe null + } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentKeyValueRepositoryTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentKeyValueRepositoryTest.kt index 74f1802..26c6766 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentKeyValueRepositoryTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentKeyValueRepositoryTest.kt @@ -2,7 +2,7 @@ package me.sujanpoudel.playdeals.repositories import io.kotest.matchers.shouldBe import io.vertx.core.Vertx -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.sqlclient.SqlClient import me.sujanpoudel.playdeals.IntegrationTest import me.sujanpoudel.playdeals.common.exec @@ -12,48 +12,52 @@ import org.junit.jupiter.api.Test import java.time.OffsetDateTime class PersistentKeyValueRepositoryTest(vertx: Vertx) : IntegrationTest(vertx) { - private val repository by lazy { di.get() } private val sqlClient by lazy { di.get() } @Test - fun `should create new entry on db`() = runTest { - val value = repository.set(KEY, "test") + fun `should create new entry on db`() = + runTest { + val value = repository.set(KEY, "test") - val valueFromDb = sqlClient.preparedQuery(""" SELECT * from "key_value_store" where key=$1""") - .exec(KEY) - .await() - .first() - .getString("value") + val valueFromDb = + sqlClient.preparedQuery(""" SELECT * from "key_value_store" where key=$1""") + .exec(KEY) + .coAwait() + .first() + .getString("value") - value shouldBe valueFromDb - } + value shouldBe valueFromDb + } @Test - fun `should perform update if item with id already exists`() = runTest { - repository.set(KEY, "test") + fun `should perform update if item with id already exists`() = + runTest { + repository.set(KEY, "test") - val updated = repository.set(KEY, "test1") + val updated = repository.set(KEY, "test1") - val fromDb = sqlClient.preparedQuery(""" SELECT * from "key_value_store" where key=$1""") - .exec(KEY) - .await() - .first() - .value() + val fromDb = + sqlClient.preparedQuery(""" SELECT * from "key_value_store" where key=$1""") + .exec(KEY) + .coAwait() + .first() + .value() - fromDb shouldBe updated - } + fromDb shouldBe updated + } @Test - fun `should be able to serialize unknown types`() = runTest { - val value = OffsetDateTime.now() + fun `should be able to serialize unknown types`() = + runTest { + val value = OffsetDateTime.now() - repository.set(KEY, value.toString()) + repository.set(KEY, value.toString()) - val fromDb = repository.get(KEY).let(OffsetDateTime::parse) + val fromDb = repository.get(KEY).let(OffsetDateTime::parse) - fromDb shouldBe value - } + fromDb shouldBe value + } companion object { const val KEY = "test_key" diff --git a/build.gradle.kts b/build.gradle.kts index be67f8c..c4aaf24 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,23 +1,16 @@ @file:Suppress("UnstableApiUsage") import org.gradle.api.tasks.testing.logging.TestLogEvent -import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformJvmPlugin -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jlleitschuh.gradle.ktlint.KtlintPlugin plugins { - kotlin("jvm") version Versions.KOTLIN - id("org.jlleitschuh.gradle.ktlint") version "11.5.0" -} - -buildscript { - repositories { - mavenCentral() - } + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.shadow) apply false + alias(libs.plugins.jib) apply false + alias(libs.plugins.ktlint) } allprojects { - apply() apply() apply() @@ -25,18 +18,6 @@ allprojects { mavenCentral() } - val compileKotlin by tasks.getting(KotlinCompile::class) { - kotlinOptions { - jvmTarget = "17" - } - } - - val compileTestKotlin by tasks.getting(KotlinCompile::class) { - kotlinOptions { - jvmTarget = "17" - } - } - task("preCommitHook") { dependsOn(tasks.ktlintCheck) } @@ -45,11 +26,12 @@ allprojects { tasks.withType { useJUnitPlatform() testLogging { - events = setOf( - TestLogEvent.PASSED, - TestLogEvent.SKIPPED, - TestLogEvent.FAILED - ) + events = + setOf( + TestLogEvent.PASSED, + TestLogEvent.SKIPPED, + TestLogEvent.FAILED, + ) } } @@ -62,6 +44,6 @@ task("installPreCommitHook") { } } -tasks.withType() { +tasks.withType { dependsOn("installPreCommitHook") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..69683bd --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,88 @@ +[versions] + +## artifacts +android-compileSdk = "34" +android-minSdk = "29" +android-targetSdk = "34" + +## plugins +kotlin = "2.0.0" +shadow = "7.1.2" +jib = "3.4.3" +klint-plugin = "12.0.3" + +## Libraries +kotlinx-coroutines-core = "1.8.1" +vertx = "4.5.8" +jackson = "2.17.1" +flyway = "10.14.0" +postgresql = "42.7.3" +ongress-scram = "2.1" +firebase-admin = "9.3.0" + +kotlin-result = "2.0.0" +kodein = "7.22.0" +slfj4 = "2.0.13" +jvm-logger = "3.0.5" + +job-runner = "7.2.0" + +ktlint = "1.2.1" + +## testing +junit-jupiter = "5.10.2" +kotest = "5.9.1" +test-container = "1.19.8" +mockk = "1.13.11" + +[libraries] +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" } +kotlinx-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } + +vertx-depchain = { module = "io.vertx:vertx-stack-depchain", version.ref = "vertx" } +vertx-core = { module = "io.vertx:vertx-core" } +vertx-web = { module = "io.vertx:vertx-web" } +vertx-pgClient = { module = "io.vertx:vertx-pg-client" } +vertx-coroutines = { module = "io.vertx:vertx-lang-kotlin-coroutines" } +vertx-kotlin = { module = "io.vertx:vertx-lang-kotlin" } +vertx-healthCheck = { module = "io.vertx:vertx-health-check" } +vertx-webClient = { module = "io.vertx:vertx-web-client" } +vertx-junit5 = { module = "io.vertx:vertx-junit5" } + +kotlinResult = { module = "com.michael-bull.kotlin-result:kotlin-result", version.ref = "kotlin-result" } + +flyway-core = { module = "org.flywaydb:flyway-core", version.ref = "flyway" } +flyway-postgresql = { module = "org.flywaydb:flyway-database-postgresql", version.ref = "flyway" } +postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" } +scramOngressClient = { module = "com.ongres.scram:client", version.ref = "ongress-scram" } + +slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slfj4" } +slf4j-simpe = { module = "org.slf4j:slf4j-simple", version.ref = "slfj4" } +kotlinLoggingJvm = { module = "io.github.microutils:kotlin-logging-jvm", version.ref = "jvm-logger" } + +jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } +jackson-moduleKotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } +jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" } + +kodein = { module = "org.kodein.di:kodein-di", version.ref = "kodein" } + +jobrunr = { module = "org.jobrunr:jobrunr", version.ref = "job-runner" } +jobrunr-kotlin = { module = "org.jobrunr:jobrunr-kotlin-1.8-support", version.ref = "job-runner" } + +firebaseAdmin = { module = "com.google.firebase:firebase-admin", version.ref = "firebase-admin" } + +## Testing +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } +kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } + +testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "test-container" } +testcontainers-junit = { module = "org.testcontainers:junit-jupiter", version.ref = "test-container" } +testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "test-container" } + +[plugins] +kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "klint-plugin" } +shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } +jib = { id = "com.google.cloud.tools.jib", version.ref = "jib" } + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 84a0b92..48c0a02 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index c9cc5b5..27da8f1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,11 @@ rootProject.name = "deals" +dependencyResolutionManagement { + repositories { + google() + gradlePluginPortal() + mavenCentral() + } +} + include("backend") From bd440db45533e5001fd24fcd6afdbfe387db2751 Mon Sep 17 00:00:00 2001 From: psuzn Date: Fri, 7 Jun 2024 14:20:31 +0545 Subject: [PATCH 2/4] using non depreciated apis --- .editorconfig | 6 +- .../me/sujanpoudel/playdeals/DIConfigurer.kt | 18 +- .../kotlin/me/sujanpoudel/playdeals/Main.kt | 28 ++- .../me/sujanpoudel/playdeals/api/ForexRate.kt | 26 ++- .../me/sujanpoudel/playdeals/api/Health.kt | 44 +++-- .../me/sujanpoudel/playdeals/api/deals/Api.kt | 46 +++-- .../me/sujanpoudel/playdeals/common/Conf.kt | 114 ++++++------- .../me/sujanpoudel/playdeals/common/Enums.kt | 11 +- .../sujanpoudel/playdeals/common/Metrices.kt | 5 +- .../sujanpoudel/playdeals/common/Routing.kt | 12 +- .../playdeals/domain/AndroidAppDetail.kt | 33 ++-- .../playdeals/domain/entities/DealEntity.kt | 9 +- .../jobs/AndroidAppDetailScrapper.kt | 52 +++--- .../jobs/AndroidAppExpiryCheckScheduler.kt | 34 ++-- .../playdeals/jobs/CoJobRequestHandler.kt | 7 +- .../playdeals/jobs/DealSummarizer.kt | 76 ++++----- .../playdeals/jobs/ForexFetcher.kt | 35 ++-- .../playdeals/jobs/RedditPostsScrapper.kt | 66 ++++--- .../playdeals/repositories/DealRepository.kt | 5 +- .../repositories/KeyValuesRepository.kt | 5 +- .../caching/CachingDealRepository.kt | 5 +- .../persistent/PersistentDealRepository.kt | 5 +- .../PersistentKeyValuesRepository.kt | 5 +- .../playdeals/services/MessagingService.kt | 41 ++--- .../playdeals/usecases/DBHealthUseCase.kt | 15 +- .../playdeals/usecases/UseCaseEngine.kt | 21 ++- .../sujanpoudel/playdeals/IntegrationTest.kt | 33 ++-- .../playdeals/api/deals/GetDealsApiTest.kt | 161 +++++++++--------- .../playdeals/api/deals/NewDealApiTest.kt | 128 +++++++------- .../playdeals/api/forex/GetForexApiTest.kt | 87 +++++----- .../playdeals/api/health/DBCleanupTest.kt | 23 ++- .../playdeals/api/health/HealthTest.kt | 28 ++- .../repositories/CachingDealRepositoryTest.kt | 136 +++++++-------- .../PersistentDealRepositoryTest.kt | 132 +++++++------- .../PersistentKeyValueRepositoryTest.kt | 57 +++---- build.gradle.kts | 31 +++- 36 files changed, 730 insertions(+), 810 deletions(-) diff --git a/.editorconfig b/.editorconfig index 215eb9e..1d71e74 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,4 +17,8 @@ ij_kotlin_name_count_to_use_star_import_for_members = 99999 ij_java_names_count_to_use_import_on_demand = 99999 ktlint_code_style = ktlint_official -ktlint_standard_filename = disabled \ No newline at end of file +ktlint_standard_filename = disabled +ktlint_function_signature_body_expression_wrapping = default +ktlint_standard_multiline-expression-wrapping = disabled +ktlint_standard_string-template-indent = disabled +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 5 \ No newline at end of file diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt index 61b634f..a17eb24 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt @@ -12,8 +12,8 @@ import com.google.firebase.messaging.FirebaseMessaging import io.vertx.core.Vertx import io.vertx.core.eventbus.DeliveryOptions import io.vertx.core.json.jackson.DatabindCodec +import io.vertx.pgclient.PgBuilder import io.vertx.pgclient.PgConnectOptions -import io.vertx.pgclient.PgPool import io.vertx.sqlclient.PoolOptions import me.sujanpoudel.playdeals.api.ApiVerticle import me.sujanpoudel.playdeals.jobs.AndroidAppExpiryCheckScheduler @@ -50,10 +50,7 @@ import java.time.Duration inline fun DI.get(tag: String? = null) = direct.instance(tag) -fun configureDI( - vertx: Vertx, - conf: Conf, -) = DI { +fun configureDI(vertx: Vertx, conf: Conf) = DI { bindSingleton { conf } bindSingleton { ApiVerticle(di = this) } @@ -93,11 +90,12 @@ fun configureDI( } bindSingleton { - PgPool.client(vertx, instance(), PoolOptions().setMaxSize(conf.db.poolSize)) - } - - bindSingleton { - PgPool.pool(vertx, instance(), PoolOptions()) + PgBuilder + .client() + .using(vertx) + .connectingTo(instance()) + .with(PoolOptions().setMaxSize(conf.db.poolSize)) + .build() } bindSingleton { diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/Main.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/Main.kt index 2bf4f45..198339d 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/Main.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/Main.kt @@ -12,22 +12,20 @@ import org.kodein.di.instance import kotlin.system.exitProcess private val vertx = Vertx.vertx() -val configuration = - buildConf(System.getenv()).getOrThrow { - (it as BootstrapException).violations.forEach(::println) - exitProcess(-1) - } +val configuration = buildConf(System.getenv()).getOrThrow { + (it as BootstrapException).violations.forEach(::println) + exitProcess(-1) +} val primaryDI = configureDI(vertx, configuration) -fun main(): Unit = - runBlocking { - primaryDI.direct.instance() +fun main(): Unit = runBlocking { + primaryDI.direct.instance() - vertx.deployVerticle(primaryDI.direct.instance()) - .onSuccess { logger.infoNotify("Deployed MainVerticle : $it") } - .onFailure { - logger.error(it) { "Error deploying main verticle" } - vertx.close() - }.coAwait() - } + vertx.deployVerticle(primaryDI.direct.instance()) + .onSuccess { logger.infoNotify("Deployed MainVerticle : $it") } + .onFailure { + logger.error(it) { "Error deploying main verticle" } + vertx.close() + }.coAwait() +} diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt index 4648e24..c61bf41 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt @@ -8,19 +8,15 @@ import me.sujanpoudel.playdeals.usecases.executeUseCase import org.kodein.di.DirectDI import org.kodein.di.instance -fun forexRateApi( - di: DirectDI, - vertx: io.vertx.core.Vertx, -): Router = - Router.router(vertx).apply { - get() - .coHandler { ctx -> - ctx.executeUseCase( - useCase = di.instance(), - toContext = { }, - toInput = { }, - ) { - ctx.json(jsonResponse(data = it)) - } +fun forexRateApi(di: DirectDI, vertx: io.vertx.core.Vertx): Router = Router.router(vertx).apply { + get() + .coHandler { ctx -> + ctx.executeUseCase( + useCase = di.instance(), + toContext = { }, + toInput = { }, + ) { + ctx.json(jsonResponse(data = it)) } - } + } +} diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/Health.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/Health.kt index 765fc1d..375195f 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/Health.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/Health.kt @@ -12,34 +12,30 @@ import me.sujanpoudel.playdeals.usecases.DBHealthUseCase import org.kodein.di.DirectDI import org.kodein.di.instance -fun healthApi( - di: DirectDI, - vertx: Vertx, -): Router = - Router.router(vertx).apply { - val dbHealthChecker = di.instance() +fun healthApi(di: DirectDI, vertx: Vertx): Router = Router.router(vertx).apply { + val dbHealthChecker = di.instance() - val livenessHandler = HealthCheckHandler.create(vertx) - val readinessHandler = HealthCheckHandler.create(vertx) + val livenessHandler = HealthCheckHandler.create(vertx) + val readinessHandler = HealthCheckHandler.create(vertx) - livenessHandler.register("status") { promise -> - promise.complete(Status.OK()) - } + livenessHandler.register("status") { promise -> + promise.complete(Status.OK()) + } - readinessHandler.register("status") { promise -> - promise.complete(Status.OK()) - } + readinessHandler.register("status") { promise -> + promise.complete(Status.OK()) + } - readinessHandler.register("postgres") { promise -> - CoroutineScope(Dispatchers.IO).launch(vertx.dispatcher()) { - if (dbHealthChecker.execute(Unit)) { - promise.complete(Status.OK()) - } else { - promise.complete(Status.KO()) - } + readinessHandler.register("postgres") { promise -> + CoroutineScope(Dispatchers.IO).launch(vertx.dispatcher()) { + if (dbHealthChecker.execute(Unit)) { + promise.complete(Status.OK()) + } else { + promise.complete(Status.KO()) } } - - get("/liveness").handler(livenessHandler) - get("/readiness").handler(readinessHandler) } + + get("/liveness").handler(livenessHandler) + get("/readiness").handler(readinessHandler) +} diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/Api.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/Api.kt index 1d44008..b44a586 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/Api.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/deals/Api.kt @@ -12,31 +12,27 @@ import me.sujanpoudel.playdeals.usecases.executeUseCase import org.kodein.di.DirectDI import org.kodein.di.instance -fun appDealsApi( - di: DirectDI, - vertx: Vertx, -): Router = - Router.router(vertx).apply { - get() - .coHandler { ctx -> - ctx.executeUseCase( - useCase = di.instance(), - toContext = { GetDealsContext(ctx.request().params()) }, - toInput = { GetDealsUseCase.Input(it.skip, it.take) }, - ) { - ctx.json(jsonResponse(data = it)) - } +fun appDealsApi(di: DirectDI, vertx: Vertx): Router = Router.router(vertx).apply { + get() + .coHandler { ctx -> + ctx.executeUseCase( + useCase = di.instance(), + toContext = { GetDealsContext(ctx.request().params()) }, + toInput = { GetDealsUseCase.Input(it.skip, it.take) }, + ) { + ctx.json(jsonResponse(data = it)) } + } - post() - .consumes(ContentTypes.JSON) - .coHandler { ctx -> - ctx.executeUseCase( - useCase = di.instance(), - toContext = { NewDealContext(ctx.request().body().coAwait().toJsonObject()) }, - toInput = { it.packageName }, - ) { - ctx.json(jsonResponse("App added for queue")) - } + post() + .consumes(ContentTypes.JSON) + .coHandler { ctx -> + ctx.executeUseCase( + useCase = di.instance(), + toContext = { NewDealContext(ctx.request().body().coAwait().toJsonObject()) }, + toInput = { it.packageName }, + ) { + ctx.json(jsonResponse("App added for queue")) } - } + } +} diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt index 100d678..089cc2d 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt @@ -9,72 +9,66 @@ import java.util.Base64 class BootstrapException(val violations: List) : RuntimeException() -fun buildConf(envs: Map) = - com.github.michaelbull.result.runCatching { - val violations = mutableListOf() +fun buildConf(envs: Map) = com.github.michaelbull.result.runCatching { + val violations = mutableListOf() - @Suppress("UNCHECKED_CAST") - fun env( - envVarName: String, - default: String? = null, - converter: (String) -> T? = { it as? T }, - ): T? = - ( - envs[envVarName] ?: default ?: run { - violations += "No '$envVarName' env var defined!".also { logger.error { it } } - null - } - )?.let(converter) ?: run { - violations += "Invalid '$envVarName'" - null - } + @Suppress("UNCHECKED_CAST") + fun env(envVarName: String, default: String? = null, converter: (String) -> T? = { it as? T }): T? = ( + envs[envVarName] ?: default ?: run { + violations += "No '$envVarName' env var defined!".also { logger.error { it } } + null + } + )?.let(converter) ?: run { + violations += "Invalid '$envVarName'" + null + } - val environment = env("ENV", Environment.PRODUCTION.name) { it.asEnumOrNull() } + val environment = env("ENV", Environment.PRODUCTION.name) { it.asEnumOrNull() } - val appPort = env("APP_PORT", "8888") { it.toIntOrNull() } - val cors = env("CORS", ".*.") + val appPort = env("APP_PORT", "8888") { it.toIntOrNull() } + val cors = env("CORS", ".*.") - val dbPort = env("DB_PORT", "5432") { it.toIntOrNull() } - val dbName = env("DB_NAME", "play_deals") - val dbPoolSize = env("DB_POOL_SIZE", "5") { it.toIntOrNull() } - val dbHost = env("DB_HOST") - val dbUsername = env("DB_USERNAME") - val dbPassword = env("DB_PASSWORD", "password") + val dbPort = env("DB_PORT", "5432") { it.toIntOrNull() } + val dbName = env("DB_NAME", "play_deals") + val dbPoolSize = env("DB_POOL_SIZE", "5") { it.toIntOrNull() } + val dbHost = env("DB_HOST") + val dbUsername = env("DB_USERNAME") + val dbPassword = env("DB_PASSWORD", "password") - val dashboardEnabled = env("DASHBOARD", "true") { it.toBooleanStrictOrNull() } - val dashboardUser = env("DASHBOARD_USER", "admin") - val dashboardPassword = env("DASHBOARD_PASS", "admin") + val dashboardEnabled = env("DASHBOARD", "true") { it.toBooleanStrictOrNull() } + val dashboardUser = env("DASHBOARD_USER", "admin") + val dashboardPassword = env("DASHBOARD_PASS", "admin") - val firebaseAuthCredential = - env("FIREBASE_ADMIN_AUTH_CREDENTIALS") { - Base64.getDecoder().decode(it).decodeToString() - } + val firebaseAuthCredential = + env("FIREBASE_ADMIN_AUTH_CREDENTIALS") { + Base64.getDecoder().decode(it).decodeToString() + } - val forexApiKey = env("FOREX_API_KEY") + val forexApiKey = env("FOREX_API_KEY") - if (violations.isNotEmpty()) { - throw BootstrapException(violations) - } else { - Conf( - api = Conf.Api(appPort!!, cors = cors!!), - environment = environment!!, - db = - Conf.DB( - host = dbHost!!, - port = dbPort!!, - name = dbName!!, - username = dbUsername!!, - password = dbPassword!!, - poolSize = dbPoolSize!!, - ), - backgroundTask = - Conf.BackgroundTask( - dashboardEnabled = dashboardEnabled!!, - dashboardUserName = dashboardUser!!, - dashboardPassword = dashboardPassword!!, - ), - firebaseAuthCredential = firebaseAuthCredential!!, - forexApiKey = forexApiKey!!, - ) - } + if (violations.isNotEmpty()) { + throw BootstrapException(violations) + } else { + Conf( + api = Conf.Api(appPort!!, cors = cors!!), + environment = environment!!, + db = + Conf.DB( + host = dbHost!!, + port = dbPort!!, + name = dbName!!, + username = dbUsername!!, + password = dbPassword!!, + poolSize = dbPoolSize!!, + ), + backgroundTask = + Conf.BackgroundTask( + dashboardEnabled = dashboardEnabled!!, + dashboardUserName = dashboardUser!!, + dashboardPassword = dashboardPassword!!, + ), + firebaseAuthCredential = firebaseAuthCredential!!, + forexApiKey = forexApiKey!!, + ) } +} diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Enums.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Enums.kt index da3680d..c52cd92 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Enums.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Enums.kt @@ -2,9 +2,8 @@ package me.sujanpoudel.playdeals.common inline fun > String.asEnum() = enumValueOf(this) -inline fun > String.asEnumOrNull() = - try { - asEnum() - } catch (e: Exception) { - null - } +inline fun > String.asEnumOrNull() = try { + asEnum() +} catch (e: Exception) { + null +} diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Metrices.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Metrices.kt index 1730c63..3a4f9bf 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Metrices.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Metrices.kt @@ -4,10 +4,7 @@ import me.sujanpoudel.playdeals.logger import kotlin.time.DurationUnit import kotlin.time.measureTimedValue -inline fun loggingExecutionTime( - message: String, - action: () -> T, -): T { +inline fun loggingExecutionTime(message: String, action: () -> T): T { val timedValue = measureTimedValue { action.invoke() diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Routing.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Routing.kt index c3f8f81..3951e3d 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Routing.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Routing.kt @@ -27,14 +27,10 @@ fun Route.coHandler(fn: suspend (RoutingContext) -> Unit): Route { fun HttpServerResponse.contentType(value: String): HttpServerResponse = putHeader("Content-Type", value) -fun jsonResponse( - message: String = "Success", - data: T? = null, -): JsonObject = - jsonObjectOf( - "message" to message, - "data" to data, - ) +fun jsonResponse(message: String = "Success", data: T? = null): JsonObject = jsonObjectOf( + "message" to message, + "data" to data, +) const val UNKNOWN_ERROR_MESSAGE = "Something went wrong" diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/AndroidAppDetail.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/AndroidAppDetail.kt index 9a7b196..eefab6a 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/AndroidAppDetail.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/AndroidAppDetail.kt @@ -19,20 +19,19 @@ data class AndroidAppDetail( val source: String, ) -fun AndroidAppDetail.asNewDeal() = - NewDeal( - id = id, - name = name, - icon = icon, - images = images, - normalPrice = normalPrice, - currentPrice = currentPrice!!, - currency = currency, - storeUrl = storeUrl, - category = category, - downloads = downloads, - rating = rating, - offerExpiresIn = offerExpiresIn!!, - type = DealType.ANDROID_APP, - source = source, - ) +fun AndroidAppDetail.asNewDeal() = NewDeal( + id = id, + name = name, + icon = icon, + images = images, + normalPrice = normalPrice, + currentPrice = currentPrice!!, + currency = currency, + storeUrl = storeUrl, + category = category, + downloads = downloads, + rating = rating, + offerExpiresIn = offerExpiresIn!!, + type = DealType.ANDROID_APP, + source = source, +) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/DealEntity.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/DealEntity.kt index 4db6a33..20b5411 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/DealEntity.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/entities/DealEntity.kt @@ -29,11 +29,10 @@ data class DealEntity( val updatedAt: OffsetDateTime, ) -private fun String.asCurrencySymbol() = - when (this) { - "USD" -> "$" - else -> this - } +private fun String.asCurrencySymbol() = when (this) { + "USD" -> "$" + else -> this +} private fun Float.formatAsPrice(): String { val int = toInt() diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppDetailScrapper.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppDetailScrapper.kt index 3484232..3a98016 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppDetailScrapper.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppDetailScrapper.kt @@ -51,36 +51,35 @@ class AppDetailScrapper( WebClient.create(jobsVerticle.vertx, WebClientOptions().setDefaultHost("play.google.com")) } - override suspend fun handleRequest(jobRequest: Request): Unit = - loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest ${jobRequest.packageName}", - ) { - val packageName = jobRequest.packageName - - val app = - loggingExecutionTime("$SIMPLE_NAME:: scrapping app details $packageName") { - getAppDetail(packageName) - } + override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( + "$SIMPLE_NAME:: handleRequest ${jobRequest.packageName}", + ) { + val packageName = jobRequest.packageName + + val app = + loggingExecutionTime("$SIMPLE_NAME:: scrapping app details $packageName") { + getAppDetail(packageName) + } - when { - app.normalPrice == 0f -> { - logger.infoNotify("App $packageName(${app.name}) doesn't have any price") - repository.delete(packageName) - } + when { + app.normalPrice == 0f -> { + logger.infoNotify("App $packageName(${app.name}) doesn't have any price") + repository.delete(packageName) + } - app.normalPrice == app.currentPrice -> { - logger.infoNotify("App $packageName(${app.name}) deals has been expired") - repository.delete(packageName) - } + app.normalPrice == app.currentPrice -> { + logger.infoNotify("App $packageName(${app.name}) deals has been expired") + repository.delete(packageName) + } - (app.currentPrice ?: 0f) < app.normalPrice -> { - logger.info("Found deal for $packageName(${app.name}) ${app.currentPrice} ${app.currency}(${app.normalPrice} ${app.currency})") - repository.upsert(app.asNewDeal()).also { - messagingService.sendMessageForNewDeal(it) - } + (app.currentPrice ?: 0f) < app.normalPrice -> { + logger.info("Found deal for $packageName(${app.name}) ${app.currentPrice} ${app.currency}(${app.normalPrice} ${app.currency})") + repository.upsert(app.asNewDeal()).also { + messagingService.sendMessageForNewDeal(it) } } } + } private suspend fun getAppDetail(packageName: String): AndroidAppDetail { val response = @@ -153,10 +152,7 @@ class AppDetailScrapper( return getValue(getJsonArray(value.root), value.path.toTypedArray()) as T } - fun getValue( - jsonObject: JsonArray, - path: Array, - ): Any { + fun getValue(jsonObject: JsonArray, path: Array): Any { return if (path.size == 1) { jsonObject.getValue(path.first()) } else { diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppExpiryCheckScheduler.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppExpiryCheckScheduler.kt index c374dda..ccf6097 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppExpiryCheckScheduler.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/AndroidAppExpiryCheckScheduler.kt @@ -19,20 +19,19 @@ class AndroidAppExpiryCheckScheduler( private val requestScheduler: JobRequestScheduler, private val storageProvider: StorageProvider, ) : CoJobRequestHandler() { - override suspend fun handleRequest(jobRequest: Request): Unit = - loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest", - ) { - val apps = - repository.getPotentiallyExpiredDeals().stream() - .map { AppDetailScrapper.Request(it.id) } + override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( + "$SIMPLE_NAME:: handleRequest", + ) { + val apps = + repository.getPotentiallyExpiredDeals().stream() + .map { AppDetailScrapper.Request(it.id) } - requestScheduler.enqueue(apps) + requestScheduler.enqueue(apps) - val lastUpdatedTime = Instant.now().minus(1, ChronoUnit.HOURS) - val jobs = storageProvider.deleteJobsPermanently(StateName.FAILED, lastUpdatedTime) - logger.info("deleted FAILED `$jobs`") - } + val lastUpdatedTime = Instant.now().minus(1, ChronoUnit.HOURS) + val jobs = storageProvider.deleteJobsPermanently(StateName.FAILED, lastUpdatedTime) + logger.info("deleted FAILED `$jobs`") + } class Request private constructor() : JobRequest { override fun getJobRequestHandler() = AndroidAppExpiryCheckScheduler::class.java @@ -40,12 +39,11 @@ class AndroidAppExpiryCheckScheduler( companion object { private val JOB_ID: UUID = UUID.nameUUIDFromBytes("AppExpiryCheckScheduler".toByteArray()) - operator fun invoke(): RecurringJobBuilder = - RecurringJobBuilder.aRecurringJob() - .withJobRequest(Request()) - .withName("App Expiry Checker") - .withId(JOB_ID.toString()) - .withDuration(Duration.ofHours(6)) + operator fun invoke(): RecurringJobBuilder = RecurringJobBuilder.aRecurringJob() + .withJobRequest(Request()) + .withName("App Expiry Checker") + .withId(JOB_ID.toString()) + .withDuration(Duration.ofHours(6)) } } } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/CoJobRequestHandler.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/CoJobRequestHandler.kt index 0175e3e..ac0608b 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/CoJobRequestHandler.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/CoJobRequestHandler.kt @@ -5,10 +5,9 @@ import org.jobrunr.jobs.lambdas.JobRequest import org.jobrunr.jobs.lambdas.JobRequestHandler abstract class CoJobRequestHandler : JobRequestHandler { - override fun run(jobRequest: T): Unit = - runBlocking { - handleRequest(jobRequest) - } + override fun run(jobRequest: T): Unit = runBlocking { + handleRequest(jobRequest) + } abstract suspend fun handleRequest(jobRequest: T) } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/DealSummarizer.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/DealSummarizer.kt index ac9b57e..6516ece 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/DealSummarizer.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/DealSummarizer.kt @@ -26,56 +26,54 @@ class DealSummarizer( private val keyValueRepository by instance() private val messagingService by instance() - override suspend fun handleRequest(jobRequest: Request): Unit = - loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest", - ) { - val lastTimestamp = - keyValueRepository.get(LAST_SUMMARY_TIMESTAMP)?.let(OffsetDateTime::parse) - ?: OffsetDateTime.now() + override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( + "$SIMPLE_NAME:: handleRequest", + ) { + val lastTimestamp = + keyValueRepository.get(LAST_SUMMARY_TIMESTAMP)?.let(OffsetDateTime::parse) + ?: OffsetDateTime.now() - val deals = dealRepository.getNewDeals(lastTimestamp) + val deals = dealRepository.getNewDeals(lastTimestamp) - if (deals.isNotEmpty()) { - val maxCount = 6 - val dealsDescription = - deals - .take(maxCount) - .mapIndexed { index, deal -> - "${index + 1}. ${deal.name} was ${deal.formattedNormalPrice()} is now ${deal.formattedCurrentPrice()}" - }.joinToString("\n") + if (deals.isNotEmpty()) { + val maxCount = 6 + val dealsDescription = + deals + .take(maxCount) + .mapIndexed { index, deal -> + "${index + 1}. ${deal.name} was ${deal.formattedNormalPrice()} is now ${deal.formattedCurrentPrice()}" + }.joinToString("\n") - messagingService.sendMessageToTopic( - topic = Constants.PushNotificationTopic.DEALS_SUMMARY, - title = "New ${deals.size} app deals are found since yesterday", - body = - if (deals.size > maxCount) { - "$dealsDescription\n\n +${deals.size - maxCount} more..." - } else { - dealsDescription - }, - ) - } else { - logger.infoNotify("$SIMPLE_NAME:: haven't got any deals since $lastTimestamp") - } - - keyValueRepository.set(LAST_SUMMARY_TIMESTAMP, OffsetDateTime.now().toString()) + messagingService.sendMessageToTopic( + topic = Constants.PushNotificationTopic.DEALS_SUMMARY, + title = "New ${deals.size} app deals are found since yesterday", + body = + if (deals.size > maxCount) { + "$dealsDescription\n\n +${deals.size - maxCount} more..." + } else { + dealsDescription + }, + ) + } else { + logger.infoNotify("$SIMPLE_NAME:: haven't got any deals since $lastTimestamp") } + keyValueRepository.set(LAST_SUMMARY_TIMESTAMP, OffsetDateTime.now().toString()) + } + class Request private constructor() : JobRequest { override fun getJobRequestHandler() = DealSummarizer::class.java companion object { private val JOB_ID: UUID = UUID.nameUUIDFromBytes("deal-summarizer".toByteArray()) - operator fun invoke(): RecurringJobBuilder = - RecurringJobBuilder.aRecurringJob() - .withJobRequest(Request()) - .withCron(Cron.daily(16)) - .withAmountOfRetries(2) - .withLabels("Deal Summarizer") - .withName("Deal Summarizer") - .withId(JOB_ID.toString()) + operator fun invoke(): RecurringJobBuilder = RecurringJobBuilder.aRecurringJob() + .withJobRequest(Request()) + .withCron(Cron.daily(16)) + .withAmountOfRetries(2) + .withLabels("Deal Summarizer") + .withName("Deal Summarizer") + .withId(JOB_ID.toString()) } } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt index 66e47d4..0f54f5b 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt @@ -51,14 +51,13 @@ class ForexFetcher( private val repository by instance() - override suspend fun handleRequest(jobRequest: Request): Unit = - loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest", - ) { - val rates = getForexRates() - logger.info("got ${rates.rates.size} forex rate") - repository.saveForexRate(rates) - } + override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( + "$SIMPLE_NAME:: handleRequest", + ) { + val rates = getForexRates() + logger.info("got ${rates.rates.size} forex rate") + repository.saveForexRate(rates) + } private suspend fun getForexRates(): ForexRate { val currencies = loadCurrencies() @@ -97,13 +96,12 @@ class ForexFetcher( companion object { private val JOB_ID: UUID = UUID.nameUUIDFromBytes("ForexFetch".toByteArray()) - operator fun invoke(): RecurringJobBuilder = - RecurringJobBuilder.aRecurringJob() - .withJobRequest(Request()) - .withName("ForexFetch") - .withId(JOB_ID.toString()) - .withDuration(Duration.ofDays(1)) - .withAmountOfRetries(3) + operator fun invoke(): RecurringJobBuilder = RecurringJobBuilder.aRecurringJob() + .withJobRequest(Request()) + .withName("ForexFetch") + .withId(JOB_ID.toString()) + .withDuration(Duration.ofDays(1)) + .withAmountOfRetries(3) fun immediate(): JobRequest = Request() } @@ -112,10 +110,9 @@ class ForexFetcher( private const val KEY_FOREX_RATE = "FOREX_RATE" -suspend fun KeyValuesRepository.getForexRate(): ForexRate? = - get(KEY_FOREX_RATE)?.let { - Json.decodeValue(it, ForexRate::class.java) - } +suspend fun KeyValuesRepository.getForexRate(): ForexRate? = get(KEY_FOREX_RATE)?.let { + Json.decodeValue(it, ForexRate::class.java) +} suspend fun KeyValuesRepository.saveForexRate(forexRate: ForexRate) = set(KEY_FOREX_RATE, Json.encode(forexRate)) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/RedditPostsScrapper.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/RedditPostsScrapper.kt index e0e5634..29bebf4 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/RedditPostsScrapper.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/RedditPostsScrapper.kt @@ -41,38 +41,37 @@ class RedditPostsScrapper( } private val jobRequestScheduler by instance() - override suspend fun handleRequest(jobRequest: Request): Unit = - loggingExecutionTime( - "$SIMPLE_NAME:: handleRequest", - ) { - val lastPostTime = keyValueRepository.get(LAST_REDDIT_POST_TIME)?.let(OffsetDateTime::parse) + override suspend fun handleRequest(jobRequest: Request): Unit = loggingExecutionTime( + "$SIMPLE_NAME:: handleRequest", + ) { + val lastPostTime = keyValueRepository.get(LAST_REDDIT_POST_TIME)?.let(OffsetDateTime::parse) - val posts = - loggingExecutionTime( - "$SIMPLE_NAME:: Fetched reddit post, last created post was at : '$lastPostTime'", - ) { - getLatestRedditPosts(lastPostTime ?: OffsetDateTime.MIN) - } + val posts = + loggingExecutionTime( + "$SIMPLE_NAME:: Fetched reddit post, last created post was at : '$lastPostTime'", + ) { + getLatestRedditPosts(lastPostTime ?: OffsetDateTime.MIN) + } - val appIds = - posts.flatMap { post -> - PLAY_CONSOLE_REGX.findAll(post.content).toList().mapNotNull { - it.groupValues.lastOrNull() - } - }.distinct() + val appIds = + posts.flatMap { post -> + PLAY_CONSOLE_REGX.findAll(post.content).toList().mapNotNull { + it.groupValues.lastOrNull() + } + }.distinct() - logger.infoNotify("$SIMPLE_NAME:: got ${posts.size} new posts (${appIds.size} Links)") + logger.infoNotify("$SIMPLE_NAME:: got ${posts.size} new posts (${appIds.size} Links)") - appIds.forEach { packageName -> - val id = UUID.nameUUIDFromBytes(packageName.toByteArray()) - jobRequestScheduler.enqueue(id, AppDetailScrapper.Request(packageName)) - } + appIds.forEach { packageName -> + val id = UUID.nameUUIDFromBytes(packageName.toByteArray()) + jobRequestScheduler.enqueue(id, AppDetailScrapper.Request(packageName)) + } - posts.firstOrNull()?.let { - logger.info("$SIMPLE_NAME:: Last reddit post was at ${it.createdAt} with id ${it.id}") - keyValueRepository.set(LAST_REDDIT_POST_TIME, it.createdAt.toString()) - } + posts.firstOrNull()?.let { + logger.info("$SIMPLE_NAME:: Last reddit post was at ${it.createdAt} with id ${it.id}") + keyValueRepository.set(LAST_REDDIT_POST_TIME, it.createdAt.toString()) } + } private suspend fun getLatestRedditPosts(lastPostTime: OffsetDateTime): List { val path = "/r/googleplaydeals/new.json?limit=100" @@ -114,14 +113,13 @@ class RedditPostsScrapper( companion object { private val JOB_ID: UUID = UUID.nameUUIDFromBytes("Reddit Posts".toByteArray()) - operator fun invoke(): RecurringJobBuilder = - RecurringJobBuilder.aRecurringJob() - .withJobRequest(Request()) - .withAmountOfRetries(2) - .withLabels("Reddit") - .withName("Reddit Post Scrap") - .withId(JOB_ID.toString()) - .withDuration(Duration.ofHours(1)) + operator fun invoke(): RecurringJobBuilder = RecurringJobBuilder.aRecurringJob() + .withJobRequest(Request()) + .withAmountOfRetries(2) + .withLabels("Reddit") + .withName("Reddit Post Scrap") + .withId(JOB_ID.toString()) + .withDuration(Duration.ofHours(1)) } } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/DealRepository.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/DealRepository.kt index 0ef9251..979bb3c 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/DealRepository.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/DealRepository.kt @@ -5,10 +5,7 @@ import me.sujanpoudel.playdeals.domain.entities.DealEntity import java.time.OffsetDateTime interface DealRepository { - suspend fun getAll( - skip: Int, - take: Int, - ): List + suspend fun getAll(skip: Int, take: Int): List suspend fun upsert(appDeal: NewDeal): DealEntity diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/KeyValuesRepository.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/KeyValuesRepository.kt index 28592bf..9e02f25 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/KeyValuesRepository.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/KeyValuesRepository.kt @@ -1,10 +1,7 @@ package me.sujanpoudel.playdeals.repositories interface KeyValuesRepository { - suspend fun set( - key: String, - value: String, - ): String + suspend fun set(key: String, value: String): String suspend fun get(key: String): String? diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/caching/CachingDealRepository.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/caching/CachingDealRepository.kt index f30463b..8770ce6 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/caching/CachingDealRepository.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/caching/CachingDealRepository.kt @@ -27,10 +27,7 @@ class CachingDealRepository( } } - override suspend fun getAll( - skip: Int, - take: Int, - ): List { + override suspend fun getAll(skip: Int, take: Int): List { initialize() return if (cacheInitialized) { cache.values.toList().drop(skip).take(take) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentDealRepository.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentDealRepository.kt index 45fb7fd..11cbb6e 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentDealRepository.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentDealRepository.kt @@ -13,10 +13,7 @@ import java.time.OffsetDateTime class PersistentDealRepository( private val sqlClient: SqlClient, ) : DealRepository { - override suspend fun getAll( - skip: Int, - take: Int, - ): List { + override suspend fun getAll(skip: Int, take: Int): List { return sqlClient.preparedQuery( """ SELECT * FROM "deal" ORDER BY created_at DESC OFFSET $1 LIMIT $2 diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentKeyValuesRepository.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentKeyValuesRepository.kt index b643049..a605267 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentKeyValuesRepository.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/repositories/persistent/PersistentKeyValuesRepository.kt @@ -10,10 +10,7 @@ import me.sujanpoudel.playdeals.repositories.KeyValuesRepository class PersistentKeyValuesRepository( private val sqlClient: SqlClient, ) : KeyValuesRepository { - override suspend fun set( - key: String, - value: String, - ): String { + override suspend fun set(key: String, value: String): String { return sqlClient.preparedQuery( """ INSERT INTO "key_value_store" VALUES ($1,$2) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/services/MessagingService.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/services/MessagingService.kt index e16c197..f8fdbf6 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/services/MessagingService.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/services/MessagingService.kt @@ -32,12 +32,7 @@ class MessagingService( ) { private fun String.asTopic() = if (environment == Environment.PRODUCTION) this else "$this-dev" - suspend fun sendMessageToTopic( - topic: String, - title: String, - body: String, - imageUrl: String? = null, - ) { + suspend fun sendMessageToTopic(topic: String, title: String, body: String, imageUrl: String? = null) { val message = Message.builder() .setTopic(topic.asTopic()) @@ -64,22 +59,20 @@ class MessagingService( } } -suspend inline fun MessagingService.sendMessageForNewDeal(deal: DealEntity) = - sendMessageToTopic( - topic = - if (deal.currentPrice == 0f) { - Constants.PushNotificationTopic.NEW_FREE_DEAL - } else { - Constants.PushNotificationTopic.NEW_DISCOUNT_DEAL - }, - title = "New deal found", - body = "${deal.name} was ${deal.formattedNormalPrice()} is now ${deal.formattedCurrentPrice()}", - imageUrl = deal.icon, - ) +suspend inline fun MessagingService.sendMessageForNewDeal(deal: DealEntity) = sendMessageToTopic( + topic = + if (deal.currentPrice == 0f) { + Constants.PushNotificationTopic.NEW_FREE_DEAL + } else { + Constants.PushNotificationTopic.NEW_DISCOUNT_DEAL + }, + title = "New deal found", + body = "${deal.name} was ${deal.formattedNormalPrice()} is now ${deal.formattedCurrentPrice()}", + imageUrl = deal.icon, +) -suspend inline fun MessagingService.sendMaintenanceLog(message: String) = - sendMessageToTopic( - topic = Constants.PushNotificationTopic.DEV_LOG, - title = "Maintenance Log", - body = message, - ) +suspend inline fun MessagingService.sendMaintenanceLog(message: String) = sendMessageToTopic( + topic = Constants.PushNotificationTopic.DEV_LOG, + title = "Maintenance Log", + body = message, +) diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/DBHealthUseCase.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/DBHealthUseCase.kt index d3e5fc1..f11aff9 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/DBHealthUseCase.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/DBHealthUseCase.kt @@ -10,12 +10,11 @@ class DBHealthUseCase( ) : UseCase { private val sqlClient by di.instance() - override suspend fun doExecute(input: Unit): Boolean = - runCatching { - sqlClient.preparedQuery("""SELECT 1""") - .execute() - .coAwait() - }.map { rs -> - rs.count() == 1 - }.getOrDefault(false) + override suspend fun doExecute(input: Unit): Boolean = runCatching { + sqlClient.preparedQuery("""SELECT 1""") + .execute() + .coAwait() + }.map { rs -> + rs.count() == 1 + }.getOrDefault(false) } diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/UseCaseEngine.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/UseCaseEngine.kt index 877db18..9f213d8 100644 --- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/UseCaseEngine.kt +++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/UseCaseEngine.kt @@ -29,15 +29,14 @@ suspend fun RoutingContext.executeUseCase( toInput: (Request) -> Input, onError: (Throwable) -> Unit = this::handleExceptions, onSuccess: (Output) -> Unit, -): Result = - runCatching { toContext.invoke() } - .andThen { - runCatching { - (it as? Validated)?.validate() - it - } +): Result = runCatching { toContext.invoke() } + .andThen { + runCatching { + (it as? Validated)?.validate() + it } - .andThen { runCatching { toInput.invoke(it) } } - .andThen { runCatching { useCase.execute(it) } } - .onSuccess(onSuccess) - .onFailure(onError) + } + .andThen { runCatching { toInput.invoke(it) } } + .andThen { runCatching { useCase.execute(it) } } + .onSuccess(onSuccess) + .onFailure(onError) diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt index 0919d55..4e513de 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt @@ -65,21 +65,19 @@ abstract class IntegrationTest(private val vertx: Vertx) { var di = configureDI(vertx, conf) - protected fun runTest(block: suspend () -> Unit): Unit = - runBlocking(vertx.dispatcher()) { - di.direct.instance() - try { - block() - } catch (e: Exception) { - e.printStackTrace() - throw e - } + protected fun runTest(block: suspend () -> Unit): Unit = runBlocking(vertx.dispatcher()) { + di.direct.instance() + try { + block() + } catch (e: Exception) { + e.printStackTrace() + throw e } + } - private fun deployVerticle(): String = - runBlocking(vertx.dispatcher()) { - vertx.deployVerticle(di.direct.instance()).coAwait() - } + private fun deployVerticle(): String = runBlocking(vertx.dispatcher()) { + vertx.deployVerticle(di.direct.instance()).coAwait() + } @BeforeEach fun assignDeploymentId() { @@ -98,11 +96,10 @@ abstract class IntegrationTest(private val vertx: Vertx) { } @AfterEach - fun undeployVerticle() = - runBlocking(vertx.dispatcher()) { - vertx.undeploy(deploymentId).coAwait() - log.info { "un-deployed deployment id $deploymentId" } - } + fun undeployVerticle() = runBlocking(vertx.dispatcher()) { + vertx.undeploy(deploymentId).coAwait() + log.info { "un-deployed deployment id $deploymentId" } + } companion object { @JvmStatic diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsApiTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsApiTest.kt index c9df63b..68755d3 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsApiTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/GetDealsApiTest.kt @@ -38,108 +38,103 @@ private val newDeal = class GetDealsApiTest(vertx: Vertx) : IntegrationTest(vertx) { @Test - fun `should send error if skip param is less than 0`() = - runTest { - val response = - httpClient.get("/api/deals/?skip=-1") - .send() - .coAwait() + fun `should send error if skip param is less than 0`() = runTest { + val response = + httpClient.get("/api/deals/?skip=-1") + .send() + .coAwait() - val responseBody = response.bodyAsJsonObject() + val responseBody = response.bodyAsJsonObject() - response.statusCode() shouldBe 400 - responseBody.getString("message") shouldBe "skip Can't be less than 0" - } + response.statusCode() shouldBe 400 + responseBody.getString("message") shouldBe "skip Can't be less than 0" + } @Test - fun `should send error if take param is less than 1`() = - runTest { - val response = - httpClient.get("/api/deals/?take=0") - .send() - .coAwait() + fun `should send error if take param is less than 1`() = runTest { + val response = + httpClient.get("/api/deals/?take=0") + .send() + .coAwait() - val responseBody = response.bodyAsJsonObject() + val responseBody = response.bodyAsJsonObject() - response.statusCode() shouldBe 400 - responseBody.getString("message") shouldBe "take Can't be less than 1" - } + response.statusCode() shouldBe 400 + responseBody.getString("message") shouldBe "take Can't be less than 1" + } @Test - fun `should return app deals`() = - runTest { - val repository = di.get() + fun `should return app deals`() = runTest { + val repository = di.get() - val app0 = repository.upsert(newDeal) - val app1 = repository.upsert(newDeal.copy(id = "id1")) + val app0 = repository.upsert(newDeal) + val app1 = repository.upsert(newDeal.copy(id = "id1")) - val response = - httpClient.get("/api/deals/") - .send() - .coAwait() + val response = + httpClient.get("/api/deals/") + .send() + .coAwait() - val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) + val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) - response.statusCode() shouldBe 200 - deals.data!!.size shouldBe 2 - deals.data.shouldContainAll(listOf(app0, app1)) - } + response.statusCode() shouldBe 200 + deals.data!!.size shouldBe 2 + deals.data.shouldContainAll(listOf(app0, app1)) + } @Test - fun `should correctly handle skip parameter`() = - runTest { - val repository = di.get() + fun `should correctly handle skip parameter`() = runTest { + val repository = di.get() - repository.upsert(newDeal) - repository.upsert(newDeal.copy(id = "id1")) - repository.upsert(newDeal.copy(id = "id2")) - repository.upsert(newDeal.copy(id = "id3")) + repository.upsert(newDeal) + repository.upsert(newDeal.copy(id = "id1")) + repository.upsert(newDeal.copy(id = "id2")) + repository.upsert(newDeal.copy(id = "id3")) - httpClient.get("/api/deals?skip=1") - .send() - .coAwait().also { response -> - val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) + httpClient.get("/api/deals?skip=1") + .send() + .coAwait().also { response -> + val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) - response.statusCode() shouldBe 200 - deals.data!!.size shouldBe 3 - } + response.statusCode() shouldBe 200 + deals.data!!.size shouldBe 3 + } - httpClient.get("/api/deals?skip=3") - .send() - .coAwait().also { response -> - val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) + httpClient.get("/api/deals?skip=3") + .send() + .coAwait().also { response -> + val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) - response.statusCode() shouldBe 200 - deals.data!!.size shouldBe 1 - } - } + response.statusCode() shouldBe 200 + deals.data!!.size shouldBe 1 + } + } @Test - fun `should correctly handle take parameter`() = - runTest { - val repository = di.get() - - repository.upsert(newDeal) - repository.upsert(newDeal.copy(id = "id1")) - repository.upsert(newDeal.copy(id = "id2")) - repository.upsert(newDeal.copy(id = "id3")) - - httpClient.get("/api/deals?take=2") - .send() - .coAwait().also { response -> - val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) - - response.statusCode() shouldBe 200 - deals.data!!.size shouldBe 2 - } - - httpClient.get("/api/deals?take=1") - .send() - .coAwait().also { response -> - val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) - - response.statusCode() shouldBe 200 - deals.data!!.size shouldBe 1 - } - } + fun `should correctly handle take parameter`() = runTest { + val repository = di.get() + + repository.upsert(newDeal) + repository.upsert(newDeal.copy(id = "id1")) + repository.upsert(newDeal.copy(id = "id2")) + repository.upsert(newDeal.copy(id = "id3")) + + httpClient.get("/api/deals?take=2") + .send() + .coAwait().also { response -> + val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) + + response.statusCode() shouldBe 200 + deals.data!!.size shouldBe 2 + } + + httpClient.get("/api/deals?take=1") + .send() + .coAwait().also { response -> + val deals: ApiResponse> = di.get().readValue(response.bodyAsString()) + + response.statusCode() shouldBe 200 + deals.data!!.size shouldBe 1 + } + } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealApiTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealApiTest.kt index 1923317..c6890da 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealApiTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/deals/NewDealApiTest.kt @@ -18,85 +18,81 @@ import java.util.UUID class NewDealApiTest(vertx: Vertx) : IntegrationTest(vertx) { @Test - fun `should send error response if packageName is null`() = - runTest { - val response = - httpClient.post("/api/deals") - .sendJson(jsonObjectOf()) - .coAwait() + fun `should send error response if packageName is null`() = runTest { + val response = + httpClient.post("/api/deals") + .sendJson(jsonObjectOf()) + .coAwait() - val responseBody = response.bodyAsJsonObject() + val responseBody = response.bodyAsJsonObject() - response.statusCode() shouldBe 400 - responseBody.getString("message") shouldBe "packageName is required" - } + response.statusCode() shouldBe 400 + responseBody.getString("message") shouldBe "packageName is required" + } @Test - fun `should send error response if packageName is invalid`() = - runTest { - val response = - httpClient.post("/api/deals") - .sendJson(jsonObjectOf("packageName" to "11111")) - .coAwait() + fun `should send error response if packageName is invalid`() = runTest { + val response = + httpClient.post("/api/deals") + .sendJson(jsonObjectOf("packageName" to "11111")) + .coAwait() - val responseBody = response.bodyAsJsonObject() + val responseBody = response.bodyAsJsonObject() - response.statusCode() shouldBe 400 - responseBody.getString("message") shouldBe "Invalid value for packageName" - } + response.statusCode() shouldBe 400 + responseBody.getString("message") shouldBe "Invalid value for packageName" + } @Test - fun `should enqueue a app detail scrap request on success`() = - runTest { - val storageProvider = di.get() + fun `should enqueue a app detail scrap request on success`() = runTest { + val storageProvider = di.get() - val packageName = "com.example.app" + val packageName = "com.example.app" - val response = - httpClient.post("/api/deals") - .sendJson(jsonObjectOf("packageName" to packageName)) - .coAwait() + val response = + httpClient.post("/api/deals") + .sendJson(jsonObjectOf("packageName" to packageName)) + .coAwait() - val job = storageProvider.getJobById(UUID.nameUUIDFromBytes(packageName.encodeToByteArray())) + val job = storageProvider.getJobById(UUID.nameUUIDFromBytes(packageName.encodeToByteArray())) - job.state shouldBe StateName.ENQUEUED + job.state shouldBe StateName.ENQUEUED - response.statusCode() shouldBe 200 - } + response.statusCode() shouldBe 200 + } @Test - fun `should should 200 if the app already exists`() = - runTest { - di.get() - val repository = di.get() - - val packageName = "com.example.app" - - val newDeal = - NewDeal( - id = packageName, - name = "name", - icon = "icon", - images = listOf("img0", "img1"), - normalPrice = 12f, - currentPrice = 12f, - currency = "USD", - storeUrl = "store_url", - category = "unknown", - downloads = "12+", - rating = "12", - offerExpiresIn = OffsetDateTime.now(), - type = DealType.ANDROID_APP, - source = Constants.DealSources.APP_DEAL_SUBREDDIT, - ) - - repository.upsert(newDeal) - - val response = - httpClient.post("/api/deals") - .sendJson(jsonObjectOf("packageName" to packageName)) - .coAwait() - - response.statusCode() shouldBe 200 - } + fun `should should 200 if the app already exists`() = runTest { + di.get() + val repository = di.get() + + val packageName = "com.example.app" + + val newDeal = + NewDeal( + id = packageName, + name = "name", + icon = "icon", + images = listOf("img0", "img1"), + normalPrice = 12f, + currentPrice = 12f, + currency = "USD", + storeUrl = "store_url", + category = "unknown", + downloads = "12+", + rating = "12", + offerExpiresIn = OffsetDateTime.now(), + type = DealType.ANDROID_APP, + source = Constants.DealSources.APP_DEAL_SUBREDDIT, + ) + + repository.upsert(newDeal) + + val response = + httpClient.post("/api/deals") + .sendJson(jsonObjectOf("packageName" to packageName)) + .coAwait() + + response.statusCode() shouldBe 200 + } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt index a7745aa..43fd0bb 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt @@ -17,64 +17,61 @@ import java.time.ZoneOffset class GetForexApiTest(vertx: Vertx) : IntegrationTest(vertx) { @Test - fun `Key value repo should properly store the forex rate`() = - runTest { - val repository = di.get() + fun `Key value repo should properly store the forex rate`() = runTest { + val repository = di.get() - val forexRate = - ForexRate( - timestamp = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC), - rates = listOf(ConversionRate("USD", "$", "US Dollar", 1.1f)), - ) - repository.saveForexRate(forexRate) + val forexRate = + ForexRate( + timestamp = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC), + rates = listOf(ConversionRate("USD", "$", "US Dollar", 1.1f)), + ) + repository.saveForexRate(forexRate) - val savedForexRate = repository.getForexRate() + val savedForexRate = repository.getForexRate() - savedForexRate shouldBe forexRate - } + savedForexRate shouldBe forexRate + } @Test - fun `should return forex if there is data`() = - runTest { - val repository = di.get() + fun `should return forex if there is data`() = runTest { + val repository = di.get() - val forexRate = - ForexRate( - timestamp = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC), - rates = listOf(ConversionRate("USD", "$", "US Dollar", 1.1f)), - ) + val forexRate = + ForexRate( + timestamp = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC), + rates = listOf(ConversionRate("USD", "$", "US Dollar", 1.1f)), + ) - repository.saveForexRate(forexRate) + repository.saveForexRate(forexRate) - val response = - httpClient.get("/api/forex") - .send() - .coAwait() - .bodyAsJsonObject() + val response = + httpClient.get("/api/forex") + .send() + .coAwait() + .bodyAsJsonObject() - response.getJsonObject("data").also { data -> - OffsetDateTime.parse(data.getString("timestamp")) shouldBe forexRate.timestamp - data.getJsonArray("rates").also { rates -> - rates.size() shouldBe 1 - (rates.first() as JsonObject).also { rate -> - rate.getString("currency") shouldBe "USD" - rate.getString("symbol") shouldBe "$" - rate.getString("name") shouldBe "US Dollar" - rate.getFloat("rate") shouldBe 1.1f - } + response.getJsonObject("data").also { data -> + OffsetDateTime.parse(data.getString("timestamp")) shouldBe forexRate.timestamp + data.getJsonArray("rates").also { rates -> + rates.size() shouldBe 1 + (rates.first() as JsonObject).also { rate -> + rate.getString("currency") shouldBe "USD" + rate.getString("symbol") shouldBe "$" + rate.getString("name") shouldBe "US Dollar" + rate.getFloat("rate") shouldBe 1.1f } } } + } @Test - fun `should return null if there is no data`() = - runTest { - val response = - httpClient.get("/api/forex") - .send() - .coAwait() - .bodyAsJsonObject() + fun `should return null if there is no data`() = runTest { + val response = + httpClient.get("/api/forex") + .send() + .coAwait() + .bodyAsJsonObject() - response.getJsonObject("data") shouldBe null - } + response.getJsonObject("data") shouldBe null + } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/DBCleanupTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/DBCleanupTest.kt index 135f6e8..3d82deb 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/DBCleanupTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/DBCleanupTest.kt @@ -10,19 +10,18 @@ import org.junit.jupiter.api.Test class DBCleanupTest(vertx: Vertx) : IntegrationTest(vertx) { @Test - fun `Does cleanup`() = - runTest { - val sqlClient = di.get() + fun `Does cleanup`() = runTest { + val sqlClient = di.get() - sqlClient - .query(CLEAN_UP_DB_QUERY).execute() - .onFailure { it.printStackTrace() } + sqlClient + .query(CLEAN_UP_DB_QUERY).execute() + .onFailure { it.printStackTrace() } - val totalDeals = - sqlClient.preparedQuery("""select count(*) from deal """) - .execute() - .coAwait().first().getInteger(0) + val totalDeals = + sqlClient.preparedQuery("""select count(*) from deal """) + .execute() + .coAwait().first().getInteger(0) - totalDeals shouldBe 0 - } + totalDeals shouldBe 0 + } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/HealthTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/HealthTest.kt index 8141c81..c0b4dd5 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/HealthTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/health/HealthTest.kt @@ -8,23 +8,21 @@ import org.junit.jupiter.api.Test class HealthTest(vertx: Vertx) : IntegrationTest(vertx) { @Test - fun `GET liveness should return 200`() = - runTest { - val response = httpClient.get(8888, "localhost", "/health/liveness").send().coAwait() + fun `GET liveness should return 200`() = runTest { + val response = httpClient.get(8888, "localhost", "/health/liveness").send().coAwait() - response.statusCode() shouldBe 200 - val responseJson = response.bodyAsJsonObject() - responseJson.getString("status") shouldBe "UP" - } + response.statusCode() shouldBe 200 + val responseJson = response.bodyAsJsonObject() + responseJson.getString("status") shouldBe "UP" + } @Test - fun `GET readiness should return 200`() = - runTest { - val response = httpClient.get(8888, "localhost", "/health/readiness").send().coAwait() + fun `GET readiness should return 200`() = runTest { + val response = httpClient.get(8888, "localhost", "/health/readiness").send().coAwait() - val responseJson = response.bodyAsJsonObject() - response.statusCode() shouldBe 200 - responseJson.getString("status") shouldBe "UP" - responseJson.getString("outcome") shouldBe "UP" - } + val responseJson = response.bodyAsJsonObject() + response.statusCode() shouldBe 200 + responseJson.getString("status") shouldBe "UP" + responseJson.getString("outcome") shouldBe "UP" + } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/CachingDealRepositoryTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/CachingDealRepositoryTest.kt index 9eecc7d..312c1dc 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/CachingDealRepositoryTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/CachingDealRepositoryTest.kt @@ -29,108 +29,104 @@ class CachingDealRepositoryTest(vertx: Vertx) : IntegrationTest(vertx) { } @Test - fun `should delegate new deal call`() = - runTest { - val dealEntity = mockk() - val newDeal = mockk() + fun `should delegate new deal call`() = runTest { + val dealEntity = mockk() + val newDeal = mockk() - coEvery { persistentDealRepository.upsert(any()) } returns dealEntity + coEvery { persistentDealRepository.upsert(any()) } returns dealEntity - repository.upsert(newDeal) shouldBe dealEntity + repository.upsert(newDeal) shouldBe dealEntity - coVerify(exactly = 1) { - persistentDealRepository.upsert(newDeal) - } + coVerify(exactly = 1) { + persistentDealRepository.upsert(newDeal) } + } @Test - fun `should cache newly added deal`() = - runTest { - val dealEntity = mockk() - val newDeal = mockk() + fun `should cache newly added deal`() = runTest { + val dealEntity = mockk() + val newDeal = mockk() - every { newDeal.id } returns "1" - every { dealEntity.id } returns newDeal.id - coEvery { persistentDealRepository.upsert(any()) } returns dealEntity - coEvery { persistentDealRepository.getAll(any(), any()) } returns emptyList() + every { newDeal.id } returns "1" + every { dealEntity.id } returns newDeal.id + coEvery { persistentDealRepository.upsert(any()) } returns dealEntity + coEvery { persistentDealRepository.getAll(any(), any()) } returns emptyList() - repository.upsert(newDeal) + repository.upsert(newDeal) - repository.getAll(0, Int.MAX_VALUE).shouldContainExactly(dealEntity) + repository.getAll(0, Int.MAX_VALUE).shouldContainExactly(dealEntity) - clearAllMocks() + clearAllMocks() - coVerify(exactly = 0) { - persistentDealRepository.getAll(any(), any()) - } + coVerify(exactly = 0) { + persistentDealRepository.getAll(any(), any()) } + } @Test - fun `should remove entry when deal is deleted`() = - runTest { - val entity1 = mockk() - val entity2 = mockk() + fun `should remove entry when deal is deleted`() = runTest { + val entity1 = mockk() + val entity2 = mockk() - val newDeal1 = mockk() - val newDeal2 = mockk() + val newDeal1 = mockk() + val newDeal2 = mockk() - every { newDeal1.id } returns "1" - every { newDeal2.id } returns "2" + every { newDeal1.id } returns "1" + every { newDeal2.id } returns "2" - every { entity1.id } returns newDeal1.id - every { entity2.id } returns newDeal2.id + every { entity1.id } returns newDeal1.id + every { entity2.id } returns newDeal2.id - coEvery { persistentDealRepository.upsert(newDeal1) } returns entity1 - coEvery { persistentDealRepository.upsert(newDeal2) } returns entity2 + coEvery { persistentDealRepository.upsert(newDeal1) } returns entity1 + coEvery { persistentDealRepository.upsert(newDeal2) } returns entity2 - coEvery { persistentDealRepository.delete("1") } returns entity1 - coEvery { persistentDealRepository.delete("2") } returns entity2 + coEvery { persistentDealRepository.delete("1") } returns entity1 + coEvery { persistentDealRepository.delete("2") } returns entity2 - coEvery { persistentDealRepository.getAll(any(), any()) } returns emptyList() + coEvery { persistentDealRepository.getAll(any(), any()) } returns emptyList() - repository.upsert(newDeal1) - repository.upsert(newDeal2) + repository.upsert(newDeal1) + repository.upsert(newDeal2) - repository.getAll(0, Int.MAX_VALUE).shouldContainExactly(entity1, entity2) + repository.getAll(0, Int.MAX_VALUE).shouldContainExactly(entity1, entity2) - repository.delete(entity2.id) + repository.delete(entity2.id) - repository.getAll(0, Int.MAX_VALUE).shouldContainExactly(entity1) - } + repository.getAll(0, Int.MAX_VALUE).shouldContainExactly(entity1) + } @Test - fun `should respect skip and take`() = - runTest { - val entity1 = mockk() - val entity2 = mockk() - val entity3 = mockk() + fun `should respect skip and take`() = runTest { + val entity1 = mockk() + val entity2 = mockk() + val entity3 = mockk() - val newDeal1 = mockk() - val newDeal2 = mockk() - val newDeal3 = mockk() + val newDeal1 = mockk() + val newDeal2 = mockk() + val newDeal3 = mockk() - every { newDeal1.id } returns "1" - every { newDeal2.id } returns "2" - every { newDeal3.id } returns "3" + every { newDeal1.id } returns "1" + every { newDeal2.id } returns "2" + every { newDeal3.id } returns "3" - every { entity1.id } returns newDeal1.id - every { entity2.id } returns newDeal2.id - every { entity3.id } returns newDeal3.id + every { entity1.id } returns newDeal1.id + every { entity2.id } returns newDeal2.id + every { entity3.id } returns newDeal3.id - coEvery { persistentDealRepository.upsert(newDeal1) } returns entity1 - coEvery { persistentDealRepository.upsert(newDeal2) } returns entity2 - coEvery { persistentDealRepository.upsert(newDeal3) } returns entity3 + coEvery { persistentDealRepository.upsert(newDeal1) } returns entity1 + coEvery { persistentDealRepository.upsert(newDeal2) } returns entity2 + coEvery { persistentDealRepository.upsert(newDeal3) } returns entity3 - coEvery { persistentDealRepository.getAll(any(), any()) } returns emptyList() + coEvery { persistentDealRepository.getAll(any(), any()) } returns emptyList() - repository.upsert(newDeal1) - repository.upsert(newDeal2) - repository.upsert(newDeal3) + repository.upsert(newDeal1) + repository.upsert(newDeal2) + repository.upsert(newDeal3) - println(repository.getAll(0, Int.MAX_VALUE)) + println(repository.getAll(0, Int.MAX_VALUE)) - repository.getAll(0, 1).shouldContainExactly(entity1) - repository.getAll(1, 1).shouldContainExactly(entity2) - repository.getAll(2, 1).shouldContainExactly(entity3) - } + repository.getAll(0, 1).shouldContainExactly(entity1) + repository.getAll(1, 1).shouldContainExactly(entity2) + repository.getAll(2, 1).shouldContainExactly(entity3) + } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentDealRepositoryTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentDealRepositoryTest.kt index ad2f0be..69a81d2 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentDealRepositoryTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentDealRepositoryTest.kt @@ -41,108 +41,100 @@ class PersistentDealRepositoryTest(vertx: Vertx) : IntegrationTest(vertx) { ) @Test - fun `should create new app deal in db`() = - runTest { - val appDeal = repository.upsert(newDeal) + fun `should create new app deal in db`() = runTest { + val appDeal = repository.upsert(newDeal) - val appDealFromDb = - sqlClient.preparedQuery(""" SELECT * from "deal" where id=$1""") - .exec(newDeal.id) - .coAwait() - .first() - .asAppDeal() + val appDealFromDb = + sqlClient.preparedQuery(""" SELECT * from "deal" where id=$1""") + .exec(newDeal.id) + .coAwait() + .first() + .asAppDeal() - appDeal.shouldBeEqualToComparingFields(appDealFromDb) - } + appDeal.shouldBeEqualToComparingFields(appDealFromDb) + } @Test - fun `should perform update if item with id already exists`() = - runTest { - repository.upsert(newDeal) + fun `should perform update if item with id already exists`() = runTest { + repository.upsert(newDeal) - repository.upsert(newDeal.copy(name = "Updated Name")) + repository.upsert(newDeal.copy(name = "Updated Name")) - val appDealFromDb = - sqlClient.preparedQuery(""" SELECT * from "deal" where id=$1""") - .exec(newDeal.id) - .coAwait() - .first() - .asAppDeal() + val appDealFromDb = + sqlClient.preparedQuery(""" SELECT * from "deal" where id=$1""") + .exec(newDeal.id) + .coAwait() + .first() + .asAppDeal() - appDealFromDb.name.shouldBe("Updated Name") - } + appDealFromDb.name.shouldBe("Updated Name") + } @Test - fun `should delete app deal in db`() = - runTest { - repository.upsert(newDeal) - repository.delete(newDeal.id) + fun `should delete app deal in db`() = runTest { + repository.upsert(newDeal) + repository.delete(newDeal.id) - sqlClient.preparedQuery("""SELECT * from "deal" where id=$1""") - .exec(newDeal.id) - .coAwait() - .rowCount() shouldBe 0 - } + sqlClient.preparedQuery("""SELECT * from "deal" where id=$1""") + .exec(newDeal.id) + .coAwait() + .rowCount() shouldBe 0 + } @Test - fun `should be able to get all app deals from db`() = - runTest { - val deal0 = repository.upsert(newDeal) - val deal1 = repository.upsert(newDeal.copy(id = "id_1")) + fun `should be able to get all app deals from db`() = runTest { + val deal0 = repository.upsert(newDeal) + val deal1 = repository.upsert(newDeal.copy(id = "id_1")) - val appDeal = repository.getAll(0, 100) + val appDeal = repository.getAll(0, 100) - appDeal shouldContainAll listOf(deal0, deal1) - } + appDeal shouldContainAll listOf(deal0, deal1) + } @Test - fun `should be able to get all app deals from db in order`() = - runTest { - val deal0 = repository.upsert(newDeal) - val deal1 = repository.upsert(newDeal.copy(id = "id_1")) + fun `should be able to get all app deals from db in order`() = runTest { + val deal0 = repository.upsert(newDeal) + val deal1 = repository.upsert(newDeal.copy(id = "id_1")) - val appDeal = repository.getAll(0, 100) + val appDeal = repository.getAll(0, 100) - appDeal shouldContainInOrder listOf(deal1, deal0) - } + appDeal shouldContainInOrder listOf(deal1, deal0) + } @Test - fun `should get deals added after a time`() = - runTest { - val deal0 = repository.upsert(newDeal) + fun `should get deals added after a time`() = runTest { + val deal0 = repository.upsert(newDeal) - val now = OffsetDateTime.now() + val now = OffsetDateTime.now() - val deal1 = repository.upsert(newDeal.copy(id = "id_1")) - val deal2 = repository.upsert(newDeal.copy(id = "id_2")) + val deal1 = repository.upsert(newDeal.copy(id = "id_1")) + val deal2 = repository.upsert(newDeal.copy(id = "id_2")) - repository.upsert(newDeal.copy(id = "id_2")) + repository.upsert(newDeal.copy(id = "id_2")) - val count = repository.getNewDeals(now) + val count = repository.getNewDeals(now) - count.shouldContainAll(deal1, deal2) - } + count.shouldContainAll(deal1, deal2) + } @Test - fun `getDealByPackageName should return correct deal by packageName`() = - runTest { - val deal0 = repository.upsert(newDeal) + fun `getDealByPackageName should return correct deal by packageName`() = runTest { + val deal0 = repository.upsert(newDeal) - repository.upsert(newDeal.copy(id = "id_1")) - repository.upsert(newDeal.copy(id = "id_2")) + repository.upsert(newDeal.copy(id = "id_1")) + repository.upsert(newDeal.copy(id = "id_2")) - val deal01 = repository.getDealByPackageName(deal0.id) + val deal01 = repository.getDealByPackageName(deal0.id) - deal0 shouldBe deal01 - } + deal0 shouldBe deal01 + } @Test - fun `getDealByPackageName should return null when there is no deal`() = - runTest { - val deal0 = repository.upsert(newDeal) + fun `getDealByPackageName should return null when there is no deal`() = runTest { + val deal0 = repository.upsert(newDeal) - val deal1 = repository.getDealByPackageName("id_3") + val deal1 = repository.getDealByPackageName("id_3") - deal1 shouldBe null - } + deal1 shouldBe null + } } diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentKeyValueRepositoryTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentKeyValueRepositoryTest.kt index 26c6766..fd488b0 100644 --- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentKeyValueRepositoryTest.kt +++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/repositories/PersistentKeyValueRepositoryTest.kt @@ -16,48 +16,45 @@ class PersistentKeyValueRepositoryTest(vertx: Vertx) : IntegrationTest(vertx) { private val sqlClient by lazy { di.get() } @Test - fun `should create new entry on db`() = - runTest { - val value = repository.set(KEY, "test") + fun `should create new entry on db`() = runTest { + val value = repository.set(KEY, "test") - val valueFromDb = - sqlClient.preparedQuery(""" SELECT * from "key_value_store" where key=$1""") - .exec(KEY) - .coAwait() - .first() - .getString("value") + val valueFromDb = + sqlClient.preparedQuery(""" SELECT * from "key_value_store" where key=$1""") + .exec(KEY) + .coAwait() + .first() + .getString("value") - value shouldBe valueFromDb - } + value shouldBe valueFromDb + } @Test - fun `should perform update if item with id already exists`() = - runTest { - repository.set(KEY, "test") + fun `should perform update if item with id already exists`() = runTest { + repository.set(KEY, "test") - val updated = repository.set(KEY, "test1") + val updated = repository.set(KEY, "test1") - val fromDb = - sqlClient.preparedQuery(""" SELECT * from "key_value_store" where key=$1""") - .exec(KEY) - .coAwait() - .first() - .value() + val fromDb = + sqlClient.preparedQuery(""" SELECT * from "key_value_store" where key=$1""") + .exec(KEY) + .coAwait() + .first() + .value() - fromDb shouldBe updated - } + fromDb shouldBe updated + } @Test - fun `should be able to serialize unknown types`() = - runTest { - val value = OffsetDateTime.now() + fun `should be able to serialize unknown types`() = runTest { + val value = OffsetDateTime.now() - repository.set(KEY, value.toString()) + repository.set(KEY, value.toString()) - val fromDb = repository.get(KEY).let(OffsetDateTime::parse) + val fromDb = repository.get(KEY).let(OffsetDateTime::parse) - fromDb shouldBe value - } + fromDb shouldBe value + } companion object { const val KEY = "test_key" diff --git a/build.gradle.kts b/build.gradle.kts index c4aaf24..f951076 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,8 @@ @file:Suppress("UnstableApiUsage") import org.gradle.api.tasks.testing.logging.TestLogEvent +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jlleitschuh.gradle.ktlint.KtlintExtension import org.jlleitschuh.gradle.ktlint.KtlintPlugin plugins { @@ -21,17 +23,34 @@ allprojects { task("preCommitHook") { dependsOn(tasks.ktlintCheck) } + + extensions.configure { + version = rootProject.libs.versions.ktlint.get() + enableExperimentalRules = false + coloredOutput = true + + filter { + exclude { + it.file.absoluteFile.startsWith(layout.buildDirectory.asFile.get().absolutePath) + } + } + } + + tasks.withType { + compilerOptions { + allWarningsAsErrors.set(true) + } + } } tasks.withType { useJUnitPlatform() testLogging { - events = - setOf( - TestLogEvent.PASSED, - TestLogEvent.SKIPPED, - TestLogEvent.FAILED, - ) + events = setOf( + TestLogEvent.PASSED, + TestLogEvent.SKIPPED, + TestLogEvent.FAILED, + ) } } From 489edaee73740e91e48d057b2a744e062e0c393a Mon Sep 17 00:00:00 2001 From: psuzn Date: Fri, 7 Jun 2024 14:32:41 +0545 Subject: [PATCH 3/4] ci/cd runner update --- .github/workflows/cd.yaml | 10 +++++----- .github/workflows/ci.yaml | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index ea65f3e..a469125 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -7,9 +7,9 @@ jobs: build_image: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' @@ -22,7 +22,7 @@ jobs: with: arguments: build - - uses: extractions/setup-just@v1 + - uses: extractions/setup-just@v2 - name: Build and publish image with jib run: just build-push-image @@ -34,9 +34,9 @@ jobs: runs-on: ubuntu-latest needs: build_image steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: extractions/setup-just@v1 + - uses: extractions/setup-just@v2 - uses: azure/setup-helm@v3 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bbee9af..521b646 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,17 +7,17 @@ jobs: check_verify: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: gradle - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v1.1.0 + uses: gradle/actions/wrapper-validation@v3 - name: Build Project with Gradle uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 From ccaeba33b5c1aceb5309241b2e228fd6f52c8378 Mon Sep 17 00:00:00 2001 From: psuzn Date: Fri, 7 Jun 2024 14:34:00 +0545 Subject: [PATCH 4/4] add cache for ci --- .github/workflows/cd.yaml | 15 ++++++++++++++- .github/workflows/ci.yaml | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index a469125..980cff3 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -14,11 +14,24 @@ jobs: java-version: '17' distribution: 'temurin' + - name: Cache + uses: actions/cache@v4 + with: + path: | + ./build + ./.gradle + ~/.gradle/caches + ~/.gradle/wrapper + ~/.m2/repository + key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Validate Gradle wrapper uses: gradle/wrapper-validation-action@ccb4328a959376b642e027874838f60f8e596de3 - name: Build Project with Gradle - uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 + uses: gradle/gradle-build-action@v3 with: arguments: build diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 521b646..bf90b74 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,11 +16,24 @@ jobs: distribution: 'temurin' cache: gradle + - name: Cache + uses: actions/cache@v4 + with: + path: | + ./build + ./.gradle + ~/.gradle/caches + ~/.gradle/wrapper + ~/.m2/repository + key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Validate Gradle wrapper uses: gradle/actions/wrapper-validation@v3 - name: Build Project with Gradle - uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 + uses: gradle/gradle-build-action@v3 with: arguments: build