Skip to content

Commit 5052d30

Browse files
committed
unit test the data layer
1 parent ca9ceb5 commit 5052d30

File tree

35 files changed

+870
-93
lines changed

35 files changed

+870
-93
lines changed

Diff for: core/database/src/main/kotlin/br/com/mob1st/core/data/DatabaseExtensions.kt

+52
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,62 @@
11
package br.com.mob1st.core.data
22

3+
import app.cash.sqldelight.Query
34
import app.cash.sqldelight.Transacter
45
import app.cash.sqldelight.TransactionWithoutReturn
6+
import app.cash.sqldelight.coroutines.asFlow
7+
import app.cash.sqldelight.coroutines.mapToList
8+
import app.cash.sqldelight.coroutines.mapToOne
9+
import kotlinx.coroutines.flow.map
510
import kotlinx.coroutines.withContext
611
import kotlin.coroutines.CoroutineContext
712

13+
/**
14+
* Converts the receiver [Query] to a flow of lists of the query results.
15+
* It map each generated SQLDelight DTO of type [T] to a list of type [R].
16+
* @param T The type of the SQLDelight DTO.
17+
* @param R The type of the result object.
18+
* @param context The coroutine context to run the flow.
19+
* @param transform The transformation function to convert the DTO to the domain object.
20+
* @return The flow of lists of domain objects.
21+
*/
22+
fun <T : Any, R> Query<T>.asFlowListEach(
23+
context: CoroutineContext,
24+
transform: (T) -> R,
25+
) = asFlowList(context) { list ->
26+
list.map(transform)
27+
}
28+
29+
/**
30+
* Converts the receiver [Query] to a flow of lists of the query results.
31+
* It maps the generated SQLDelight DTO of type [T] to a object of type [R].
32+
* @param T The type of the SQLDelight DTO.
33+
* @param R The type of the domain object.
34+
* @param context The coroutine context to run the flow.
35+
* @param transform The transformation function to convert the DTO to the domain object.
36+
*/
37+
fun <T : Any, R> Query<T>.asFlowList(
38+
context: CoroutineContext,
39+
transform: (List<T>) -> List<R>,
40+
) = asFlow().mapToList(context).map { list ->
41+
transform(list)
42+
}
43+
44+
/**
45+
* Converts the receiver [Query] to a flow of single results of the query.
46+
* It maps the generated SQLDelight DTO of type [T] to a object of type [R].
47+
* @param T The type of the SQLDelight DTO.
48+
* @param R The type of the result object.
49+
* @param context The coroutine context to run the flow.
50+
* @param transform The transformation function to convert the DTO to the domain object.
51+
* @return The flow of domain objects.
52+
*/
53+
fun <T : Any, R> Query<T>.asFlowSingle(
54+
context: CoroutineContext,
55+
transform: (T) -> R,
56+
) = asFlow().mapToOne(context).map { t ->
57+
transform(t)
58+
}
59+
860
/**
961
* Handy extension to run a transaction in a suspend context.
1062
* @param context The coroutine context to run the transaction.

Diff for: core/database/src/main/sqldelight/br/com/mob1st/core/database/Schema.sq

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import kotlin.Boolean;
88
CREATE TABLE Categories(
99
id INTEGER PRIMARY KEY,
1010
name TEXT NOT NULL,
11-
suggested INTEGER AS Boolean NOT NULL DEFAULT 0,
11+
is_suggested INTEGER AS Boolean NOT NULL DEFAULT 0,
1212
amount INTEGER NOT NULL CHECK (amount >= 0),
1313
image TEXT NOT NULL,
14-
recurrence_type TEXT NOT NULL UNIQUE CHECK(
14+
recurrence_type TEXT NOT NULL CHECK(
1515
recurrence_type IN ('fixed', 'variable', 'seasonal')
1616
),
1717
recurrences TEXT,
@@ -22,7 +22,7 @@ CREATE TABLE Categories(
2222
CREATE INDEX idx_categories__description ON Categories(name);
2323
CREATE INDEX idx_categories__recurrences ON Categories(recurrences);
2424
CREATE INDEX idx_categories__is_expense_X_suggested_X_recurrence_type
25-
ON Categories(is_expense, suggested, recurrence_type);
25+
ON Categories(is_expense, is_suggested, recurrence_type);
2626

2727
CREATE TRIGGER prevent_update_Categories
2828
BEFORE UPDATE OF created_at ON Categories

Diff for: core/kotlinx/src/main/kotlin/br/com/mob1st/core/kotlinx/coroutines/FlowExtensions.kt

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package br.com.mob1st.core.kotlinx.coroutines
22

33
import kotlinx.coroutines.flow.Flow
4+
import kotlinx.coroutines.flow.map
45
import kotlinx.coroutines.flow.onStart
56

67
/**
@@ -10,3 +11,7 @@ import kotlinx.coroutines.flow.onStart
1011
* @return The flow with the value emitted at the beginning.
1112
*/
1213
fun <T> Flow<T>.startsWith(value: T) = onStart { emit(value) }
14+
15+
fun <T, R> Flow<List<T>>.mapList(transform: (T) -> R): Flow<List<R>> = map { list ->
16+
list.map(transform)
17+
}

