Skip to content
This repository has been archived by the owner on Nov 5, 2024. It is now read-only.

More integration tests #2899

Merged
merged 7 commits into from
Jan 28, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions buildSrc/src/main/kotlin/ivy.integration.testing.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
plugins {
id("ivy.feature")
}

android {
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

packaging {
resources.pickFirsts.add("win32-x86-64/attach_hotspot_windows.dll")
resources.pickFirsts.add("win32-x86/attach_hotspot_windows.dll")
resources.pickFirsts.add("META-INF/**")
}
}

dependencies {
androidTestImplementation(libs.bundles.integration.testing)
}
5 changes: 2 additions & 3 deletions ivy-data/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
plugins {
id("ivy.feature")
id("ivy.room")
id("ivy.integration.testing")
}

android {
namespace = "com.ivy.data"
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}

dependencies {
@@ -17,5 +15,6 @@ dependencies {
implementation(libs.bundles.ktor)

androidTestImplementation(libs.bundles.integration.testing)
androidTestImplementation(projects.ivyTesting)
testImplementation(projects.ivyTesting)
}
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.ivy.data.backup

import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.ivy.base.di.KotlinxSerializationModule
import com.ivy.base.legacy.SharedPrefs
import com.ivy.data.db.IvyRoomDatabase
import com.ivy.data.file.IvyFileReader
import com.ivy.testing.TestDispatchersProvider
import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.ints.shouldBeGreaterThan
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File

@RunWith(AndroidJUnit4::class)
class BackupDataUseCaseAndroidTest {

private lateinit var db: IvyRoomDatabase
private lateinit var useCase: BackupDataUseCase

@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(context, IvyRoomDatabase::class.java).build()
val appContext = InstrumentationRegistry.getInstrumentation().context
useCase = BackupDataUseCase(
accountDao = db.accountDao,
budgetDao = db.budgetDao,
categoryDao = db.categoryDao,
loanRecordDao = db.loanRecordDao,
loanDao = db.loanDao,
plannedPaymentRuleDao = db.plannedPaymentRuleDao,
settingsDao = db.settingsDao,
transactionDao = db.transactionDao,
sharedPrefs = SharedPrefs(appContext),
accountWriter = db.writeAccountDao,
categoryWriter = db.writeCategoryDao,
transactionWriter = db.writeTransactionDao,
settingsWriter = db.writeSettingsDao,
budgetWriter = db.writeBudgetDao,
loanWriter = db.writeLoanDao,
loanRecordWriter = db.writeLoanRecordDao,
plannedPaymentRuleWriter = db.writePlannedPaymentRuleDao,
context = appContext,
json = KotlinxSerializationModule.provideJson(),
dispatchersProvider = TestDispatchersProvider,
fileReader = IvyFileReader(appContext)
)
}

@After
fun closeDb() {
db.close()
}

@Test
fun backup450_150() = runBlocking {
backupTestCase("450-150")
}

private suspend fun backupTestCase(version: String) {
importBackupZipTestCase(version)
importBackupJsonTestCase(version)

// close and re-open the db to ensure fresh data
closeDb()
createDb()
exportsAndImportsTestCase(version)
}

private suspend fun importBackupZipTestCase(version: String) {
// given
val backupUri = copyTestResourceToInternalStorage("backups/$version.zip")

// when
val res = useCase.importBackupFile(backupUri, onProgress = {})

// then
res.shouldBeSuccessful()
}

private suspend fun importBackupJsonTestCase(version: String) {
// given
val backupUri = copyTestResourceToInternalStorage("backups/$version.json")

// when
val res = useCase.importBackupFile(backupUri, onProgress = {})

// then
res.shouldBeSuccessful()
}

private suspend fun exportsAndImportsTestCase(version: String) {
// given
val backupUri = copyTestResourceToInternalStorage("backups/$version.zip")
// preload data
useCase.importBackupFile(backupUri, onProgress = {}).shouldBeSuccessful()
val exportedFileUri = tempAndroidFile("exported", ".zip").toUri()

// then
useCase.exportToFile(exportedFileUri)
val reImportRes = useCase.importBackupFile(backupUri, onProgress = {})

// then
reImportRes.shouldBeSuccessful()
}

private fun copyTestResourceToInternalStorage(resPath: String): Uri {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val assetManager = context.assets
val inputStream = assetManager.open(resPath)
val outputFile = tempAndroidFile("temp-backup", resPath.split(".").last())
outputFile.outputStream().use { fileOut ->
fileOut.write(inputStream.readBytes())
}
return Uri.fromFile(outputFile)
}

private fun tempAndroidFile(prefix: String, suffix: String): File {
val context = InstrumentationRegistry.getInstrumentation().targetContext
return File.createTempFile(prefix, suffix, context.filesDir)
}

private fun ImportResult.shouldBeSuccessful() {
failedRows.shouldBeEmpty()
categoriesImported shouldBeGreaterThan 0
accountsImported shouldBeGreaterThan 0
transactionsImported shouldBeGreaterThan 0
}
}
62 changes: 35 additions & 27 deletions ivy-data/src/main/java/com/ivy/data/backup/BackupDataUseCase.kt
Original file line number Diff line number Diff line change
@@ -4,7 +4,6 @@ import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import com.ivy.base.legacy.SharedPrefs
import com.ivy.base.legacy.readFile
import com.ivy.base.legacy.unzip
import com.ivy.base.legacy.zip
import com.ivy.base.threading.DispatchersProvider
@@ -24,6 +23,7 @@ import com.ivy.data.db.dao.write.WriteLoanRecordDao
import com.ivy.data.db.dao.write.WritePlannedPaymentRuleDao
import com.ivy.data.db.dao.write.WriteSettingsDao
import com.ivy.data.db.dao.write.WriteTransactionDao
import com.ivy.data.file.IvyFileReader
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.async
@@ -58,6 +58,7 @@ class BackupDataUseCase @Inject constructor(
private val context: Context,
private val json: Json,
private val dispatchersProvider: DispatchersProvider,
private val fileReader: IvyFileReader,
) {
suspend fun exportToFile(
zipFileUri: Uri
@@ -125,38 +126,15 @@ class BackupDataUseCase @Inject constructor(
return hashmap
}

suspend fun import(
suspend fun importBackupFile(
backupFileUri: Uri,
onProgress: suspend (progressPercent: Double) -> Unit
): ImportResult = withContext(dispatchersProvider.io) {
return@withContext try {
val jsonString = try {
val folderName = "backup" + System.currentTimeMillis()
val cacheFolderPath = File(context.cacheDir, folderName)

unzip(context, backupFileUri, cacheFolderPath)

val filesArray = cacheFolderPath.listFiles()

onProgress(0.05)

if (filesArray == null || filesArray.isEmpty()) {
error("Couldn't unzip")
}

val filesList = filesArray.toList().filter {
hasJsonExtension(it)
}

onProgress(0.1)

if (filesList.size != 1) {
error("Didn't unzip exactly one file.")
}

readFile(context, filesList[0].toUri(), Charsets.UTF_16)
extractAndReadBackupZip(backupFileUri, onProgress)
} catch (e: Exception) {
readFile(context, backupFileUri, Charsets.UTF_16)
fileReader.read(backupFileUri, Charsets.UTF_16).getOrNull()
} ?: ""

importJson(jsonString, onProgress, clearCacheDir = true)
@@ -172,6 +150,36 @@ class BackupDataUseCase @Inject constructor(
}
}

private suspend fun extractAndReadBackupZip(
backupFileUri: Uri,
onProgress: suspend (progressPercent: Double) -> Unit
): String? {
val folderName = "backup" + System.currentTimeMillis()
val cacheFolderPath = File(context.cacheDir, folderName)

unzip(context, backupFileUri, cacheFolderPath)

val filesArray = cacheFolderPath.listFiles()

onProgress(0.05)

if (filesArray == null || filesArray.isEmpty()) {
error("Couldn't unzip")
}

val filesList = filesArray.toList().filter {
hasJsonExtension(it)
}

onProgress(0.1)

if (filesList.size != 1) {
error("Didn't unzip exactly one file.")
}

return fileReader.read(filesList[0].toUri(), Charsets.UTF_16).getOrNull()
}

suspend fun importJson(
jsonString: String,
onProgress: suspend (Double) -> Unit = {},
66 changes: 66 additions & 0 deletions ivy-data/src/main/java/com/ivy/data/file/IvyFileReader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.ivy.data.file

import android.content.Context
import android.net.Uri
import arrow.core.Either
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.BufferedReader
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStreamReader
import java.nio.charset.Charset
import javax.inject.Inject

class IvyFileReader @Inject constructor(
@ApplicationContext
private val appContext: Context
) {
fun read(
uri: Uri,
charset: Charset = Charsets.UTF_8
): Either<Failure, String> {
return try {
val contentResolver = appContext.contentResolver
var fileContent: String? = null

contentResolver.openFileDescriptor(uri, "r")?.use {
FileInputStream(it.fileDescriptor).use { fileInputStream ->
fileContent = readFileContent(
fileInputStream = fileInputStream,
charset = charset
)
}
}

Either.Right(fileContent!!)
} catch (e: FileNotFoundException) {
Either.Left(Failure.FileNotFound(e))
} catch (e: Exception) {
Either.Left(Failure.IO(e))
}
}

@Throws(IOException::class)
private fun readFileContent(
fileInputStream: FileInputStream,
charset: Charset
): String {
BufferedReader(InputStreamReader(fileInputStream, charset)).use { br ->
val sb = StringBuilder()
var line: String?
while (br.readLine().also { line = it } != null) {
sb.append(line)
sb.append('\n')
}
return sb.toString()
}
}

sealed interface Failure {
val e: Throwable

data class FileNotFound(override val e: Throwable) : Failure
data class IO(override val e: Throwable) : Failure
}
}
Original file line number Diff line number Diff line change
@@ -48,6 +48,7 @@ class BackupDataUseCaseTest : FreeSpec({
sharedPrefs = mockk(relaxed = true),
json = KotlinxSerializationModule.provideJson(),
dispatchersProvider = TestDispatchersProvider,
fileReader = mockk(relaxed = true)
)

suspend fun backupTestCase(backupVersion: String) {
10 changes: 0 additions & 10 deletions ivy-testing/src/main/java/com/ivy/testing/TestResourceUtil.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
package com.ivy.testing

import java.io.File
import java.io.FileInputStream

fun testResourceInputStream(resPath: String): FileInputStream {
try {
val file = testResource(resPath)
return FileInputStream(file)
} catch (e: Exception) {
throw TestResourceLoadException(resPath, e)
}
}

fun testResource(resPath: String): File {
try {
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@ import com.ivy.importdata.csv.domain.parseToAccount
import com.ivy.importdata.csv.domain.parseToAccountCurrency
import com.ivy.importdata.csv.domain.parseTransactionType
import com.ivy.navigation.Navigation
import com.ivy.wallet.domain.deprecated.logic.csv.IvyFileReader
import com.ivy.data.file.IvyFileReader
import com.opencsv.CSVReaderBuilder
import com.opencsv.validators.LineValidator
import com.opencsv.validators.RowValidator
@@ -463,7 +463,7 @@ class CSVViewModel @Inject constructor(
charset: Charset = Charsets.UTF_8
): List<CSVRow>? {
return try {
val fileContent = fileReader.read(uri, charset) ?: return null
val fileContent = fileReader.read(uri, charset).getOrNull() ?: return null
parseCSV(fileContent, normalizeCSV).takeIf { it.isNotEmpty() }
} catch (e: Exception) {
if (charset != Charsets.UTF_16) {
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ import com.ivy.navigation.Navigation
import com.ivy.onboarding.viewmodel.OnboardingViewModel
import com.ivy.wallet.domain.deprecated.logic.csv.CSVMapper
import com.ivy.wallet.domain.deprecated.logic.csv.CSVNormalizer
import com.ivy.wallet.domain.deprecated.logic.csv.IvyFileReader
import com.ivy.data.file.IvyFileReader
import com.ivy.data.backup.ImportResult
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.persistentListOf
@@ -84,7 +84,7 @@ class ImportViewModel @Inject constructor(
_importResult.value = if (hasCSVExtension(context, fileUri)) {
restoreCSVFile(fileUri = fileUri, importType = importType)
} else {
backupDataUseCase.import(
backupDataUseCase.importBackupFile(
backupFileUri = fileUri
) { progressPercent ->
com.ivy.legacy.utils.uiThread {
@@ -110,8 +110,8 @@ class ImportViewModel @Inject constructor(
ImportType.IVY -> Charsets.UTF_16
else -> Charsets.UTF_8
}
)
if (rawCSV == null || rawCSV.isBlank()) {
).getOrNull()
if (rawCSV.isNullOrBlank()) {
return@ioThread ImportResult(
rowsFound = 0,
transactionsImported = 0,

This file was deleted.