Skip to content
Open
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.mockito.kotlin)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.androidx.arch.core.testing)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
@file:Suppress("PackageName")

package ee.ria.DigiDoc.viewmodel

import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import androidx.test.platform.app.InstrumentationRegistry
import ee.ria.DigiDoc.R
import ee.ria.DigiDoc.configuration.repository.ConfigurationRepository
import ee.ria.DigiDoc.cryptolib.Addressee
import ee.ria.DigiDoc.cryptolib.CDOC2Settings
import ee.ria.DigiDoc.cryptolib.CertType
import ee.ria.DigiDoc.cryptolib.CryptoContainer
import ee.ria.DigiDoc.cryptolib.exception.CryptoException
import ee.ria.DigiDoc.cryptolib.repository.RecipientRepository
import ee.ria.DigiDoc.utilsLib.mimetype.MimeTypeResolver
import ee.ria.DigiDoc.viewmodel.shared.SharedContainerViewModel
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.junit.MockitoJUnitRunner
import java.io.File

@RunWith(MockitoJUnitRunner::class)
class EncryptRecipientViewModelTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()

@Mock
private lateinit var mimeTypeResolver: MimeTypeResolver

@Mock
private lateinit var recipientRepository: RecipientRepository

@Mock
private lateinit var configurationRepository: ConfigurationRepository

@Mock
private lateinit var errorStateObserver: Observer<Int?>

private lateinit var context: Context
private lateinit var sharedContainerViewModel: SharedContainerViewModel
private lateinit var viewModel: EncryptRecipientViewModel

@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
context = InstrumentationRegistry.getInstrumentation().targetContext
sharedContainerViewModel = SharedContainerViewModel(context, context.contentResolver)
viewModel =
EncryptRecipientViewModel(
context,
mimeTypeResolver,
recipientRepository,
CDOC2Settings(context, configurationRepository),
configurationRepository,
)
}

@Test
fun encryptRecipientViewModel_encryptContainerWithPassword_returnsEncryptErrorWithNoContainer() =
runTest {
viewModel.errorState.observeForever(errorStateObserver)

viewModel.encryptContainerWithPassword("MyKey", "password123".toByteArray(), sharedContainerViewModel)

assertEquals(R.string.crypto_encrypt_error, viewModel.errorState.value)
}

@Test
fun encryptRecipientViewModel_decryptContainerWithPassword_throwsCryptoExceptionWithNoContainer() =
runTest {
try {
viewModel.decryptContainerWithPassword("password123".toByteArray(), sharedContainerViewModel)
fail("Expected CryptoException to be thrown")
} catch (e: CryptoException) {
assertEquals("No container to decrypt", e.message)
}
}

@Test
fun encryptRecipientViewModel_resetErrorState_clearsErrorState() =
runTest {
viewModel.encryptContainerWithPassword("key", "pass".toByteArray(), sharedContainerViewModel)
viewModel.resetErrorState()

assertNull(viewModel.errorState.value)
}