Diff for: features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/dependencies/InfraModules.kt

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import br.com.mob1st.features.finances.impl.domain.repositories.RecurrenceBuilde
66
import br.com.mob1st.features.finances.impl.infra.data.preferences.RecurrenceBuilderCompletionsDataSource
77
import br.com.mob1st.features.finances.impl.infra.data.ram.RecurrenceBuilderListsDataSource
88
import br.com.mob1st.features.finances.impl.infra.data.repositories.RecurrenceBuilderRepositoryImpl
9+
import br.com.mob1st.features.finances.impl.infra.data.repositories.categories.CategoriesDataMap
910
import br.com.mob1st.features.finances.impl.infra.data.repositories.categories.CategoryRepositoryImpl
1011
import br.com.mob1st.features.finances.impl.infra.data.repositories.suggestions.CategorySuggestionsRepositoryImpl
1112
import br.com.mob1st.features.finances.impl.infra.data.repositories.suggestions.SuggestionListPerStep
@@ -36,6 +37,7 @@ private val repositoriesModule = module {
3637
io = get(),
3738
)
3839
}
40+
single { CategoriesDataMap }
3941
factoryOf(::CategoryRepositoryImpl) bind CategoriesRepository::class
4042
factoryOf(::CategorySuggestionsRepositoryImpl) bind CategorySuggestionRepository::class
4143
}

Diff for: features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/domain/entities/Category.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import br.com.mob1st.core.kotlinx.structures.Identifiable
44
import br.com.mob1st.core.kotlinx.structures.Money
55
import br.com.mob1st.core.kotlinx.structures.RowId
66
import br.com.mob1st.core.kotlinx.structures.Uri
7-
import br.com.mob1st.features.finances.impl.domain.values.DayOfMonth
8-
import br.com.mob1st.features.finances.impl.domain.values.DayOfYear
7+
import br.com.mob1st.features.finances.impl.domain.fixtures.DayOfMonth
8+
import br.com.mob1st.features.finances.impl.domain.fixtures.DayOfYear
99

