Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-24 18:55:42 +01:00
parent a629de6271
commit 3cef7c5092
2161 changed files with 246605 additions and 2 deletions

View file

@ -0,0 +1,9 @@
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id(ThunderbirdPlugins.Library.jvm)
alias(libs.plugins.android.lint)
}
dependencies {
testImplementation(projects.core.testing)
}

View file

@ -0,0 +1,9 @@
package app.k9mail.core.common
import kotlinx.datetime.Clock
import org.koin.core.module.Module
import org.koin.dsl.module
val coreCommonModule: Module = module {
single<Clock> { Clock.System }
}

View file

@ -0,0 +1,12 @@
package app.k9mail.core.common.cache
interface Cache<KEY : Any, VALUE : Any?> {
operator fun get(key: KEY): VALUE?
operator fun set(key: KEY, value: VALUE)
fun hasKey(key: KEY): Boolean
fun clear()
}

View file

@ -0,0 +1,46 @@
package app.k9mail.core.common.cache
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
class ExpiringCache<KEY : Any, VALUE : Any?>(
private val clock: Clock,
private val delegateCache: Cache<KEY, VALUE> = InMemoryCache(),
private var lastClearTime: Instant = clock.now(),
private val cacheTimeValidity: Long = CACHE_TIME_VALIDITY_IN_MILLIS,
) : Cache<KEY, VALUE> {
override fun get(key: KEY): VALUE? {
recycle()
return delegateCache[key]
}
override fun set(key: KEY, value: VALUE) {
recycle()
delegateCache[key] = value
}
override fun hasKey(key: KEY): Boolean {
recycle()
return delegateCache.hasKey(key)
}
override fun clear() {
lastClearTime = clock.now()
delegateCache.clear()
}
private fun recycle() {
if (isExpired()) {
clear()
}
}
private fun isExpired(): Boolean {
return (clock.now() - lastClearTime).inWholeMilliseconds >= cacheTimeValidity
}
private companion object {
const val CACHE_TIME_VALIDITY_IN_MILLIS = 30_000L
}
}

View file

@ -0,0 +1,21 @@
package app.k9mail.core.common.cache
class InMemoryCache<KEY : Any, VALUE : Any?>(
private val cache: MutableMap<KEY, VALUE> = mutableMapOf(),
) : Cache<KEY, VALUE> {
override fun get(key: KEY): VALUE? {
return cache[key]
}
override fun set(key: KEY, value: VALUE) {
cache[key] = value
}
override fun hasKey(key: KEY): Boolean {
return cache.containsKey(key)
}
override fun clear() {
cache.clear()
}
}

View file

@ -0,0 +1,30 @@
package app.k9mail.core.common.cache
class SynchronizedCache<KEY : Any, VALUE : Any?>(
private val delegateCache: Cache<KEY, VALUE>,
) : Cache<KEY, VALUE> {
override fun get(key: KEY): VALUE? {
synchronized(delegateCache) {
return delegateCache[key]
}
}
override fun set(key: KEY, value: VALUE) {
synchronized(delegateCache) {
delegateCache[key] = value
}
}
override fun hasKey(key: KEY): Boolean {
synchronized(delegateCache) {
return delegateCache.hasKey(key)
}
}
override fun clear() {
synchronized(delegateCache) {
delegateCache.clear()
}
}
}

View file

@ -0,0 +1,8 @@
package app.k9mail.core.common.mail
@JvmInline
value class EmailAddress(val address: String) {
init {
require(address.isNotBlank()) { "Email address must not be blank" }
}
}

View file

@ -0,0 +1,16 @@
package app.k9mail.core.common
import org.junit.Test
import org.koin.dsl.koinApplication
import org.koin.test.check.checkModules
internal class CoreCommonModuleKtTest {
@Test
fun `should have a valid di module`() {
koinApplication {
modules(coreCommonModule)
checkModules()
}
}
}

View file

