Skip to content

Commit 93073df

Browse files
committed
refactor(data): abstract data manager and migrate to KMP tests
- Introduce `DataManagerProvider` interface and implement it in `DataManager`. - Refactor all repositories to depend on `DataManagerProvider` for better abstraction. - Remove legacy Mockito-based JVM unit tests. - Add KMP-compatible tests using Fakes (`FakeDataManager`, `FakeClientService`) and `Turbine`. - Implement `JsonLoader` and `TestResourceReader` utilities for loading test fixtures across platforms. - Update build configuration to include test resources and KMP test dependencies.
1 parent 97c3695 commit 93073df

File tree

46 files changed

+1141
-2269
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1141
-2269
lines changed

core/data/build.gradle.kts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ android {
2323
}
2424
}
2525

26+
sourceSets["test"].resources.srcDirs(
27+
"src/commonTest/resources"
28+
)
29+
2630
// defaultConfig {
2731
// consumerProguardFiles("consumer-rules.pro")
2832
// }
@@ -43,5 +47,13 @@ kotlin {
4347
implementation(libs.androidx.tracing.ktx)
4448
implementation(libs.koin.android)
4549
}
50+
commonTest.dependencies {
51+
implementation(libs.kotlin.test)
52+
implementation(libs.kotlin.test.annotations.common)
53+
implementation(libs.kotlinx.coroutines.test)
54+
implementation(libs.turbine)
55+
56+
implementation(libs.koin.test)
57+
}
4658
}
4759
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
9+
*/
10+
package org.mifos.mobile.core.data.utils
11+
12+
actual fun readResource(path: String): String {
13+
return requireNotNull(
14+
object {}.javaClass.classLoader
15+
?.getResourceAsStream(path),
16+
) {
17+
"Test resource not found: $path"
18+
}.bufferedReader().use { it.readText() }
19+
}

core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/AccountsRepositoryImp.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,24 @@ import org.mifos.mobile.core.common.DataState
1616
import org.mifos.mobile.core.common.asDataStateFlow
1717
import org.mifos.mobile.core.data.repository.AccountsRepository
1818
import org.mifos.mobile.core.model.entity.client.ClientAccounts
19-
import org.mifos.mobile.core.network.DataManager
19+
import org.mifos.mobile.core.network.DataManagerProvider
2020

2121
class AccountsRepositoryImp(
22-
private val dataManager: DataManager,
22+
private val dataManager: DataManagerProvider,
2323
private val ioDispatcher: CoroutineDispatcher,
2424
) : AccountsRepository {
2525

2626
override fun loadAccounts(clientId: Long?, accountType: String?): Flow<DataState<ClientAccounts>> {
27-
return dataManager.clientsApi.getAccounts(clientId!!, accountType)
28-
.asDataStateFlow().flowOn(ioDispatcher)
27+
// return dataManager.clientsApi.getAccounts(clientId!!, accountType)
28+
// .asDataStateFlow().flowOn(ioDispatcher)
29+
30+
val clientsApi = requireNotNull(dataManager.clientsApi) {
31+
"ClientService must be provided"
32+
}
33+
34+
return clientsApi
35+
.getAccounts(clientId!!, accountType)
36+
.asDataStateFlow()
37+
.flowOn(ioDispatcher)
2938
}
3039
}

core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/BeneficiaryRepositoryImp.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,20 @@ import org.mifos.mobile.core.model.entity.beneficiary.Beneficiary
2525
import org.mifos.mobile.core.model.entity.beneficiary.BeneficiaryPayload
2626
import org.mifos.mobile.core.model.entity.beneficiary.BeneficiaryUpdatePayload
2727
import org.mifos.mobile.core.model.entity.templates.beneficiary.BeneficiaryTemplate
28-
import org.mifos.mobile.core.network.DataManager
28+
import org.mifos.mobile.core.network.DataManagerProvider
2929