1010
data class Category(
1111
override val id: Id = Id(),
@@ -14,7 +14,7 @@ data class Category(
1414
val amount: Money,
1515
val isExpense: Boolean,
1616
val recurrences: Recurrences,
17-
val suggested: Boolean,
17+
val isSuggested: Boolean,
1818
) : Identifiable<Category.Id> {
1919
@JvmInline
2020
value class Id(override val value: Long = 0) : RowId

Diff for: features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/domain/values/DayOfMonth.kt renamed to features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/domain/fixtures/DayOfMonth.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package br.com.mob1st.features.finances.impl.domain.values
1+
package br.com.mob1st.features.finances.impl.domain.fixtures
22

33
@JvmInline
44
value class DayOfMonth(val value: Int) {

Diff for: features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/domain/values/DayOfWeek.kt renamed to features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/domain/fixtures/DayOfWeek.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package br.com.mob1st.features.finances.impl.domain.values
1+
package br.com.mob1st.features.finances.impl.domain.fixtures
22

33
@JvmInline
44
value class DayOfWeek(val value: Int) {

Diff for: features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/domain/values/DayOfYear.kt renamed to features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/domain/fixtures/DayOfYear.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package br.com.mob1st.features.finances.impl.domain.values
1+
package br.com.mob1st.features.finances.impl.domain.fixtures
22

33
@JvmInline
44
value class DayOfYear(val value: Int) {

Diff for: features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/domain/usecases/GetCategoryBuilderUseCase.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal class GetCategoryBuilderUseCase(
2020
return categoryRepository.getByStep(step)
2121
.map { categories ->
2222
val (manuallyAdded, suggestions) = categories.partition {
23-
it.suggested
23+
it.isSuggested
2424
}
2525
BudgetBuilder(
2626
id = step,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package br.com.mob1st.features.finances.impl.infra.data.repositories.categories
2+
3+
import br.com.mob1st.core.database.Categories
4+
import br.com.mob1st.core.kotlinx.structures.Money
5+
import br.com.mob1st.core.kotlinx.structures.Uri
6+
import br.com.mob1st.features.finances.impl.domain.entities.Category
7+
8+
/**
9+
* Maps a [Categories] entity to a [Category] domain entity.
10+
*/
11+
internal interface CategoriesDataMap : (Categories) -> Category {
12+
/**
13+
* Default implementation of the [CategoriesDataMap] interface.
14+
*/
15+
companion object : CategoriesDataMap {
16+
override fun invoke(entity: Categories): Category {
17+
return entity.toDomain()
18+
}
19+
}
20+
}
21+
22+
/**
23+
* Maps the receiver [Categories] to a [Category] domain entity.
24+
* @return the [Category] domain entity.
25+
*/
26+
private fun Categories.toDomain(): Category {
27+
val recurrenceColumns = RecurrenceColumns(
28+
rawType = recurrence_type,
29+
rawRecurrences = recurrences,
30+
)
31+
return Category(
32+
id = Category.Id(id),
33+
name = name,
34+
image = Uri(image),
35+
amount = Money(amount),
36+
isExpense = is_expense,
37+
isSuggested = is_suggested,
38+
recurrences = recurrenceColumns.toRecurrences(),
39+
)
40+
}

Diff for: features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/infra/data/repositories/categories/CategoryRepositoryImpl.kt

+9-16
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
package br.com.mob1st.features.finances.impl.infra.data.repositories.categories
22

3-
import app.cash.sqldelight.coroutines.asFlow
4-
import app.cash.sqldelight.coroutines.mapToList
5-
import app.cash.sqldelight.coroutines.mapToOne
3+
import br.com.mob1st.core.data.asFlowListEach
4+
import br.com.mob1st.core.data.asFlowSingle
65
import br.com.mob1st.core.data.suspendTransaction
76
import br.com.mob1st.core.kotlinx.coroutines.IoCoroutineDispatcher
87
import br.com.mob1st.features.finances.impl.TwoCentsDb
98
import br.com.mob1st.features.finances.impl.domain.entities.BuilderNextAction
109
import br.com.mob1st.features.finances.impl.domain.entities.Category
1110
import br.com.mob1st.features.finances.impl.domain.repositories.CategoriesRepository
1211
import kotlinx.coroutines.flow.Flow
13-
import kotlinx.coroutines.flow.map
1412
import kotlinx.coroutines.withContext
1513

1614
/**
@@ -21,6 +19,7 @@ import kotlinx.coroutines.withContext
2119
internal class CategoryRepositoryImpl(
2220
private val io: IoCoroutineDispatcher,
2321
private val db: TwoCentsDb,
22+
private val categoriesDataMap: CategoriesDataMap,
2423
) : CategoriesRepository {
2524
override fun getByStep(step: BuilderNextAction.Step): Flow<List<Category>> {
2625
val args = SelectCategoriesByStepArgs.from(step)
@@ -29,22 +28,16 @@ internal class CategoryRepositoryImpl(
2928
is_expense = args.isExpense,
3029
recurrence_type = args.recurrenceTypeDescription,
3130
)
32-
.asFlow()
33-
.mapToList(io)
34-
.map { categoriesList ->
35-
categoriesList.map { categories ->
36-
categories.toDomain()
37-
}
31+
.asFlowListEach(io) { categories ->
32+
categoriesDataMap(categories)
3833
}
3934
}
4035

4136
override fun getById(id: Category.Id): Flow<Category> {
4237
return db.categoriesQueries
4338
.selectCategoryById(id.value)
44-
.asFlow()
45-
.mapToOne(io)
46-
.map { categories ->
47-
categories.toDomain()
39+
.asFlowSingle(io) { categories ->
40+
categoriesDataMap(categories)
4841
}
4942
}
5043

@@ -56,11 +49,11 @@ internal class CategoryRepositoryImpl(
5649

5750
override suspend fun add(
5851
category: Category,
59-
) = db.suspendTransaction(io) {
52+
) = withContext(io) {
6053
db.categoriesQueries.insert(category)
6154
}
6255

63-
override suspend fun set(category: Category) = db.suspendTransaction(io) {
56+
override suspend fun set(category: Category) = withContext(io) {
6457
db.categoriesQueries.update(category)
6558
}
6659

Diff for: features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/infra/data/repositories/categories/QueriesExt.kt

+1-24
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,8 @@
11
package br.com.mob1st.features.finances.impl.infra.data.repositories.categories
22

3-
import br.com.mob1st.core.database.Categories
4-
import br.com.mob1st.core.kotlinx.structures.Money
5-
import br.com.mob1st.core.kotlinx.structures.Uri
63
import br.com.mob1st.features.finances.impl.CategoriesQueries
74
import br.com.mob1st.features.finances.impl.domain.entities.Category
85

9-
/**
10-
* Maps the receiver [Categories] to a [Category] domain entity.
11-
* @return the [Category] domain entity.
12-
*/
13-
internal fun Categories.toDomain(): Category {
14-
val columns = RecurrenceColumns(
15-
rawType = recurrence_type,
16-
rawRecurrences = recurrences,
17-
)
18-
return Category(
19-
id = Category.Id(id),
20-
name = name,
21-
image = Uri(image),
22-
amount = Money(amount),
23-
isExpense = is_expense,
24-
suggested = suggested,
25-
recurrences = columns.toRecurrences(),
26-
)
27-
}
28-
296
/**
307
* Inserts the given [category] into the database.
318
* It must be called inside a transaction created by the caller of this function.
@@ -38,7 +15,7 @@ internal fun CategoriesQueries.insert(category: Category) {
3815
amount = category.amount.cents,
3916
image = category.image.value,
4017
is_expense = category.isExpense,
41-
suggested = category.suggested,
18+
is_suggested = category.isSuggested,
4219
recurrences = columns.rawRecurrences,
4320
recurrence_type = columns.rawType,
4421
)

Diff for: features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/infra/data/repositories/categories/RecurrenceColumns.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package br.com.mob1st.features.finances.impl.infra.data.repositories.categories
22

33
import br.com.mob1st.features.finances.impl.domain.entities.Recurrences
4-
import br.com.mob1st.features.finances.impl.domain.values.DayOfMonth
5-
import br.com.mob1st.features.finances.impl.domain.values.DayOfYear
4+
import br.com.mob1st.features.finances.impl.domain.fixtures.DayOfMonth
5+
import br.com.mob1st.features.finances.impl.domain.fixtures.DayOfYear
66
import java.util.Locale
77

88
private const val COLUMN_SEPARATOR = ","

Diff for: features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/infra/data/repositories/categories/RecurrenceType.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal enum class RecurrenceType(val value: String) {
2525
* @throws IllegalArgumentException if the given [value] is unknown.
2626
*/
2727
fun fromValue(value: String): RecurrenceType {
28-
return checkNotNull(valueToEnum[value]) {
28+
return requireNotNull(valueToEnum[value]) {
2929
"Unknown recurrence type: $value"
3030
}
3131
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package br.com.mob1st.features.finances.impl.ui.builder.completion
22

3+
import androidx.compose.material3.Text
34
import androidx.compose.runtime.Composable
45
import br.com.mob1st.core.design.utils.ThemedPreview
56

67
@Composable
78
fun BuilderCompletionPage(
89
onComplete: () -> Unit,
910
) {
11+
Text(text = "Sample")
1012
}
1113

1214
@Composable
1315
@ThemedPreview
1416
private fun BuilderCompletionPagePreview() {
17+
BuilderCompletionPage {}
1518
}

Diff for: features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/ui/builder/steps/BudgetBuilderStepPreviewFixture.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import br.com.mob1st.features.finances.impl.domain.entities.BudgetBuilder
66
import br.com.mob1st.features.finances.impl.domain.entities.Category
77
import br.com.mob1st.features.finances.impl.domain.entities.FixedIncomesStep
88
import br.com.mob1st.features.finances.impl.domain.entities.Recurrences
9-
import br.com.mob1st.features.finances.impl.domain.values.DayOfMonth
9+
import br.com.mob1st.features.finances.impl.domain.fixtures.DayOfMonth
1010

1111
@Suppress("MagicNumber")
1212
internal object BudgetBuilderStepPreviewFixture {
@@ -16,7 +16,7 @@ internal object BudgetBuilderStepPreviewFixture {
1616
amount = Money(48558),
1717
isExpense = true,
1818
recurrences = Recurrences.Fixed(DayOfMonth(1)),
19-
suggested = false,
19+
isSuggested = false,
2020
image = Uri("file:///android_asset/icons/finances_builder_suggestions_item_back_to_school_supplies.svg"),
2121
)
2222

@@ -26,7 +26,7 @@ internal object BudgetBuilderStepPreviewFixture {
2626
amount = Money(50000),
2727
isExpense = true,
2828
recurrences = Recurrences.Fixed(DayOfMonth(1)),
29-
suggested = false,
29+
isSuggested = false,
3030
image = Uri("file:///android_asset/icons/finances_builder_suggestions_item_back_to_school_supplies.svg"),
3131
)
3232

@@ -36,7 +36,7 @@ internal object BudgetBuilderStepPreviewFixture {
3636
amount = Money(124056),
3737
isExpense = true,
3838
recurrences = Recurrences.Fixed(DayOfMonth(1)),
39-
suggested = true,
39+
isSuggested = true,
4040
image = Uri("file:///android_asset/icons/finances_builder_suggestions_item_back_to_school_supplies.svg"),
4141
)
4242

Diff for: features/finances/impl/src/main/kotlin/br/com/mob1st/features/finances/impl/ui/utils/parcelers/RecurrencesParceler.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package br.com.mob1st.features.finances.impl.ui.utils.parcelers
22

33
import android.os.Parcel
44
import br.com.mob1st.features.finances.impl.domain.entities.Recurrences
5-
import br.com.mob1st.features.finances.impl.domain.values.DayOfMonth
6-
import br.com.mob1st.features.finances.impl.domain.values.DayOfYear
5+
import br.com.mob1st.features.finances.impl.domain.fixtures.DayOfMonth
6+
import br.com.mob1st.features.finances.impl.domain.fixtures.DayOfYear
77
import br.com.mob1st.features.finances.publicapi.domain.entities.RecurrenceType
88
import kotlinx.parcelize.Parceler
99

0 commit comments

Comments
 (0)