@ -0,0 +1,81 @@
package app.k9mail.core.common.cache
import app.k9mail.core.testing.TestClock
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isNull
import assertk.assertions.isTrue
import kotlin.test.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
data class CacheTestData<KEY : Any, VALUE : Any?>(
val name: String,
val createCache: () -> Cache<KEY, VALUE>
) {
override fun toString(): String = name
}
@RunWith(Parameterized::class)
class CacheTest(data: CacheTestData<Any, Any?>) {
private val testSubject = data.createCache()
companion object {
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun data(): Collection<CacheTestData<Any, Any?>> {
return listOf(
CacheTestData("InMemoryCache") { InMemoryCache() },
CacheTestData("ExpiringCache") { ExpiringCache(TestClock(), InMemoryCache()) },
CacheTestData("SynchronizedCache") { SynchronizedCache(InMemoryCache()) }
)
}
const val KEY = "key"
const val VALUE = "value"
}
@Test
fun `get should return null with empty cache`() {
assertThat(testSubject[KEY]).isNull()
}
@Test
fun `set should add entry with empty cache`() {
testSubject[KEY] = VALUE
assertThat(testSubject[KEY]).isEqualTo(VALUE)
}
@Test
fun `set should overwrite entry when already present`() {
testSubject[KEY] = VALUE
testSubject[KEY] = "$VALUE changed"
assertThat(testSubject[KEY]).isEqualTo("$VALUE changed")
}
@Test
fun `hasKey should answer no with empty cache`() {
assertThat(testSubject.hasKey(KEY)).isFalse()
}
@Test
fun `hasKey should answer yes when cache has entry`() {
testSubject[KEY] = VALUE
assertThat(testSubject.hasKey(KEY)).isTrue()
}
@Test
fun `clear should empty cache`() {
testSubject[KEY] = VALUE
testSubject.clear()
assertThat(testSubject[KEY]).isNull()
}
}

View file

@ -0,0 +1,68 @@
package app.k9mail.core.common.cache
import app.k9mail.core.testing.TestClock
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isNull
import kotlin.test.Test
import kotlin.time.Duration.Companion.milliseconds
class ExpiringCacheTest {
private val clock = TestClock()
private val testSubject: Cache<String, String> = ExpiringCache(clock, InMemoryCache())
@Test
fun `get should return null when entry present and cache expired`() {
testSubject[KEY] = VALUE
clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION)
val result = testSubject[KEY]
assertThat(result).isNull()
}
@Test
fun `set should clear cache and add new entry when cache expired`() {
testSubject[KEY] = VALUE
clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION)
testSubject[KEY + 1] = "$VALUE changed"
assertThat(testSubject[KEY]).isNull()
assertThat(testSubject[KEY + 1]).isEqualTo("$VALUE changed")
}
@Test
fun `hasKey should answer no when cache has entry and validity expired`() {
testSubject[KEY] = VALUE
clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION)
assertThat(testSubject.hasKey(KEY)).isFalse()
}
@Test
fun `should keep cache when time progresses within expiration`() {
testSubject[KEY] = VALUE
clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION.minus(1L.milliseconds))
assertThat(testSubject[KEY]).isEqualTo(VALUE)
}
@Test
fun `should empty cache after time progresses to expiration`() {
testSubject[KEY] = VALUE
clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION)
assertThat(testSubject[KEY]).isNull()
}
private companion object {
const val KEY = "key"
const val VALUE = "value"
val CACHE_TIME_VALIDITY_DURATION = 30_000L.milliseconds
}
}

View file

@ -0,0 +1,29 @@
package app.k9mail.core.common.mail
import assertk.assertThat
import assertk.assertions.isEqualTo
import kotlin.test.Test
import kotlin.test.assertFails
internal class EmailAddressTest {
@Test
fun `should reject blank email address`() {
assertFails("Email address must not be blank") {
EmailAddress("")
}
}
@Test
fun `should return email address`() {
val emailAddress = EmailAddress(EMAIL_ADDRESS)
val address = emailAddress.address
assertThat(address).isEqualTo(EMAIL_ADDRESS)
}
private companion object {
private const val EMAIL_ADDRESS = "email@example.com"
}
}