diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml
index 0420b64..ea65f3e 100644
--- a/.github/workflows/cd.yaml
+++ b/.github/workflows/cd.yaml
@@ -62,6 +62,7 @@ jobs:
DB_NAME: ${{ secrets.DB_NAME }}
DASHBOARD: ${{ secrets.DASHBOARD }}
FIREBASE_ADMIN_AUTH_CREDENTIALS: ${{ secrets.FIREBASE_ADMIN_AUTH_CREDENTIALS }}
+ FOREX_API_KEY: ${{ secrets.FOREX_API_KEY }}
DASHBOARD_USER: ${{ secrets.DASHBOARD_USER }}
DASHBOARD_PASS: ${{ secrets.DASHBOARD_PASS }}
diff --git a/README.md b/README.md
index 216181d..044bae1 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,21 @@
# Play Deals
-[](https://play.google.com/store/apps/details?id=me.sujanpoudel.playdeals)
-[](https://github.com/psuzn/app-deals/releases/latest)
-[](https://github.com/psuzn/app-deals/releases/latest)
-[](https://github.com/psuzn/app-deals/releases/latest)
-[](https://github.com/psuzn/app-deals/releases/latest)
+
+[](https://play.google.com/store/apps/details?id=me.sujanpoudel.playdeals)
+
+[](https://github.com/psuzn/app-deals/releases/latest)
+
+[](https://github.com/psuzn/app-deals/releases/latest)
+
+[](https://github.com/psuzn/app-deals/releases/latest)
+
+[](https://github.com/psuzn/app-deals/releases/latest)
+

[](https://github.com/psuzn/play-deals-backend/actions/workflows/ci.yaml)

-
|
|
|
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------:|
@@ -41,21 +46,23 @@ Configuration can be done by passing environment variables listed below:
> 1. run `just dev-run` from terminal, OR
> 2. Install [Envfile](https://plugins.jetbrains.com/plugin/7861-envfile) plugin for IntelliJ and run using IntelliJ
-| ENV_VAR | REQUIRED | DEFAULT | EXAMPLE | NOTES |
-|------------------|----------|---------------|--------------|:--------------------------------------------------------------------|
-| `DB_HOST` | `Y` | | `localhost` | |
-| `DB_USERNAME` | `Y` | | `whatever` | |
-| `DB_PASSWORD` | `N` | `password` | `whatever` | |
-| `DB_PORT` | `N` | `5432` | `6868` | |
-| `DB_NAME` | `N` | `play_deals` | `whatever` | |
-| `DB_POOL_SIZE` | `N` | `5` | `6` | |
-| `ENV` | `N` | `PRODUCTION` | `PRODUCTION` | one of `PRODUCTION or DEVELOPMENT or TEST ` |
-| `APP_PORT` | `N` | `8888` | `9999` | |
-| `POSTGRES_IMAGE` | `N` | `postgres:14` | | Useful for testing new versions of postgres. Used only in test code |
-| `DASHBOARD` | `N` | `true` | `false` | Whether to enable or not the Jobrunr dashboard |
-| `DASHBOARD_USER` | `N` | `admin` | `whatever` | Jobrunr dashboard login credential |
-| `DASHBOARD_PASS` | `N` | `admin` | `whatever` | Jobrunr dashboard login credential |
-| `CORS` | `N` | `*` | `whatever` | origins allowed for CORS |
+| ENV_VAR | REQUIRED | DEFAULT | EXAMPLE | NOTES |
+|-----------------------------------|----------|---------------|--------------|:-----------------------------------------------------------------------|
+| `DB_HOST` | `Y` | | `localhost` | |
+| `DB_USERNAME` | `Y` | | `whatever` | |
+| `FIREBASE_ADMIN_AUTH_CREDENTIALS` | `Y` | | `whatever` | Firebase admin auth credentials |
+| `FOREX_API_KEY` | `Y` | | `whatever` | Api key for [https://exchangeratesapi.io](https://exchangeratesapi.io) |
+| `DB_PASSWORD` | `N` | `password` | `whatever` | |
+| `DB_PORT` | `N` | `5432` | `6868` | |
+| `DB_NAME` | `N` | `play_deals` | `whatever` | |
+| `DB_POOL_SIZE` | `N` | `5` | `6` | |
+| `ENV` | `N` | `PRODUCTION` | `PRODUCTION` | one of `PRODUCTION or DEVELOPMENT or TEST ` |
+| `APP_PORT` | `N` | `8888` | `9999` | |
+| `POSTGRES_IMAGE` | `N` | `postgres:14` | | Useful for testing new versions of postgres. Used only in test code |
+| `DASHBOARD` | `N` | `true` | `false` | Whether to enable or not the Jobrunr dashboard |
+| `DASHBOARD_USER` | `N` | `admin` | `whatever` | Jobrunr dashboard login credential |
+| `DASHBOARD_PASS` | `N` | `admin` | `whatever` | Jobrunr dashboard login credential |
+| `CORS` | `N` | `*` | `whatever` | origins allowed for CORS |
## License
diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/Conf.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/Conf.kt
index 6fe7044..b745318 100644
--- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/Conf.kt
+++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/Conf.kt
@@ -11,7 +11,8 @@ data class Conf(
val api: Api,
val environment: Environment,
val backgroundTask: BackgroundTask,
- val firebaseAuthCredential: String
+ val firebaseAuthCredential: String,
+ val forexApiKey: String
) {
data class DB(
val host: String,
diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt
index 1e20e73..cc1b7fa 100644
--- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt
+++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/DIConfigurer.kt
@@ -19,6 +19,7 @@ import me.sujanpoudel.playdeals.jobs.AndroidAppExpiryCheckScheduler
import me.sujanpoudel.playdeals.jobs.AppDetailScrapper
import me.sujanpoudel.playdeals.jobs.BackgroundJobsVerticle
import me.sujanpoudel.playdeals.jobs.DealSummarizer
+import me.sujanpoudel.playdeals.jobs.ForexFetcher
import me.sujanpoudel.playdeals.jobs.RedditPostsScrapper
import me.sujanpoudel.playdeals.repositories.DealRepository
import me.sujanpoudel.playdeals.repositories.KeyValuesRepository
@@ -28,6 +29,7 @@ import me.sujanpoudel.playdeals.repositories.persistent.PersistentKeyValuesRepos
import me.sujanpoudel.playdeals.services.MessagingService
import me.sujanpoudel.playdeals.usecases.DBHealthUseCase
import me.sujanpoudel.playdeals.usecases.GetDealsUseCase
+import me.sujanpoudel.playdeals.usecases.GetForexUseCase
import me.sujanpoudel.playdeals.usecases.NewDealUseCase
import org.flywaydb.core.Flyway
import org.jobrunr.configuration.JobRunr
@@ -155,10 +157,17 @@ fun configureDI(
requestScheduler = instance()
)
}
+ bindSingleton {
+ ForexFetcher(
+ di = di,
+ conf = instance()
+ )
+ }
bindSingleton { DBHealthUseCase(di) }
bindSingleton { GetDealsUseCase(di) }
bindSingleton { NewDealUseCase(di) }
+ bindSingleton { GetForexUseCase(di) }
bindSingleton {
FirebaseOptions.builder()
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 9be11bc..36e6a9a 100644
--- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ApiVerticle.kt
+++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ApiVerticle.kt
@@ -21,6 +21,7 @@ class ApiVerticle(
router.route().handler(CorsHandler.create().addRelativeOrigin(config.api.cors))
router.route("/health/*").subRouter(healthApi(di, vertx))
router.route("/api/deals/*").subRouter(appDealsApi(di, vertx))
+ router.route("/api/forex/*").subRouter(forexRateApi(di, vertx))
vertx.createHttpServer()
.requestHandler(router)
diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt
new file mode 100644
index 0000000..91bf83f
--- /dev/null
+++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/api/ForexRate.kt
@@ -0,0 +1,25 @@
+package me.sujanpoudel.playdeals.api
+
+import io.vertx.ext.web.Router
+import me.sujanpoudel.playdeals.common.coHandler
+import me.sujanpoudel.playdeals.common.jsonResponse
+import me.sujanpoudel.playdeals.usecases.GetForexUseCase
+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))
+ }
+ }
+}
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 291d589..d0279ab 100644
--- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt
+++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt
@@ -1,3 +1,5 @@
+@file:Suppress("UNCHECKED_CAST")
+
package me.sujanpoudel.playdeals.common
import me.sujanpoudel.playdeals.Conf
@@ -7,74 +9,67 @@ import java.util.Base64
class BootstrapException(val violations: List) : RuntimeException()
-fun buildConf(env: Map) = com.github.michaelbull.result.runCatching {
+fun buildConf(envs: Map) = com.github.michaelbull.result.runCatching {
val violations = mutableListOf()
- val environment = env.getOrDefault("ENV", Environment.PRODUCTION.name).asEnumOrNull()
-
- if (environment == null) {
- violations += "Invalid ENV"
- }
-
- val dashboardEnabled = env.getOrDefault("DASHBOARD", "true").toBooleanStrictOrNull()
- if (dashboardEnabled == null) {
- violations += "Invalid DASHBOARD"
+ @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 appPort = env.getOrDefault("APP_PORT", "8888").toIntOrNull()
-
- if (appPort == null) {
- violations += "Invalid APP_PORT"
- }
+ val environment = env("ENV", Environment.PRODUCTION.name) { it.asEnumOrNull() }
- val dbPort = env.getOrDefault("DB_PORT", "5432").toIntOrNull()
- if (dbPort == null) {
- violations += "Invalid DB_PORT"
- }
+ val appPort = env("APP_PORT", "8888") { it.toIntOrNull() }
+ val cors = env("CORS", ".*.")
- val dbName = env.getOrDefault("DB_NAME", "play_deals")
- val dbPoolSize = (env["DB_POOL_SIZE"] ?: "5").toIntOrNull()
- if (dbPoolSize == null) {
- violations += "Invalid DB_POOL_SIZE"
- }
+ 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")
- fun envVar(envVarName: String): String? {
- val value = env[envVarName]
+ val dashboardEnabled = env("DASHBOARD", "true") { it.toBooleanStrictOrNull() }
+ val dashboardUser = env("DASHBOARD_USER", "admin")
+ val dashboardPassword = env("DASHBOARD_PASS", "admin")
- return if (value.isNullOrBlank()) {
- violations += "No $envVarName env var defined!".also { logger.error { it } }
- null
- } else {
- value
- }
+ val firebaseAuthCredential = env("FIREBASE_ADMIN_AUTH_CREDENTIALS") {
+ Base64.getDecoder().decode(it).decodeToString()
}
- val dbHost: String = envVar("DB_HOST").orEmpty()
- val dbUsername: String = envVar("DB_USERNAME").orEmpty()
- val firebaseAuthCredential = envVar("FIREBASE_ADMIN_AUTH_CREDENTIALS")?.let {
- Base64.getDecoder().decode(it).decodeToString()
- }.orEmpty()
+ val forexApiKey = env("FOREX_API_KEY")
if (violations.isNotEmpty()) {
throw BootstrapException(violations)
} else {
Conf(
- api = Conf.Api(appPort!!, cors = env.getOrDefault("CORS", ".*.")),
+ api = Conf.Api(appPort!!, cors = cors!!),
environment = environment!!,
db = Conf.DB(
- host = dbHost,
+ host = dbHost!!,
port = dbPort!!,
- name = dbName,
- username = dbUsername,
- password = env.getOrDefault("DB_PASSWORD", "password"),
+ name = dbName!!,
+ username = dbUsername!!,
+ password = dbPassword!!,
poolSize = dbPoolSize!!
),
backgroundTask = Conf.BackgroundTask(
- dashboardEnabled!!,
- env.getOrDefault("DASHBOARD_USER", "admin"),
- env.getOrDefault("DASHBOARD_PASS", "admin")
+ dashboardEnabled = dashboardEnabled!!,
+ dashboardUserName = dashboardUser!!,
+ dashboardPassword = dashboardPassword!!
),
- firebaseAuthCredential = firebaseAuthCredential
+ firebaseAuthCredential = firebaseAuthCredential!!,
+ forexApiKey = forexApiKey!!
)
}
}
diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/ForexRate.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/ForexRate.kt
new file mode 100644
index 0000000..286e35f
--- /dev/null
+++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/domain/ForexRate.kt
@@ -0,0 +1,11 @@
+package me.sujanpoudel.playdeals.domain
+
+import java.time.OffsetDateTime
+
+// Rates are USD based
+data class ForexRate(
+ val timestamp: OffsetDateTime,
+ val rates: List
+)
+
+data class ConversionRate(val currency: String, val rate: Float)
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 afdb2dd..2b46299 100644
--- a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/BackgroundJobsVerticle.kt
+++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/BackgroundJobsVerticle.kt
@@ -1,6 +1,7 @@
package me.sujanpoudel.playdeals.jobs
import io.vertx.kotlin.coroutines.CoroutineVerticle
+import me.sujanpoudel.playdeals.repositories.KeyValuesRepository
import org.jobrunr.configuration.JobRunr
import org.jobrunr.configuration.JobRunrConfiguration
import org.jobrunr.scheduling.JobRequestScheduler
@@ -14,16 +15,22 @@ class BackgroundJobsVerticle(
) : CoroutineVerticle(), DIAware {
private val jobRequestScheduler by instance()
+ private val keyValuesRepository by instance()
override suspend fun start() {
direct.instance()
setupRecurringJobs()
}
- private fun setupRecurringJobs() {
+ private suspend fun setupRecurringJobs() {
jobRequestScheduler.createRecurrently(RedditPostsScrapper.Request())
jobRequestScheduler.createRecurrently(AndroidAppExpiryCheckScheduler.Request())
jobRequestScheduler.createRecurrently(DealSummarizer.Request())
+ jobRequestScheduler.createRecurrently(ForexFetcher.Request())
+
+ if (keyValuesRepository.getForexRate() == null) {
+ jobRequestScheduler.enqueue(ForexFetcher.Request.immediate())
+ }
}
override suspend fun stop() {
diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt
new file mode 100644
index 0000000..5d87dd3
--- /dev/null
+++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/jobs/ForexFetcher.kt
@@ -0,0 +1,91 @@
+package me.sujanpoudel.playdeals.jobs
+
+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 me.sujanpoudel.playdeals.Conf
+import me.sujanpoudel.playdeals.common.SIMPLE_NAME
+import me.sujanpoudel.playdeals.common.loggingExecutionTime
+import me.sujanpoudel.playdeals.domain.ConversionRate
+import me.sujanpoudel.playdeals.domain.ForexRate
+import me.sujanpoudel.playdeals.logger
+import me.sujanpoudel.playdeals.repositories.KeyValuesRepository
+import org.jobrunr.jobs.lambdas.JobRequest
+import org.jobrunr.scheduling.RecurringJobBuilder
+import org.kodein.di.DI
+import org.kodein.di.DIAware
+import org.kodein.di.instance
+import java.time.Duration
+import java.time.OffsetDateTime
+import java.time.ZoneOffset
+import java.util.UUID
+
+class ForexFetcher(
+ override val di: DI,
+ 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")
+ )
+ }
+
+ 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)
+ }
+
+ private suspend fun getForexRates(): ForexRate {
+ 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 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 {
+ ConversionRate(it.key, (it.value as Number).toFloat() / usdRate)
+ }
+ )
+ }
+
+ class Request private constructor() : JobRequest {
+ override fun getJobRequestHandler() = ForexFetcher::class.java
+
+ 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)
+
+ fun immediate(): JobRequest = Request()
+ }
+ }
+}
+
+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.saveForexRate(forexRate: ForexRate) = set(KEY_FOREX_RATE, Json.encode(forexRate))
diff --git a/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetForexUseCase.kt b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetForexUseCase.kt
new file mode 100644
index 0000000..a9ddfbe
--- /dev/null
+++ b/backend/src/main/kotlin/me/sujanpoudel/playdeals/usecases/GetForexUseCase.kt
@@ -0,0 +1,16 @@
+package me.sujanpoudel.playdeals.usecases
+
+import me.sujanpoudel.playdeals.domain.ForexRate
+import me.sujanpoudel.playdeals.jobs.getForexRate
+import me.sujanpoudel.playdeals.repositories.KeyValuesRepository
+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? {
+ return appDealsRepository.getForexRate()
+ }
+}
diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt
index 1f6f1c0..adad98c 100644
--- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt
+++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/IntegrationTest.kt
@@ -56,7 +56,8 @@ abstract class IntegrationTest(private val vertx: Vertx) {
"admin",
"admin"
),
- firebaseAuthCredential = ""
+ firebaseAuthCredential = "",
+ forexApiKey = ""
)
var di = configureDI(vertx, conf)
diff --git a/backend/src/test/kotlin/me/sujanpoudel/playdeals/MainTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/MainTest.kt
index 3b374b8..0c8e1ee 100644
--- a/backend/src/test/kotlin/me/sujanpoudel/playdeals/MainTest.kt
+++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/MainTest.kt
@@ -3,7 +3,6 @@ package me.sujanpoudel.playdeals
import com.github.michaelbull.result.unwrap
import com.github.michaelbull.result.unwrapError
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
-import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.collections.shouldNotBeEmpty
import io.kotest.matchers.shouldBe
import me.sujanpoudel.playdeals.common.BootstrapException
@@ -14,6 +13,7 @@ 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",
@@ -28,14 +28,14 @@ class MainTest {
"DB_PORT" to "3333",
"DB_NAME" to "db-name",
- "APP_PORT" to "123",
-
"FIREBASE_ADMIN_AUTH_CREDENTIALS" to "dGVzdF9jcmVk",
- "ENV" to "DEVELOPMENT"
+ "FOREX_API_KEY" to "forex_key"
)
val conf = buildConf(env).unwrap()
+ conf.environment shouldBe Environment.DEVELOPMENT
+ conf.api.port shouldBe 123
conf.api.cors shouldBe "*.example.com"
conf.backgroundTask.dashboardEnabled shouldBe true
@@ -49,41 +49,40 @@ class MainTest {
conf.db.username shouldBe "u"
conf.db.port shouldBe 3333
- conf.api.port shouldBe 123
-
- conf.environment shouldBe Environment.DEVELOPMENT
- }
-
- @Test
- fun `Should fail on first critical incorrect val from env`() {
- val env = mutableMapOf(
- "ENV" to "prod"
- )
-
- val err = buildConf(env).unwrapError()
- err.printStackTrace()
- val violations = (err as BootstrapException).violations
- violations.shouldHaveSize(4) shouldContainExactlyInAnyOrder listOf(
- "Invalid ENV",
- "No DB_HOST env var defined!",
- "No DB_USERNAME env var defined!",
- "No FIREBASE_ADMIN_AUTH_CREDENTIALS env var defined!"
- )
+ conf.firebaseAuthCredential shouldBe "test_cred"
+ conf.forexApiKey shouldBe "forex_key"
}
@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",
- "DB_PORT" to "3333",
- "DB_NAME" to "db-name",
- "FIREBASE_ADMIN_AUTH_CREDENTIALS" to "dGVzdF9jcmVk"
+ "FIREBASE_ADMIN_AUTH_CREDENTIALS" to "dGVzdF9jcmVk",
+ "FOREX_API_KEY" to "forex_key"
)
val conf = buildConf(env).unwrap()
+
+ conf.environment shouldBe Environment.PRODUCTION
conf.api.port shouldBe 8888
+ conf.api.cors shouldBe ".*."
+
+ conf.backgroundTask.dashboardEnabled shouldBe true
+ conf.backgroundTask.dashboardUserName shouldBe "admin"
+ conf.backgroundTask.dashboardPassword shouldBe "admin"
+
+ conf.db.name shouldBe "play_deals"
+ conf.db.host shouldBe "localhost"
+ conf.db.password shouldBe "p"
+ conf.db.poolSize shouldBe 5
+ conf.db.username shouldBe "u"
+ conf.db.port shouldBe 3333
+
+ conf.firebaseAuthCredential shouldBe "test_cred"
+ conf.forexApiKey shouldBe "forex_key"
}
@Test
@@ -96,11 +95,16 @@ class MainTest {
val violations = ((buildConf(env).unwrapError()) as BootstrapException).violations
violations.shouldNotBeEmpty()
violations shouldContainExactlyInAnyOrder listOf(
- "Invalid APP_PORT",
- "Invalid ENV",
- "No DB_HOST env var defined!",
- "No DB_USERNAME env var defined!",
- "No FIREBASE_ADMIN_AUTH_CREDENTIALS env var defined!"
+ "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/forex/GetForexApiTest.kt b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt
new file mode 100644
index 0000000..5b40a58
--- /dev/null
+++ b/backend/src/test/kotlin/me/sujanpoudel/playdeals/api/forex/GetForexApiTest.kt
@@ -0,0 +1,51 @@
+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 me.sujanpoudel.playdeals.IntegrationTest
+import me.sujanpoudel.playdeals.domain.ConversionRate
+import me.sujanpoudel.playdeals.domain.ForexRate
+import me.sujanpoudel.playdeals.get
+import me.sujanpoudel.playdeals.jobs.saveForexRate
+import me.sujanpoudel.playdeals.repositories.KeyValuesRepository
+import org.junit.jupiter.api.Test
+import java.time.OffsetDateTime
+
+class GetForexApiTest(vertx: Vertx) : IntegrationTest(vertx) {
+ @Test
+ fun `should return forex if there is data`() = runTest {
+ val repository = di.get()
+
+ val forexRate = ForexRate(OffsetDateTime.now(), listOf(ConversionRate("USD", 1.1f)))
+
+ repository.saveForexRate(forexRate)
+
+ val response = httpClient.get("/api/forex")
+ .send()
+ .await()
+ .bodyAsJsonObject()
+
+ response.getJsonObject("data").also { data ->
+ OffsetDateTime.parse(data.getString("timestamp")).toEpochSecond() shouldBe forexRate.timestamp.toEpochSecond()
+ data.getJsonArray("rates").also { rates ->
+ rates.size() shouldBe 1
+ (rates.first() as JsonObject).also { rate ->
+ rate.getString("currency") shouldBe "USD"
+ 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()
+
+ response.getJsonObject("data") shouldBe null
+ }
+}
diff --git a/helm/backend/templates/deployment.yaml b/helm/backend/templates/deployment.yaml
index 9d36e2d..3c27230 100644
--- a/helm/backend/templates/deployment.yaml
+++ b/helm/backend/templates/deployment.yaml
@@ -52,6 +52,8 @@ spec:
value: {{ .Values.backgroundTask.dashboardPass | quote }}
- name: "FIREBASE_ADMIN_AUTH_CREDENTIALS"
value: {{ .Values.firebaseAdminAuthCredential | quote }}
+ - name: "FOREX_API_KEY"
+ value: {{ .Values.forexApiKey | quote }}
ports:
- name: api
containerPort: 8888
diff --git a/justfile b/justfile
index 9015e9e..f39a89f 100644
--- a/justfile
+++ b/justfile
@@ -43,6 +43,7 @@ helm-upgrade imageTag=tag:
--set backgroundTask.dashboardUser=$DASHBOARD_USER \
--set backgroundTask.dashboardPass=$DASHBOARD_PASS \
--set firebaseAdminAuthCredential=$FIREBASE_ADMIN_AUTH_CREDENTIALS \
+ --set forexApiKey=$FOREX_API_KEY \
--set image.tag={{imageTag}} \
--set image.repository={{imageRepo}}