diff options
| author | test@dangofactory.cos <you@example.com> | 2026-08-20 16:57:18 +0000 |
|---|---|---|
| committer | test@dangofactory.cos <you@example.com> | 2026-08-20 16:57:18 +0000 |
| commit | 50ce5d5e6cd0d720ff437d35eace545816848428 (patch) | |
| tree | b59af206a36265cf081945e434900b32b0c43edd /core/android/common/src | |
| download | monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.gz monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.bz2 monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.zip | |
Inital
Diffstat (limited to 'core/android/common/src')
17 files changed, 785 insertions, 0 deletions
diff --git a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/CoreCommonAndroidModule.kt b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/CoreCommonAndroidModule.kt new file mode 100644 index 0000000..5a0b599 --- /dev/null +++ b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/CoreCommonAndroidModule.kt @@ -0,0 +1,12 @@ +package app.k9mail.core.android.common + +import app.k9mail.core.android.common.contact.contactModule +import app.k9mail.core.common.coreCommonModule +import org.koin.core.module.Module +import org.koin.dsl.module + +val coreCommonAndroidModule: Module = module { + includes(coreCommonModule) + + includes(contactModule) +} diff --git a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/Contact.kt b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/Contact.kt new file mode 100644 index 0000000..cc5d3d1 --- /dev/null +++ b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/Contact.kt @@ -0,0 +1,12 @@ +package app.k9mail.core.android.common.contact + +import android.net.Uri +import app.k9mail.core.common.mail.EmailAddress + +data class Contact( + val id: Long, + val name: String?, + val emailAddress: EmailAddress, + val uri: Uri, + val photoUri: Uri?, +) diff --git a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactDataSource.kt b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactDataSource.kt new file mode 100644 index 0000000..934cacc --- /dev/null +++ b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactDataSource.kt @@ -0,0 +1,86 @@ +package app.k9mail.core.android.common.contact + +import android.content.ContentResolver +import android.database.Cursor +import android.net.Uri +import android.provider.ContactsContract +import app.k9mail.core.android.common.database.EmptyCursor +import app.k9mail.core.android.common.database.getLongOrThrow +import app.k9mail.core.android.common.database.getStringOrNull +import app.k9mail.core.common.mail.EmailAddress + +interface ContactDataSource { + + fun getContactFor(emailAddress: EmailAddress): Contact? + + fun hasContactFor(emailAddress: EmailAddress): Boolean +} + +internal class ContentResolverContactDataSource( + private val contentResolver: ContentResolver, + private val contactPermissionResolver: ContactPermissionResolver, +) : ContactDataSource { + + override fun getContactFor(emailAddress: EmailAddress): Contact? { + getCursorFor(emailAddress).use { cursor -> + if (cursor.moveToFirst()) { + val contactId = cursor.getLongOrThrow(ContactsContract.CommonDataKinds.Email._ID) + val lookupKey = cursor.getStringOrNull(ContactsContract.Contacts.LOOKUP_KEY) + val uri = ContactsContract.Contacts.getLookupUri(contactId, lookupKey) + + val name = cursor.getStringOrNull(ContactsContract.CommonDataKinds.Identity.DISPLAY_NAME) + + val photoUri = cursor.getStringOrNull(ContactsContract.CommonDataKinds.Photo.PHOTO_URI) + ?.let { photoUriString -> Uri.parse(photoUriString) } + + return Contact( + id = contactId, + name = name, + emailAddress = emailAddress, + uri = uri, + photoUri = photoUri, + ) + } else { + return null + } + } + } + + override fun hasContactFor(emailAddress: EmailAddress): Boolean { + getCursorFor(emailAddress).use { cursor -> + return cursor.count > 0 + } + } + + private fun getCursorFor(emailAddress: EmailAddress): Cursor { + return if (contactPermissionResolver.hasContactPermission()) { + val uri = Uri.withAppendedPath( + ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, + Uri.encode(emailAddress.address), + ) + + contentResolver.query( + uri, + PROJECTION, + null, + null, + SORT_ORDER, + ) ?: EmptyCursor() + } else { + EmptyCursor() + } + } + + private companion object { + + private const val SORT_ORDER = ContactsContract.Contacts.DISPLAY_NAME + + ", " + ContactsContract.CommonDataKinds.Email._ID + + private val PROJECTION = arrayOf( + ContactsContract.CommonDataKinds.Email._ID, + ContactsContract.CommonDataKinds.Identity.DISPLAY_NAME, + ContactsContract.CommonDataKinds.Photo.PHOTO_URI, + ContactsContract.Contacts.LOOKUP_KEY, + ) + } +} diff --git a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactKoinModule.kt b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactKoinModule.kt new file mode 100644 index 0000000..db931ee --- /dev/null +++ b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactKoinModule.kt @@ -0,0 +1,34 @@ +package app.k9mail.core.android.common.contact + +import android.content.Context +import app.k9mail.core.common.cache.Cache +import app.k9mail.core.common.cache.ExpiringCache +import app.k9mail.core.common.cache.SynchronizedCache +import app.k9mail.core.common.mail.EmailAddress +import org.koin.core.qualifier.named +import org.koin.dsl.module + +internal val contactModule = module { + single<Cache<EmailAddress, Contact?>>(named(CACHE_NAME)) { + SynchronizedCache( + delegateCache = ExpiringCache(clock = get()), + ) + } + factory<ContactDataSource> { + ContentResolverContactDataSource( + contentResolver = get<Context>().contentResolver, + contactPermissionResolver = get(), + ) + } + factory<ContactRepository> { + CachingContactRepository( + cache = get(named(CACHE_NAME)), + dataSource = get(), + ) + } + factory<ContactPermissionResolver> { + AndroidContactPermissionResolver(context = get()) + } +} + +internal const val CACHE_NAME = "ContactCache" diff --git a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactPermissionResolver.kt b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactPermissionResolver.kt new file mode 100644 index 0000000..d0490c1 --- /dev/null +++ b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactPermissionResolver.kt @@ -0,0 +1,16 @@ +package app.k9mail.core.android.common.contact + +import android.Manifest.permission.READ_CONTACTS +import android.content.Context +import android.content.pm.PackageManager.PERMISSION_GRANTED +import androidx.core.content.ContextCompat + +interface ContactPermissionResolver { + fun hasContactPermission(): Boolean +} + +internal class AndroidContactPermissionResolver(private val context: Context) : ContactPermissionResolver { + override fun hasContactPermission(): Boolean { + return ContextCompat.checkSelfPermission(context, READ_CONTACTS) == PERMISSION_GRANTED + } +} diff --git a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactRepository.kt b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactRepository.kt new file mode 100644 index 0000000..3378a2d --- /dev/null +++ b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactRepository.kt @@ -0,0 +1,48 @@ +package app.k9mail.core.android.common.contact + +import app.k9mail.core.common.cache.Cache +import app.k9mail.core.common.mail.EmailAddress + +interface ContactRepository { + + fun getContactFor(emailAddress: EmailAddress): Contact? + + fun hasContactFor(emailAddress: EmailAddress): Boolean + + fun hasAnyContactFor(emailAddresses: List<EmailAddress>): Boolean +} + +interface CachingRepository { + fun clearCache() +} + +internal class CachingContactRepository( + private val cache: Cache<EmailAddress, Contact?>, + private val dataSource: ContactDataSource, +) : ContactRepository, CachingRepository { + + override fun getContactFor(emailAddress: EmailAddress): Contact? { + if (cache.hasKey(emailAddress)) { + return cache[emailAddress] + } + + return dataSource.getContactFor(emailAddress).also { + cache[emailAddress] = it + } + } + + override fun hasContactFor(emailAddress: EmailAddress): Boolean { + if (cache.hasKey(emailAddress)) { + return cache[emailAddress] != null + } + + return dataSource.hasContactFor(emailAddress) + } + + override fun hasAnyContactFor(emailAddresses: List<EmailAddress>): Boolean = + emailAddresses.any { emailAddress -> hasContactFor(emailAddress) } + + override fun clearCache() { + cache.clear() + } +} diff --git a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/database/CursorExtensions.kt b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/database/CursorExtensions.kt new file mode 100644 index 0000000..44b82b9 --- /dev/null +++ b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/database/CursorExtensions.kt @@ -0,0 +1,37 @@ +package app.k9mail.core.android.common.database + +import android.database.Cursor + +fun <T> Cursor.map(block: (Cursor) -> T): List<T> { + return List(count) { index -> + moveToPosition(index) + block(this) + } +} + +fun Cursor.getStringOrNull(columnName: String): String? { + val columnIndex = getColumnIndex(columnName) + return if (isNull(columnIndex)) null else getString(columnIndex) +} + +fun Cursor.getIntOrNull(columnName: String): Int? { + val columnIndex = getColumnIndex(columnName) + return if (isNull(columnIndex)) null else getInt(columnIndex) +} + +fun Cursor.getLongOrNull(columnName: String): Long? { + val columnIndex = getColumnIndex(columnName) + return if (isNull(columnIndex)) null else getLong(columnIndex) +} + +fun Cursor.getStringOrThrow(columnName: String): String { + return getStringOrNull(columnName) ?: error("Column $columnName must not be null") +} + +fun Cursor.getIntOrThrow(columnName: String): Int { + return getIntOrNull(columnName) ?: error("Column $columnName must not be null") +} + +fun Cursor.getLongOrThrow(columnName: String): Long { + return getLongOrNull(columnName) ?: error("Column $columnName must not be null") +} diff --git a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/database/EmptyCursor.kt b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/database/EmptyCursor.kt new file mode 100644 index 0000000..a5a4d6f --- /dev/null +++ b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/database/EmptyCursor.kt @@ -0,0 +1,26 @@ +package app.k9mail.core.android.common.database + +import android.database.AbstractCursor + +/** + * A dummy class that provides an empty cursor + */ +class EmptyCursor : AbstractCursor() { + override fun getCount() = 0 + + override fun getColumnNames() = arrayOf<String>() + + override fun getString(column: Int) = null + + override fun getShort(column: Int): Short = 0 + + override fun getInt(column: Int) = 0 + + override fun getLong(column: Int): Long = 0 + + override fun getFloat(column: Int) = 0f + + override fun getDouble(column: Int) = 0.0 + + override fun isNull(column: Int) = true +} diff --git a/core/android/common/src/test/kotlin/app/k9mail/core/android/common/CoreCommonAndroidModuleKtTest.kt b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/CoreCommonAndroidModuleKtTest.kt new file mode 100644 index 0000000..5d1a3d7 --- /dev/null +++ b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/CoreCommonAndroidModuleKtTest.kt @@ -0,0 +1,22 @@ +package app.k9mail.core.android.common + +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.koinApplication +import org.koin.test.check.checkModules +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +internal class CoreCommonAndroidModuleKtTest { + + @Test + fun `should have a valid di module`() { + koinApplication { + modules(coreCommonAndroidModule) + androidContext(RuntimeEnvironment.getApplication()) + checkModules() + } + } +} diff --git a/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/AndroidContactPermissionResolverTest.kt b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/AndroidContactPermissionResolverTest.kt new file mode 100644 index 0000000..ea0dc67 --- /dev/null +++ b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/AndroidContactPermissionResolverTest.kt @@ -0,0 +1,43 @@ +package app.k9mail.core.android.common.contact + +import android.Manifest +import assertk.assertThat +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows + +@RunWith(RobolectricTestRunner::class) +class AndroidContactPermissionResolverTest { + private val application = RuntimeEnvironment.getApplication() + private val testSubject = AndroidContactPermissionResolver(context = application) + + @Test + fun `hasPermission() with contact permission`() { + grantContactPermission() + + val result = testSubject.hasContactPermission() + + assertThat(result).isTrue() + } + + @Test + fun `hasPermission() without contact permission`() { + denyContactPermission() + + val result = testSubject.hasContactPermission() + + assertThat(result).isFalse() + } + + private fun grantContactPermission() { + Shadows.shadowOf(application).grantPermissions(Manifest.permission.READ_CONTACTS) + } + + private fun denyContactPermission() { + Shadows.shadowOf(application).denyPermissions(Manifest.permission.READ_CONTACTS) + } +} diff --git a/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/CachingContactRepositoryTest.kt b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/CachingContactRepositoryTest.kt new file mode 100644 index 0000000..5ea564e --- /dev/null +++ b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/CachingContactRepositoryTest.kt @@ -0,0 +1,143 @@ +package app.k9mail.core.android.common.contact + +import app.k9mail.core.common.cache.InMemoryCache +import app.k9mail.core.common.mail.EmailAddress +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.Before +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doReturnConsecutively +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class CachingContactRepositoryTest { + + private val dataSource = mock<ContactDataSource>() + private val cache = InMemoryCache<EmailAddress, Contact?>() + + private val testSubject = CachingContactRepository(cache = cache, dataSource = dataSource) + + @Before + fun setUp() { + cache.clear() + } + + @Test + fun `getContactFor() returns null if no contact exists`() { + val result = testSubject.getContactFor(CONTACT_EMAIL_ADDRESS) + + assertThat(result).isNull() + } + + @Test + fun `getContactFor() returns contact if it exists`() { + dataSource.stub { on { getContactFor(CONTACT_EMAIL_ADDRESS) } doReturn CONTACT } + + val result = testSubject.getContactFor(CONTACT_EMAIL_ADDRESS) + + assertThat(result).isEqualTo(CONTACT) + } + + @Test + fun `getContactFor() caches contact`() { + dataSource.stub { + on { getContactFor(CONTACT_EMAIL_ADDRESS) } doReturnConsecutively listOf( + CONTACT, + CONTACT.copy(id = 567L), + ) + } + + val result1 = testSubject.getContactFor(CONTACT_EMAIL_ADDRESS) + val result2 = testSubject.getContactFor(CONTACT_EMAIL_ADDRESS) + + assertThat(result1).isEqualTo(result2) + } + + @Test + fun `getContactFor() caches null`() { + dataSource.stub { + on { getContactFor(CONTACT_EMAIL_ADDRESS) } doReturnConsecutively listOf( + null, + CONTACT, + ) + } + + val result1 = testSubject.getContactFor(CONTACT_EMAIL_ADDRESS) + val result2 = testSubject.getContactFor(CONTACT_EMAIL_ADDRESS) + + assertThat(result1).isEqualTo(result2) + } + + @Test + fun `getContactFor() returns cached contact`() { + cache[CONTACT_EMAIL_ADDRESS] = CONTACT + + val result = testSubject.getContactFor(CONTACT_EMAIL_ADDRESS) + + assertThat(result).isEqualTo(CONTACT) + } + + @Test + fun `hasContactFor() returns false if no contact exists`() { + val result = testSubject.hasContactFor(CONTACT_EMAIL_ADDRESS) + + assertThat(result).isFalse() + } + + @Test + fun `hasContactFor() returns false if cached contact is null`() { + cache[CONTACT_EMAIL_ADDRESS] = null + + val result = testSubject.hasContactFor(CONTACT_EMAIL_ADDRESS) + + assertThat(result).isFalse() + } + + @Test + fun `hasContactFor() returns true if contact exists`() { + dataSource.stub { on { hasContactFor(CONTACT_EMAIL_ADDRESS) } doReturn true } + + val result = testSubject.hasContactFor(CONTACT_EMAIL_ADDRESS) + + assertThat(result).isTrue() + } + + @Test + fun `hasAnyContactFor() returns false if no contact exists`() { + val result = testSubject.hasAnyContactFor(listOf(CONTACT_EMAIL_ADDRESS)) + + assertThat(result).isFalse() + } + + @Test + fun `hasAnyContactFor() returns false if list is empty`() { + val result = testSubject.hasAnyContactFor(listOf()) + + assertThat(result).isFalse() + } + + @Test + fun `hasAnyContactFor() returns true if contact exists`() { + dataSource.stub { on { hasContactFor(CONTACT_EMAIL_ADDRESS) } doReturn true } + + val result = testSubject.hasAnyContactFor(listOf(CONTACT_EMAIL_ADDRESS)) + + assertThat(result).isTrue() + } + + @Test + fun `clearCache() clears cache`() { + cache[CONTACT_EMAIL_ADDRESS] = CONTACT + + testSubject.clearCache() + + assertThat(cache[CONTACT_EMAIL_ADDRESS]).isNull() + } +} diff --git a/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/ContactFixture.kt b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/ContactFixture.kt new file mode 100644 index 0000000..600ca31 --- /dev/null +++ b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/ContactFixture.kt @@ -0,0 +1,19 @@ +package app.k9mail.core.android.common.contact + +import android.net.Uri +import app.k9mail.core.common.mail.EmailAddress + +const val CONTACT_ID = 123L +const val CONTACT_NAME = "user name" +const val CONTACT_LOOKUP_KEY = "0r1-4F314D4F2F294F29" +val CONTACT_EMAIL_ADDRESS = EmailAddress("user@example.com") +val CONTACT_URI: Uri = Uri.parse("content://com.android.contacts/contacts/lookup/$CONTACT_LOOKUP_KEY/$CONTACT_ID") +val CONTACT_PHOTO_URI: Uri = Uri.parse("content://com.android.contacts/display_photo/$CONTACT_ID") + +val CONTACT = Contact( + id = CONTACT_ID, + name = CONTACT_NAME, + emailAddress = CONTACT_EMAIL_ADDRESS, + uri = CONTACT_URI, + photoUri = CONTACT_PHOTO_URI, +) diff --git a/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/ContactKoinModuleKtTest.kt b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/ContactKoinModuleKtTest.kt new file mode 100644 index 0000000..863a7d1 --- /dev/null +++ b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/ContactKoinModuleKtTest.kt @@ -0,0 +1,25 @@ +package app.k9mail.core.android.common.contact + +import app.k9mail.core.android.common.coreCommonAndroidModule +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.koinApplication +import org.koin.test.check.checkModules +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +internal class ContactKoinModuleKtTest { + @Test + fun `should have a valid di module`() { + koinApplication { + modules(coreCommonAndroidModule) + + modules(contactModule) + + androidContext(RuntimeEnvironment.getApplication()) + checkModules() + } + } +} diff --git a/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/ContentResolverContactDataSourceTest.kt b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/ContentResolverContactDataSourceTest.kt new file mode 100644 index 0000000..d74ef93 --- /dev/null +++ b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/ContentResolverContactDataSourceTest.kt @@ -0,0 +1,120 @@ +package app.k9mail.core.android.common.contact + +import android.content.ContentResolver +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import android.provider.ContactsContract +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.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class ContentResolverContactDataSourceTest { + private val contactPermissionResolver = TestContactPermissionResolver(hasPermission = true) + private val contentResolver = mock<ContentResolver>() + + private val testSubject = ContentResolverContactDataSource( + contentResolver = contentResolver, + contactPermissionResolver = contactPermissionResolver, + ) + + @Test + fun `getContactForEmail() returns null if permission is not granted`() { + contactPermissionResolver.hasContactPermission = false + + val result = testSubject.getContactFor(CONTACT_EMAIL_ADDRESS) + + assertThat(result).isNull() + } + + @Test + fun `getContactForEmail() returns null if no contact is found`() { + setupContactProvider(setupEmptyContactCursor()) + + val result = testSubject.getContactFor(CONTACT_EMAIL_ADDRESS) + + assertThat(result).isNull() + } + + @Test + fun `getContactForEmail() returns contact if a contact is found`() { + setupContactProvider(setupContactCursor()) + + val result = testSubject.getContactFor(CONTACT_EMAIL_ADDRESS) + + assertThat(result).isEqualTo(CONTACT) + } + + @Test + fun `hasContactForEmail() returns false if permission is not granted`() { + contactPermissionResolver.hasContactPermission = false + + val result = testSubject.hasContactFor(CONTACT_EMAIL_ADDRESS) + + assertThat(result).isFalse() + } + + @Test + fun `hasContactForEmail() returns false if no contact is found`() { + setupContactProvider(setupEmptyContactCursor()) + + val result = testSubject.hasContactFor(CONTACT_EMAIL_ADDRESS) + + assertThat(result).isFalse() + } + + @Test + fun `hasContactForEmail() returns true if a contact is found`() { + setupContactProvider(setupContactCursor()) + + val result = testSubject.hasContactFor(CONTACT_EMAIL_ADDRESS) + + assertThat(result).isTrue() + } + + private fun setupContactProvider(contactCursor: Cursor) { + val emailUri = Uri.withAppendedPath( + ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, + Uri.encode(CONTACT_EMAIL_ADDRESS.address), + ) + + contentResolver.stub { + on { + query(eq(emailUri), eq(PROJECTION), anyOrNull(), anyOrNull(), eq(SORT_ORDER)) + } doReturn contactCursor + } + } + + private fun setupEmptyContactCursor(): Cursor { + return MatrixCursor(PROJECTION) + } + + private fun setupContactCursor(): Cursor { + return MatrixCursor(PROJECTION).apply { + addRow(arrayOf(CONTACT_ID, CONTACT_NAME, CONTACT_PHOTO_URI, CONTACT_LOOKUP_KEY)) + } + } + + private companion object { + val PROJECTION = arrayOf( + ContactsContract.CommonDataKinds.Email._ID, + ContactsContract.CommonDataKinds.Identity.DISPLAY_NAME, + ContactsContract.CommonDataKinds.Photo.PHOTO_URI, + ContactsContract.Contacts.LOOKUP_KEY, + ) + + const val SORT_ORDER = ContactsContract.Contacts.DISPLAY_NAME + + ", " + ContactsContract.CommonDataKinds.Email._ID + } +} diff --git a/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/TestContactPermissionResolver.kt b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/TestContactPermissionResolver.kt new file mode 100644 index 0000000..d0b7623 --- /dev/null +++ b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/TestContactPermissionResolver.kt @@ -0,0 +1,9 @@ +package app.k9mail.core.android.common.contact + +class TestContactPermissionResolver(hasPermission: Boolean) : ContactPermissionResolver { + var hasContactPermission = hasPermission + + override fun hasContactPermission(): Boolean { + return hasContactPermission + } +} diff --git a/core/android/common/src/test/kotlin/app/k9mail/core/android/common/database/CursorExtensionsKtAccessTest.kt b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/database/CursorExtensionsKtAccessTest.kt new file mode 100644 index 0000000..d228ae4 --- /dev/null +++ b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/database/CursorExtensionsKtAccessTest.kt @@ -0,0 +1,100 @@ +package app.k9mail.core.android.common.database + +import android.database.Cursor +import android.database.MatrixCursor +import assertk.assertThat +import assertk.assertions.hasMessage +import assertk.assertions.isEqualTo +import assertk.assertions.isFailure +import assertk.assertions.isNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.ParameterizedRobolectricTestRunner + +data class CursorExtensionsAccessTestData<T : Any>( + val name: String, + val value: T, + val access: (Cursor, String) -> T?, + val throwingAccess: (Cursor, String) -> T, +) { + override fun toString(): String = name +} + +@RunWith(ParameterizedRobolectricTestRunner::class) +class CursorExtensionsKtAccessTest(data: CursorExtensionsAccessTestData<Any>) { + + private val testValue = data.value + private val testAction = data.access + private val testThrowingAction = data.throwingAccess + + @Test + fun `testAction should return null if column is null`() { + val cursor = MatrixCursor(arrayOf("column")).apply { + addRow(arrayOf(null)) + } + + val result = cursor.map { testAction(it, "column") } + + assertThat(result[0]).isNull() + } + + @Test + fun `testAction should return value if column is not null`() { + val cursor = MatrixCursor(arrayOf("column")).apply { + addRow(arrayOf(testValue)) + } + + val result = cursor.map { testAction(it, "column") } + + assertThat(result[0]).isEqualTo(testValue) + } + + @Test + fun `testThrowingAction should throw if column is null`() { + val cursor = MatrixCursor(arrayOf("column")).apply { + addRow(arrayOf(null)) + } + + assertThat { + cursor.map { testThrowingAction(it, "column") } + }.isFailure().hasMessage("Column column must not be null") + } + + @Test + fun `testThrowingAction should return value if column is not null`() { + val cursor = MatrixCursor(arrayOf("column")).apply { + addRow(arrayOf(testValue)) + } + + val result = cursor.map { testThrowingAction(it, "column") } + + assertThat(result[0]).isEqualTo(testValue) + } + + companion object { + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters(name = "{0}") + fun data(): Collection<CursorExtensionsAccessTestData<Any>> { + return listOf( + CursorExtensionsAccessTestData( + name = "getString", + value = "value", + access = { cursor, column -> cursor.getStringOrNull(column) }, + throwingAccess = { cursor, column -> cursor.getStringOrThrow(column) }, + ), + CursorExtensionsAccessTestData( + name = "getInt", + value = Int.MAX_VALUE, + access = { cursor, column -> cursor.getIntOrNull(column) }, + throwingAccess = { cursor, column -> cursor.getIntOrThrow(column) }, + ), + CursorExtensionsAccessTestData( + name = "getLong", + value = Long.MAX_VALUE, + access = { cursor, column -> cursor.getLongOrNull(column) }, + throwingAccess = { cursor, column -> cursor.getLongOrThrow(column) }, + ), + ) + } + } +} diff --git a/core/android/common/src/test/kotlin/app/k9mail/core/android/common/database/CursorExtensionsKtTest.kt b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/database/CursorExtensionsKtTest.kt new file mode 100644 index 0000000..7601712 --- /dev/null +++ b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/database/CursorExtensionsKtTest.kt @@ -0,0 +1,33 @@ +package app.k9mail.core.android.common.database + +import android.database.MatrixCursor +import assertk.assertThat +import assertk.assertions.isEqualTo +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class CursorExtensionsKtTest { + + @Test + fun `map should return an empty list if cursor is empty`() { + val cursor = MatrixCursor(arrayOf("column")) + + val result = cursor.map { it.getStringOrNull("column") } + + assertThat(result).isEqualTo(emptyList<String>()) + } + + @Test + fun `map should return a list of mapped values`() { + val cursor = MatrixCursor(arrayOf("column")).apply { + addRow(arrayOf("value1")) + addRow(arrayOf("value2")) + } + + val result = cursor.map { it.getStringOrNull("column") } + + assertThat(result).isEqualTo(listOf("value1", "value2")) + } +} |