@Test
fun encryptRecipientViewModel_encryptContainerWithPassword_returnsDataFilesEmptyErrorWithEmptyDataFiles() =
runTest {
sharedContainerViewModel.setCryptoContainer(
CryptoContainer(
context = context,
file = File(context.cacheDir, "test.cdoc"),
dataFiles = ArrayList(),
recipients =
ArrayList<Addressee>().apply {
add(
Addressee(
data = ByteArray(0),
identifier = "key",
serialNumber = null,
givenName = null,
surname = null,
certType = CertType.PasswordType,
validTo = null,
concatKDFAlgorithmURI = null,
),
)
},
decrypted = false,
encrypted = false,
),
)

viewModel.encryptContainerWithPassword("key", "password".toByteArray(), sharedContainerViewModel)

assertEquals(R.string.crypto_encrypt_data_files_empty_error, viewModel.errorState.value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -89,7 +90,9 @@ import ee.ria.DigiDoc.ui.component.shared.LoadingScreen
import ee.ria.DigiDoc.ui.component.shared.MessageDialog
import ee.ria.DigiDoc.ui.component.shared.PreventResize
import ee.ria.DigiDoc.ui.component.shared.Recipient
import ee.ria.DigiDoc.ui.component.shared.StatusAnnouncer
import ee.ria.DigiDoc.ui.component.shared.StatusSnackbarHost
import ee.ria.DigiDoc.ui.component.shared.TabItem
import ee.ria.DigiDoc.ui.component.shared.TabView
import ee.ria.DigiDoc.ui.component.shared.TopBar
import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding
Expand All @@ -105,6 +108,7 @@ import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.sendAccess
import ee.ria.DigiDoc.utils.extensions.reachedBottom
import ee.ria.DigiDoc.utils.snackbar.SnackBarManager.showMessage
import ee.ria.DigiDoc.utils.snackbar.SnackbarType
import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog
import ee.ria.DigiDoc.utilsLib.validator.PersonalCodeValidator
import ee.ria.DigiDoc.viewmodel.EncryptRecipientViewModel
import ee.ria.DigiDoc.viewmodel.EncryptViewModel
Expand All @@ -116,6 +120,10 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.time.Duration.Companion.seconds

private const val RECIPIENT_TAB_INDEX = 0
private const val PASSWORD_TAB_INDEX = 1

@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
Expand All @@ -127,6 +135,7 @@ fun EncryptRecipientScreen(
sharedRecipientViewModel: SharedRecipientViewModel,
encryptRecipientViewModel: EncryptRecipientViewModel = hiltViewModel(),
) {
val logTag = "EncryptRecipientScreen"
val context = LocalContext.current

val scope = rememberCoroutineScope()
Expand All @@ -144,12 +153,16 @@ fun EncryptRecipientScreen(
val containerEncryptedSuccess = remember { mutableStateOf(false) }
val containerEncryptedSuccessText = stringResource(id = R.string.crypto_create_success)

var statusAnnouncement by remember { mutableStateOf("") }

val containerRecipientList =
remember {
mutableStateOf(
encryptRecipientViewModel.getContainerRecipientList(sharedContainerViewModel),
)
}

val hasRecipients = containerRecipientList.value.isNotEmpty()
val showRecipientBottomSheet = remember { mutableStateOf(false) }
var actionRecipient by rememberSaveable { mutableStateOf<Addressee?>(null) }
val clickedRecipient = rememberSaveable { mutableStateOf<Addressee?>(null) }
Expand Down Expand Up @@ -184,7 +197,7 @@ fun EncryptRecipientScreen(
},
)
val isCdoc2 = encryptViewModel.cdocSetting == CDOCSetting.CDOC2
var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(RECIPIENT_TAB_INDEX) }

val tabRecipientTitle = stringResource(R.string.crypto_encrypt_tab_recipient)
val tabPasswordTitle = stringResource(R.string.crypto_encrypt_tab_password)
Expand All @@ -207,11 +220,8 @@ fun EncryptRecipientScreen(
containerRecipientList.value =
encryptRecipientViewModel
.getContainerRecipientList(sharedContainerViewModel)
sendAccessibilityEvent(
context,
getAccessibilityEventType(),
recipientAddedSuccessText,
)
withFrameNanos { }
statusAnnouncement = recipientAddedSuccessText
encryptRecipientViewModel.handleIsRecipientAdded(false)
}
}
Expand All @@ -223,11 +233,6 @@ fun EncryptRecipientScreen(
if (isContainerEncrypted) {
withContext(Main) {
containerEncryptedSuccess.value = true
sendAccessibilityEvent(
context,
getAccessibilityEventType(),
containerEncryptedSuccessText,
)
delay(2000)

encryptRecipientViewModel.handleIsContainerEncrypted(false)
Expand Down Expand Up @@ -269,6 +274,19 @@ fun EncryptRecipientScreen(
}
}

LaunchedEffect(statusAnnouncement) {
if (statusAnnouncement.isNotEmpty()) {
delay(1.seconds)
statusAnnouncement = ""
}
}

LaunchedEffect(hasRecipients) {
if (hasRecipients && selectedTabIndex == PASSWORD_TAB_INDEX) {
selectedTabIndex = RECIPIENT_TAB_INDEX
}
}

Scaffold(
modifier =
modifier
Expand All @@ -293,7 +311,7 @@ fun EncryptRecipientScreen(
},
bottomBar = {
if (cryptoContainer != null) {
if (isCdoc2 && selectedTabIndex == 1) {
if (isCdoc2 && selectedTabIndex == PASSWORD_TAB_INDEX) {
EncryptButtonBottomBar(
modifier = modifier,
encryptButtonIcon = R.drawable.ic_m3_arrow_forward_48dp_wght400,
Expand Down Expand Up @@ -326,54 +344,10 @@ fun EncryptRecipientScreen(
isBottomSheetVisible = isSettingsMenuBottomSheetVisible,
)

if (isCdoc2) {
Column(
modifier = modifier.padding(paddingValues).fillMaxSize(),
) {
TabView(
modifier = modifier,
testTag = "encryptRecipientTabView",
selectedTabIndex = selectedTabIndex,
onTabSelected = { index ->
selectedTabIndex = index
if (index != 0) expanded = false
},
tabItems =
listOf(
Pair(tabRecipientTitle) {
RecipientTabContent(
modifier = Modifier.fillMaxSize(),
expanded = expanded,
onExpandedChange = { expanded = it },
searchText = searchText,
onSearchTextChange = encryptRecipientViewModel::onSearchTextChange,
invalidPersonalCodeMessage = invalidPersonalCodeMessage,
onSearch = { encryptRecipientViewModel.onQueryTextChange(it) },
onDismissSearch = dismissSearch,
recipientList = recipientList,
hasSearched = hasSearched,
containerRecipientList = containerRecipientList.value,
onAddRecipientToContainer = { recipient ->
encryptRecipientViewModel.addRecipientToContainer(
recipient,
sharedContainerViewModel,
)
},
onRecipientClick = { recipient ->
clickedRecipient.value = recipient
showRecipientBottomSheet.value = true
},
)
},
Pair(tabPasswordTitle) {
PasswordTabContent(modifier = Modifier.fillMaxSize())
},
),
)
}
} else {
@Composable
fun RecipientTab(tabModifier: Modifier) {
RecipientTabContent(
modifier = Modifier.padding(paddingValues).fillMaxWidth(),
modifier = tabModifier,
expanded = expanded,
onExpandedChange = { expanded = it },
searchText = searchText,
Expand All @@ -397,11 +371,59 @@ fun EncryptRecipientScreen(
)
}

if (isCdoc2) {
Column(
modifier = modifier.padding(paddingValues).fillMaxSize(),
) {
StatusAnnouncer(message = statusAnnouncement)

TabView(
modifier = modifier,
testTag = "encryptRecipientTabView",
selectedTabIndex = selectedTabIndex,
onTabSelected = { index ->
selectedTabIndex = index
if (index != RECIPIENT_TAB_INDEX) expanded = false
},
tabItems =
listOf(
TabItem(tabRecipientTitle) {
RecipientTab(Modifier.fillMaxSize())
},
TabItem(tabPasswordTitle, enabled = !hasRecipients) {
PasswordTabContent(modifier = Modifier.fillMaxSize())
},
),
)
}
} else {
Column(
modifier = modifier.padding(paddingValues).fillMaxWidth(),
) {
StatusAnnouncer(message = statusAnnouncement)

RecipientTab(Modifier.fillMaxWidth())
}
}

if (showPasswordDialog.value) {
EncryptPasswordDialog(
modifier = modifier,
onDismiss = { showPasswordDialog.value = false },
onEncrypt = { _, _ -> showPasswordDialog.value = false },
onEncrypt = { keyLabel, password ->
debugLog(logTag, "User submitted password encryption dialog")
showPasswordDialog.value = false
encryptionButtonEnabled.value = false
showLoading.value = true
scope.launch(Main) {
encryptRecipientViewModel.encryptContainerWithPassword(
keyLabel = keyLabel,
password = password.toByteArray(Charsets.UTF_8),
sharedContainerViewModel = sharedContainerViewModel,
)
showLoading.value = false
}
},
)
}

Expand Down
Loading
Loading