3030
class BeneficiaryRepositoryImp(
31-
private val dataManager: DataManager,
31+
private val dataManager: DataManagerProvider,
3232
private val ioDispatcher: CoroutineDispatcher,
3333
) : BeneficiaryRepository {
34+
35+
val beneficiaryApi = requireNotNull(dataManager.beneficiaryApi) {
36+
"BeneficiaryService must be provided"
37+
}
38+
3439
override fun beneficiaryTemplate(): Flow<DataState<BeneficiaryTemplate>> = flow {
3540
try {
36-
dataManager.beneficiaryApi.beneficiaryTemplate()
41+
beneficiaryApi.beneficiaryTemplate()
3742
.collect { response ->
3843
emit(DataState.Success(response))
3944
}
@@ -45,7 +50,7 @@ class BeneficiaryRepositoryImp(
4550
override suspend fun createBeneficiary(beneficiaryPayload: BeneficiaryPayload?): DataState<String> {
4651
return withContext(ioDispatcher) {
4752
try {
48-
val response = dataManager.beneficiaryApi.createBeneficiary(beneficiaryPayload)
53+
val response = beneficiaryApi.createBeneficiary(beneficiaryPayload)
4954

5055
DataState.Success(response.bodyAsText())
5156
} catch (e: ClientRequestException) {
@@ -65,7 +70,7 @@ class BeneficiaryRepositoryImp(
6570
): DataState<String> {
6671
return withContext(ioDispatcher) {
6772
try {
68-
val response = dataManager.beneficiaryApi.updateBeneficiary(beneficiaryId!!, payload)
73+
val response = beneficiaryApi.updateBeneficiary(beneficiaryId!!, payload)
6974
DataState.Success(response.bodyAsText())
7075
} catch (e: ClientRequestException) {
7176
val errorMessage = extractErrorMessage(e.response)
@@ -81,7 +86,7 @@ class BeneficiaryRepositoryImp(
8186
override suspend fun deleteBeneficiary(beneficiaryId: Long?): DataState<String> {
8287
return withContext(ioDispatcher) {
8388
try {
84-
val response = dataManager.beneficiaryApi.deleteBeneficiary(beneficiaryId!!)
89+
val response = beneficiaryApi.deleteBeneficiary(beneficiaryId!!)
8590

8691
DataState.Success(response.bodyAsText())
8792
} catch (e: ClientRequestException) {
@@ -97,7 +102,7 @@ class BeneficiaryRepositoryImp(
97102

98103
override fun beneficiaryList(): Flow<DataState<List<Beneficiary>>> = flow {
99104
try {
100-
dataManager.beneficiaryApi.beneficiaryList()
105+
beneficiaryApi.beneficiaryList()
101106
.collect { response ->
102107
emit(DataState.Success(response))
103108
}

core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/ClientChargeRepositoryImp.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,30 @@ import org.mifos.mobile.core.data.repository.ClientChargeRepository
2121
import org.mifos.mobile.core.model.entity.Charge
2222
import org.mifos.mobile.core.model.entity.Page
2323
import org.mifos.mobile.core.model.enums.ChargeType
24-
import org.mifos.mobile.core.network.DataManager
24+
import org.mifos.mobile.core.network.DataManagerProvider
2525

2626
class ClientChargeRepositoryImp(
27-
private val dataManager: DataManager,
27+
private val dataManager: DataManagerProvider,
2828
// private val chargeDao: ChargeDao,
2929
private val ioDispatcher: CoroutineDispatcher,
3030
) : ClientChargeRepository {
3131

32+
val clientChargeApi = requireNotNull(dataManager.clientChargeApi) {
33+
"ClientChargeService must be provided"
34+
}
35+
36+
val shareAccountApi = requireNotNull(dataManager.shareAccountApi) {
37+
"ShareAccountService must be provided"
38+
}
3239
override fun getCharges(clientId: Long): Flow<DataState<Page<Charge>>> {
33-
return dataManager.clientChargeApi.getClientChargeList(clientId)
40+
return clientChargeApi.getClientChargeList(clientId)
3441
.map { response -> DataState.Success(response) }
3542
.catch { exception -> DataState.Error(exception, exception.message) }
3643
.flowOn(ioDispatcher)
3744
}
3845

3946
override fun getLoanOrSavingsCharges(chargeType: ChargeType, chargeTypeId: Long): Flow<DataState<List<Charge>>> {
40-
return dataManager.clientChargeApi.getChargeList(chargeType.type, chargeTypeId)
47+
return clientChargeApi.getChargeList(chargeType.type, chargeTypeId)
4148
.map { response -> DataState.Success(response) }
4249
.catch { exception -> DataState.Error(exception, exception.message) }
4350
.flowOn(ioDispatcher)
@@ -64,7 +71,7 @@ class ClientChargeRepositoryImp(
6471
}
6572

6673
override fun getShareAccountCharges(shareAccountId: Long): Flow<DataState<List<Charge>>> {
67-
return dataManager.shareAccountApi.getShareAccountDetails(shareAccountId)
74+
return shareAccountApi.getShareAccountDetails(shareAccountId)
6875
.map { response ->
6976
DataState.Success(response.charges)
7077
}

core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/ClientRepositoryImp.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,19 @@ import org.mifos.mobile.core.common.asDataStateFlow
1717
import org.mifos.mobile.core.data.repository.ClientRepository
1818
import org.mifos.mobile.core.model.entity.Page
1919
import org.mifos.mobile.core.model.entity.client.Client
20-
import org.mifos.mobile.core.network.DataManager
20+
import org.mifos.mobile.core.network.DataManagerProvider
2121

2222
class ClientRepositoryImp(
23-
private val dataManager: DataManager,
23+
private val dataManager: DataManagerProvider,
2424
private val ioDispatcher: CoroutineDispatcher,
2525
) : ClientRepository {
2626

27+
val clientsApi = requireNotNull(dataManager.clientsApi) {
28+
"ClientService must be provided"
29+
}
30+
2731
override fun loadClient(): Flow<DataState<Page<Client>>> {
28-
return dataManager.clientsApi.clients()
32+
return clientsApi.clients()
2933
.asDataStateFlow().flowOn(ioDispatcher)
3034
}
3135
}

core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/GuarantorRepositoryImp.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,18 @@ import org.mifos.mobile.core.data.util.extractErrorMessage
2323
import org.mifos.mobile.core.model.entity.guarantor.GuarantorApplicationPayload
2424
import org.mifos.mobile.core.model.entity.guarantor.GuarantorPayload
2525
import org.mifos.mobile.core.model.entity.guarantor.GuarantorTemplatePayload
26-
import org.mifos.mobile.core.network.DataManager
26+
import org.mifos.mobile.core.network.DataManagerProvider
2727

2828
class GuarantorRepositoryImp(
29-
private val dataManager: DataManager,
29+
private val dataManager: DataManagerProvider,
3030
private val ioDispatcher: CoroutineDispatcher,
3131
) : GuarantorRepository {
3232

33+
val guarantorApi = requireNotNull(dataManager.guarantorApi) {
34+
"GuarantorService must be provided"
35+
}
3336
override fun getGuarantorTemplate(loanId: Long?): Flow<DataState<GuarantorTemplatePayload?>> {
34-
return dataManager.guarantorApi.getGuarantorTemplate(loanId!!)
37+
return guarantorApi.getGuarantorTemplate(loanId!!)
3538
.asDataStateFlow().flowOn(ioDispatcher)
3639
}
3740

@@ -41,7 +44,7 @@ class GuarantorRepositoryImp(
4144
): DataState<String> {
4245
return withContext(ioDispatcher) {
4346
try {
44-
val response = dataManager.guarantorApi.createGuarantor(loanId!!, payload)
47+
val response = guarantorApi.createGuarantor(loanId!!, payload)
4548
DataState.Success(response.bodyAsText())
4649
} catch (e: ClientRequestException) {
4750
val errorMessage = extractErrorMessage(e.response)
@@ -57,7 +60,7 @@ class GuarantorRepositoryImp(
5760
): DataState<String> {
5861
return withContext(ioDispatcher) {
5962
try {
60-
val response = dataManager.guarantorApi.updateGuarantor(
63+
val response = guarantorApi.updateGuarantor(
6164
payload,
6265
loanId!!,
6366
guarantorId!!,
@@ -73,7 +76,7 @@ class GuarantorRepositoryImp(
7376
override suspend fun deleteGuarantor(loanId: Long?, guarantorId: Long?): DataState<String> {
7477
return withContext(ioDispatcher) {
7578
try {
76-
val response = dataManager.guarantorApi.deleteGuarantor(loanId!!, guarantorId!!)
79+
val response = guarantorApi.deleteGuarantor(loanId!!, guarantorId!!)
7780
DataState.Success(response.bodyAsText())
7881
} catch (e: ClientRequestException) {
7982
val errorMessage = extractErrorMessage(e.response)

core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/HomeRepositoryImp.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,29 @@ import org.mifos.mobile.core.data.repository.HomeRepository
2020
import org.mifos.mobile.core.data.repository.NotificationRepository
2121
import org.mifos.mobile.core.model.entity.client.Client
2222
import org.mifos.mobile.core.model.entity.client.ClientAccounts
23-
import org.mifos.mobile.core.network.DataManager
23+
import org.mifos.mobile.core.network.DataManagerProvider
2424

2525
class HomeRepositoryImp(
26-
private val dataManager: DataManager,
26+
private val dataManager: DataManagerProvider,
2727
private val notificationRepository: NotificationRepository,
2828
private val ioDispatcher: CoroutineDispatcher,
2929
) : HomeRepository {
3030

31+
val clientsApi = requireNotNull(dataManager.clientsApi) {
32+
"ClientService must be provided"
33+
}
34+
3135
override fun clientAccounts(clientId: Long): Flow<DataState<ClientAccounts>> =
32-
dataManager.clientsApi.getClientAccounts(clientId)
36+
clientsApi.getClientAccounts(clientId)
3337
.asDataStateFlow().flowOn(ioDispatcher)
3438

3539
override fun currentClient(clientId: Long): Flow<DataState<Client>> {
36-
return dataManager.clientsApi.getClientForId(clientId)
40+
return clientsApi.getClientForId(clientId)
3741
.asDataStateFlow().flowOn(ioDispatcher)
3842
}
3943

4044
override fun clientImage(clientId: Long): Flow<DataState<String>> {
41-
return dataManager.clientsApi.getClientImage(clientId)
45+
return clientsApi.getClientImage(clientId)
4246
.asDataStateFlow()
4347
.map { response ->
4448
when (response) {

core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/LoanRepositoryImp.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,23 @@ import org.mifos.mobile.core.model.entity.TransactionDetails
2626
import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithAssociations
2727
import org.mifos.mobile.core.model.entity.accounts.loan.LoanWithdraw
2828
import org.mifos.mobile.core.model.entity.templates.loans.LoanTemplate
29-
import org.mifos.mobile.core.network.DataManager
29+
import org.mifos.mobile.core.network.DataManagerProvider
3030

3131
class LoanRepositoryImp(
32-
private val dataManager: DataManager,
32+
private val dataManager: DataManagerProvider,
3333
private val ioDispatcher: CoroutineDispatcher,
3434
) : LoanRepository {
3535

36+
val loanAccountsListApi = requireNotNull(dataManager.loanAccountsListApi) {
37+
"LoanAccountsListService must be provided"
38+
}
39+
3640
override fun getLoanWithAssociations(
3741
associationType: String?,
3842
loanId: Long?,
3943
): Flow<DataState<LoanWithAssociations?>> = flow {
4044
try {
41-
dataManager.loanAccountsListApi.getLoanWithAssociations(loanId!!, associationType)
45+
loanAccountsListApi.getLoanWithAssociations(loanId!!, associationType)
4246
.collect { response ->
4347
emit(DataState.Success(response))
4448
}
@@ -51,7 +55,7 @@ class LoanRepositoryImp(
5155
loanId: Long,
5256
transactionId: Long,
5357
): Flow<DataState<TransactionDetails>> {
54-
return dataManager.loanAccountsListApi
58+
return loanAccountsListApi
5559
.getLoanTransactionDetails(loanId, transactionId)
5660
.asDataStateFlow()
5761
.flowOn(ioDispatcher)
@@ -64,7 +68,7 @@ class LoanRepositoryImp(
6468
return withContext(ioDispatcher) {
6569
try {
6670
val response =
67-
dataManager.loanAccountsListApi.withdrawLoanAccount(loanId!!, loanWithdraw)
71+
loanAccountsListApi.withdrawLoanAccount(loanId!!, loanWithdraw)
6872
DataState.Success(response.bodyAsText())
6973
} catch (e: ClientRequestException) {
7074
val errorMessage = extractErrorMessage(e.response)
@@ -78,12 +82,12 @@ class LoanRepositoryImp(
7882
}
7983

8084
override fun template(clientId: Long?): Flow<DataState<LoanTemplate?>> {
81-
return dataManager.loanAccountsListApi.getLoanTemplate(clientId = clientId)
85+
return loanAccountsListApi.getLoanTemplate(clientId = clientId)
8286
.asDataStateFlow().flowOn(ioDispatcher)
8387
}
8488

8589
override fun getLoanTemplateByProduct(clientId: Long?, productId: Int?): Flow<DataState<LoanTemplate?>> {
86-
return dataManager.loanAccountsListApi.getLoanTemplateByProduct(clientId, productId)
90+
return loanAccountsListApi.getLoanTemplateByProduct(clientId, productId)
8791
.asDataStateFlow().flowOn(ioDispatcher)
8892
}
8993
}

core/data/src/commonMain/kotlin/org/mifos/mobile/core/data/repositoryImpl/RecentTransactionRepositoryImp.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,23 @@ import org.mifos.mobile.core.common.asDataStateFlow
1717
import org.mifos.mobile.core.data.repository.RecentTransactionRepository
1818
import org.mifos.mobile.core.model.entity.Page
1919
import org.mifos.mobile.core.model.entity.Transaction
20-
import org.mifos.mobile.core.network.DataManager
20+
import org.mifos.mobile.core.network.DataManagerProvider
2121

2222
class RecentTransactionRepositoryImp(
23-
private val dataManager: DataManager,
23+
private val dataManager: DataManagerProvider,
2424
private val ioDispatcher: CoroutineDispatcher,
2525
) : RecentTransactionRepository {
26+
27+
val recentTransactionsApi = requireNotNull(dataManager.recentTransactionsApi) {
28+
"RecentTransactionsService must be provided"
29+
}
30+
2631
override fun recentTransactions(
2732
clientId: Long?,
2833
offset: Int?,
2934
limit: Int?,
3035
): Flow<DataState<Page<Transaction>>> {
31-
return dataManager.recentTransactionsApi.getRecentTransactionsList(
36+
return recentTransactionsApi.getRecentTransactionsList(
3237
clientId!!,
3338
offset,
3439
limit,

0 commit comments

Comments
 (0)