Skip to content
This repository was archived by the owner on Aug 11, 2025. It is now read-only.

Commit be76546

Browse files
Fixes before release (#3114)
1 parent 78a6d56 commit be76546

File tree

12 files changed

+348
-20
lines changed

12 files changed

+348
-20
lines changed

gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ paparazzi = "1.3.3"
1919
# Android
2020
min-sdk = "28"
2121
compile-sdk = "34"
22-
version-name = "4.6.2"
23-
version-code = "162"
22+
version-name = "4.6.3"
23+
version-code = "163"
2424
jvm-target = "17"
2525

2626

screen/home/src/main/java/com/ivy/home/customerjourney/CustomerJourneyCardsProvider.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.ivy.home.customerjourney
22

3+
import com.ivy.base.legacy.SharedPrefs
34
import com.ivy.base.legacy.stringRes
45
import com.ivy.base.model.TransactionType
56
import com.ivy.data.db.dao.read.PlannedPaymentRuleDao
@@ -15,7 +16,6 @@ import com.ivy.design.l0_system.Red
1516
import com.ivy.design.l0_system.Red3
1617
import com.ivy.legacy.Constants
1718
import com.ivy.legacy.IvyWalletCtx
18-
import com.ivy.base.legacy.SharedPrefs
1919
import com.ivy.legacy.data.model.MainTab
2020
import com.ivy.navigation.EditPlannedScreen
2121
import com.ivy.navigation.PieChartStatisticScreen
@@ -65,6 +65,7 @@ class CustomerJourneyCardsProvider @Inject constructor(
6565
rateUsCard_2(),
6666
joinTelegram2(),
6767
ivyWalletIsOpenSource(),
68+
bugsApology(),
6869
)
6970

7071
fun adjustBalanceCard() = CustomerJourneyCardModel(
@@ -231,5 +232,27 @@ class CustomerJourneyCardsProvider @Inject constructor(
231232
ivyActivity.openUrlInBrowser(Constants.URL_IVY_TELEGRAM_INVITE)
232233
}
233234
)
235+
236+
fun bugsApology(): CustomerJourneyCardModel = CustomerJourneyCardModel(
237+
id = "bugs_apology_1",
238+
condition = { trnCount, _, _ ->
239+
trnCount > 10
240+
},
241+
title = "Apologies for the bugs!",
242+
description = "Ivy Wallet v4.6.2 had some annoying bugs... " +
243+
"We're sorry for that and we hope that we have fixed them.\n\n" +
244+
"Ivy Wallet is an open-source and community-driven project " +
245+
"that is maintained and develop solely by voluntary contributors. " +
246+
"So to help us and make your experience better, " +
247+
"please report any bugs as a GitHub issue. You can also" +
248+
" join our community and become a contributor!",
249+
cta = "Report a bug",
250+
ctaIcon = R.drawable.github_logo,
251+
background = Gradient.solid(Blue),
252+
hasDismiss = true,
253+
onAction = { _, _, ivyActivity ->
254+
ivyActivity.openUrlInBrowser(Constants.URL_GITHUB_NEW_ISSUE)
255+
}
256+
)
234257
}
235258
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.ivy.data
2+
3+
import com.ivy.data.db.entity.AccountEntity
4+
import com.ivy.data.model.testing.colorInt
5+
import com.ivy.data.model.testing.iconAsset
6+
import com.ivy.data.model.testing.maybe
7+
import com.ivy.data.model.testing.notBlankTrimmedString
8+
import io.kotest.property.Arb
9+
import io.kotest.property.arbitrary.arbitrary
10+
import io.kotest.property.arbitrary.boolean
11+
import io.kotest.property.arbitrary.double
12+
import io.kotest.property.arbitrary.of
13+
import io.kotest.property.arbitrary.removeEdgecases
14+
import io.kotest.property.arbitrary.string
15+
import io.kotest.property.arbitrary.uuid
16+
17+
fun Arb.Companion.invalidAccountEntity(): Arb<AccountEntity> = arbitrary {
18+
val validEntity = validAccountEntity().bind()
19+
validEntity.copy(
20+
name = Arb.of("", " ", " ").bind()
21+
)
22+
}
23+
24+
fun Arb.Companion.validAccountEntity(): Arb<AccountEntity> = arbitrary {
25+
AccountEntity(
26+
name = Arb.notBlankTrimmedString().bind().value,
27+
currency = Arb.maybe(Arb.string()).bind(),
28+
color = Arb.colorInt().bind().value,
29+
icon = Arb.iconAsset().bind().id,
30+
orderNum = Arb.double().removeEdgecases().bind(),
31+
includeInBalance = Arb.boolean().bind(),
32+
isSynced = Arb.boolean().bind(),
33+
isDeleted = Arb.boolean().bind(),
34+
id = Arb.uuid().bind()
35+
)
36+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.ivy.data.repository.mapper
2+
3+
import com.ivy.data.invalidAccountEntity
4+
import com.ivy.data.model.primitive.AssetCode
5+
import com.ivy.data.model.testing.account
6+
import com.ivy.data.repository.CurrencyRepository
7+
import com.ivy.data.validAccountEntity
8+
import io.kotest.assertions.arrow.core.shouldBeLeft
9+
import io.kotest.assertions.arrow.core.shouldBeRight
10+
import io.kotest.matchers.nulls.shouldNotBeNull
11+
import io.kotest.matchers.shouldBe
12+
import io.kotest.property.Arb
13+
import io.kotest.property.checkAll
14+
import io.mockk.coEvery
15+
import io.mockk.mockk
16+
import kotlinx.coroutines.test.runTest
17+
import org.junit.Before
18+
import org.junit.Test
19+
20+
class AccountMapperPropertyTest {
21+
22+
private val currencyRepository = mockk<CurrencyRepository>()
23+
24+
private lateinit var mapper: AccountMapper
25+
26+
@Before
27+
fun mapper() {
28+
mapper = AccountMapper(
29+
currencyRepository = currencyRepository,
30+
)
31+
}
32+
33+
@Test
34+
fun `property - domain-entity isomorphism`() = runTest {
35+
checkAll(Arb.account()) { accOrig ->
36+
with(mapper) {
37+
// when: domain -> entity -> domain
38+
val entityOne = accOrig.toEntity()
39+
val accTwo = entityOne.toDomain().getOrNull()
40+
41+
// then: the recovered domain trn must be the same
42+
accTwo.shouldNotBeNull() shouldBe accOrig
43+
44+
// and when again: domain -> entity
45+
val entityTwo = accTwo.toEntity()
46+
47+
// then: the recovered entity must be the same
48+
entityTwo shouldBe entityOne
49+
}
50+
}
51+
}
52+
53+
@Test
54+
fun `maps invalid accounts - always fails`() = runTest {
55+
checkAll(Arb.invalidAccountEntity()) { entity ->
56+
// given
57+
coEvery { currencyRepository.getBaseCurrency() } returns AssetCode.EUR
58+
59+
// when
60+
val res = with(mapper) { entity.toDomain() }
61+
62+
// then
63+
res.shouldBeLeft()
64+
}
65+
}
66+
67+
@Test
68+
fun `maps valid accounts - always succeeds`() = runTest {
69+
checkAll(Arb.validAccountEntity()) { entity ->
70+
// given
71+
coEvery { currencyRepository.getBaseCurrency() } returns AssetCode.EUR
72+
73+
// when
74+
val res = with(mapper) { entity.toDomain() }
75+
76+
// then
77+
res.shouldBeRight()
78+
}
79+
}
80+
}

shared/data/core/src/test/java/com/ivy/data/repository/mapper/AccountMapperTest.kt

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package com.ivy.data.repository.mapper
22

3+
import com.google.testing.junit.testparameterinjector.TestParameter
4+
import com.google.testing.junit.testparameterinjector.TestParameterInjector
35
import com.ivy.data.db.entity.AccountEntity
46
import com.ivy.data.model.Account
57
import com.ivy.data.model.AccountId
68
import com.ivy.data.model.primitive.AssetCode
79
import com.ivy.data.model.primitive.ColorInt
810
import com.ivy.data.model.primitive.IconAsset
911
import com.ivy.data.model.primitive.NotBlankTrimmedString
12+
import com.ivy.data.model.testing.ModelFixtures
1013
import com.ivy.data.repository.CurrencyRepository
1114
import io.kotest.assertions.arrow.core.shouldBeLeft
1215
import io.kotest.assertions.arrow.core.shouldBeRight
@@ -16,9 +19,11 @@ import io.mockk.mockk
1619
import kotlinx.coroutines.test.runTest
1720
import org.junit.Before
1821
import org.junit.Test
22+
import org.junit.runner.RunWith
1923
import java.time.Instant
2024
import java.util.UUID
2125

26+
@RunWith(TestParameterInjector::class)
2227
class AccountMapperTest {
2328

2429
private val currencyRepository = mockk<CurrencyRepository>(relaxed = true)
@@ -31,19 +36,21 @@ class AccountMapperTest {
3136
}
3237

3338
@Test
34-
fun `maps domain to entity`() {
39+
fun `maps domain to entity`(
40+
@TestParameter includeInBalance: Boolean,
41+
@TestParameter removed: Boolean,
42+
) {
3543
// given
36-
val id = UUID.randomUUID()
3744
val account = Account(
38-
id = AccountId(id),
45+
id = ModelFixtures.AccountId,
3946
name = NotBlankTrimmedString.unsafe("Test"),
4047
asset = AssetCode.unsafe("USD"),
4148
color = ColorInt(value = 42),
4249
icon = IconAsset.unsafe("icon"),
43-
includeInBalance = true,
50+
includeInBalance = includeInBalance,
4451
orderNum = 3.14,
4552
lastUpdated = Instant.EPOCH,
46-
removed = false
53+
removed = removed
4754
)
4855

4956
// when
@@ -55,20 +62,25 @@ class AccountMapperTest {
5562
currency = "USD",
5663
color = 42,
5764
icon = "icon",
58-
includeInBalance = true,
65+
includeInBalance = includeInBalance,
5966
orderNum = 3.14,
6067
isSynced = true,
61-
isDeleted = false,
62-
id = id,
68+
isDeleted = removed,
69+
id = ModelFixtures.AccountId.value,
6370
)
6471
}
6572

6673
// region entity to domain
6774
@Test
68-
fun `maps entity to domain - valid entity`() = runTest {
75+
fun `maps entity to domain - valid entity`(
76+
@TestParameter includeInBalance: Boolean,
77+
@TestParameter removed: Boolean,
78+
) = runTest {
6979
// given
7080
val entity = ValidEntity.copy(
71-
orderNum = 42.0
81+
orderNum = 42.0,
82+
includeInBalance = includeInBalance,
83+
isDeleted = removed,
7284
)
7385

7486
// when
@@ -81,10 +93,10 @@ class AccountMapperTest {
8193
asset = AssetCode.unsafe("USD"),
8294
color = ColorInt(value = 42),
8395
icon = IconAsset.unsafe("icon"),
84-
includeInBalance = true,
96+
includeInBalance = includeInBalance,
8597
orderNum = 42.0,
8698
lastUpdated = Instant.EPOCH,
87-
removed = false
99+
removed = removed
88100
)
89101
}
90102

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
11
package com.ivy.data.model.testing
22

3+
import arrow.core.None
4+
import arrow.core.Option
5+
import arrow.core.getOrElse
6+
import com.ivy.data.model.Category
37
import com.ivy.data.model.CategoryId
48
import io.kotest.property.Arb
9+
import io.kotest.property.arbitrary.arbitrary
10+
import io.kotest.property.arbitrary.boolean
11+
import io.kotest.property.arbitrary.double
12+
import io.kotest.property.arbitrary.instant
513
import io.kotest.property.arbitrary.map
14+
import io.kotest.property.arbitrary.removeEdgecases
615
import io.kotest.property.arbitrary.uuid
716

17+
fun Arb.Companion.category(
18+
categoryId: Option<CategoryId> = None,
19+
): Arb<Category> = arbitrary {
20+
Category(
21+
id = categoryId.getOrElse { Arb.categoryId().bind() },
22+
name = Arb.notBlankTrimmedString().bind(),
23+
color = Arb.colorInt().bind(),
24+
icon = Arb.maybe(Arb.iconAsset()).bind(),
25+
orderNum = Arb.double().removeEdgecases().bind(),
26+
lastUpdated = Arb.instant().bind(),
27+
removed = Arb.boolean().bind()
28+
)
29+
}
30+
831
fun Arb.Companion.categoryId(): Arb<CategoryId> = Arb.uuid().map(::CategoryId)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.ivy.data.model.testing
2+
3+
import arrow.core.Some
4+
import io.kotest.matchers.shouldBe
5+
import io.kotest.property.Arb
6+
import io.kotest.property.checkAll
7+
import io.kotest.property.forAll
8+
import kotlinx.coroutines.test.runTest
9+
import org.junit.Test
10+
11+
class ArbCategoryTest {
12+
13+
@Test
14+
fun `generates arb category`() = runTest {
15+
forAll(Arb.category()) {
16+
true
17+
}
18+
}
19+
20+
@Test
21+
fun `arb category respects passed param`() = runTest {
22+
val categoryId = ModelFixtures.CategoryId
23+
24+
checkAll(Arb.category(categoryId = Some(categoryId))) { category ->
25+
category.id shouldBe categoryId
26+
}
27+
}
28+
}

shared/data/model-testing/src/test/java/com/ivy/data/model/testing/ArbTransactionTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ class ArbTransactionTest {
109109

110110
@Test
111111
fun `generates arb transfer`() = runTest {
112-
forAll(Arb.transfer()) {
113-
true
112+
forAll(Arb.transfer()) { transfer ->
113+
transfer.fromAccount != transfer.toAccount
114114
}
115115
}
116116

shared/domain/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ dependencies {
1616
implementation(libs.bundles.ktor)
1717
implementation(libs.bundles.opencsv)
1818

19+
testImplementation(projects.shared.data.modelTesting)
20+
testImplementation(projects.shared.data.coreTesting)
21+
1922
androidTestImplementation(libs.bundles.integration.testing)
2023
androidTestImplementation(libs.mockk.android)
2124
}

shared/domain/src/main/java/com/ivy/domain/usecase/csv/ExportCsvUseCase.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,13 @@ class ExportCsvUseCase @Inject constructor(
118118
}
119119

120120
private fun String.escapeCsvString(): String = try {
121-
StringEscapeUtils.escapeCsv(this)
121+
StringEscapeUtils.escapeCsv(this).escapeSpecialChars()
122122
} catch (e: Exception) {
123-
replace(CSV_SEPARATOR, " ")
124-
.replace(NEWLINE, " ")
123+
escapeSpecialChars()
125124
}
126125

126+
private fun String.escapeSpecialChars(): String = replace("\\", "")
127+
127128
private fun Transaction.toIvyCsvRow(): IvyCsvRow = when (this) {
128129
is Expense -> expenseCsvRow()
129130
is Income -> incomeCsvRow()

0 commit comments

Comments
 (0)