diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/domain/interactor/OptionsSelectionInteractor.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/domain/interactor/OptionsSelectionInteractor.kt index ce997bf..72fc1ce 100644 --- a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/domain/interactor/OptionsSelectionInteractor.kt +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/domain/interactor/OptionsSelectionInteractor.kt @@ -28,32 +28,30 @@ import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesSetSelectedQtspPartial import eu.europa.ec.eudi.rqesui.domain.controller.RqesController import eu.europa.ec.eudi.rqesui.domain.entities.error.EudiRQESUiError import eu.europa.ec.eudi.rqesui.infrastructure.config.data.CertificateData -import eu.europa.ec.eudi.rqesui.infrastructure.config.data.DocumentData import eu.europa.ec.eudi.rqesui.infrastructure.config.data.QtspData import eu.europa.ec.eudi.rqesui.infrastructure.provider.ResourceProvider import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -internal sealed class OptionsSelectionInteractorGetSelectedFileAndQtspPartialState { +internal sealed class OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState { data class Success( - val selectedFile: DocumentData, - val selectedQtsp: QtspData, - ) : OptionsSelectionInteractorGetSelectedFileAndQtspPartialState() + val certificates: List, + ) : OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState() data class Failure( val error: EudiRQESUiError - ) : OptionsSelectionInteractorGetSelectedFileAndQtspPartialState() + ) : OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState() } -internal sealed class OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState { +internal sealed class OptionsSelectionInteractorGetSelectedQtspPartialState { data class Success( - val certificates: List, - ) : OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState() + val selectedQtsp: QtspData, + ) : OptionsSelectionInteractorGetSelectedQtspPartialState() data class Failure( val error: EudiRQESUiError - ) : OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState() + ) : OptionsSelectionInteractorGetSelectedQtspPartialState() } internal interface OptionsSelectionInteractor { @@ -63,7 +61,7 @@ internal interface OptionsSelectionInteractor { fun updateQtspUserSelection(qtspData: QtspData): EudiRqesSetSelectedQtspPartialState - fun getSelectedFileAndQtsp(): OptionsSelectionInteractorGetSelectedFileAndQtspPartialState + fun getSelectedQtsp(): OptionsSelectionInteractorGetSelectedQtspPartialState suspend fun getServiceAuthorizationUrl(rqesService: RQESService): EudiRqesGetServiceAuthorizationUrlPartialState @@ -173,34 +171,23 @@ internal class OptionsSelectionInteractorImpl( } } - override fun getSelectedFileAndQtsp(): OptionsSelectionInteractorGetSelectedFileAndQtspPartialState { + override fun getSelectedQtsp(): OptionsSelectionInteractorGetSelectedQtspPartialState { return runCatching { - when (val getSelectedFileResponse = eudiRqesController.getSelectedFile()) { - is EudiRqesGetSelectedFilePartialState.Failure -> { - return@runCatching OptionsSelectionInteractorGetSelectedFileAndQtspPartialState.Failure( - error = getSelectedFileResponse.error + when (val getSelectedQtspResponse = eudiRqesController.getSelectedQtsp()) { + is EudiRqesGetSelectedQtspPartialState.Failure -> { + return@runCatching OptionsSelectionInteractorGetSelectedQtspPartialState.Failure( + error = getSelectedQtspResponse.error ) } - is EudiRqesGetSelectedFilePartialState.Success -> { - when (val getSelectedQtspResponse = eudiRqesController.getSelectedQtsp()) { - is EudiRqesGetSelectedQtspPartialState.Failure -> { - return@runCatching OptionsSelectionInteractorGetSelectedFileAndQtspPartialState.Failure( - error = getSelectedQtspResponse.error - ) - } - - is EudiRqesGetSelectedQtspPartialState.Success -> { - return@runCatching OptionsSelectionInteractorGetSelectedFileAndQtspPartialState.Success( - selectedFile = getSelectedFileResponse.file, - selectedQtsp = getSelectedQtspResponse.qtsp, - ) - } - } + is EudiRqesGetSelectedQtspPartialState.Success -> { + return@runCatching OptionsSelectionInteractorGetSelectedQtspPartialState.Success( + selectedQtsp = getSelectedQtspResponse.qtsp, + ) } } }.getOrElse { - OptionsSelectionInteractorGetSelectedFileAndQtspPartialState.Failure( + OptionsSelectionInteractorGetSelectedQtspPartialState.Failure( error = EudiRQESUiError( message = it.localizedMessage ?: genericErrorMsg ) diff --git a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/OptionsSelectionViewModel.kt b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/OptionsSelectionViewModel.kt index 2e7e8aa..4acb763 100644 --- a/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/OptionsSelectionViewModel.kt +++ b/rqes-ui-sdk/src/main/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/OptionsSelectionViewModel.kt @@ -27,7 +27,7 @@ import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesSetSelectedQtspPartial import eu.europa.ec.eudi.rqesui.domain.entities.localization.LocalizableKey import eu.europa.ec.eudi.rqesui.domain.interactor.OptionsSelectionInteractor import eu.europa.ec.eudi.rqesui.domain.interactor.OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState -import eu.europa.ec.eudi.rqesui.domain.interactor.OptionsSelectionInteractorGetSelectedFileAndQtspPartialState +import eu.europa.ec.eudi.rqesui.domain.interactor.OptionsSelectionInteractorGetSelectedQtspPartialState import eu.europa.ec.eudi.rqesui.domain.serializer.UiSerializer import eu.europa.ec.eudi.rqesui.infrastructure.config.data.CertificateData import eu.europa.ec.eudi.rqesui.infrastructure.config.data.DocumentData @@ -182,7 +182,7 @@ internal class OptionsSelectionViewModel( CERTIFICATE_SELECTION_STATE -> { createQTSPSelectionItemOnSelectCertificateStep(event = event) - createCertificateSelectionItemOnSelectCertificateStep(event = event) + createCertificateSelectionItemOnSelectCertificateStep() } } } @@ -333,7 +333,6 @@ internal class OptionsSelectionViewModel( is EudiRqesGetSelectedFilePartialState.Failure -> { setState { copy( - documentSelectionItem = null, error = ContentErrorConfig( onRetry = { setEvent(event) }, errorSubTitle = response.error.message, @@ -373,7 +372,6 @@ internal class OptionsSelectionViewModel( is EudiRqesGetSelectedFilePartialState.Failure -> { setState { copy( - qtspServiceSelectionItem = null, error = ContentErrorConfig( onRetry = { setEvent(event) }, errorSubTitle = response.error.message, @@ -406,11 +404,10 @@ internal class OptionsSelectionViewModel( } private fun createQTSPSelectionItemOnSelectCertificateStep(event: Event) { - when (val response = optionsSelectionInteractor.getSelectedFileAndQtsp()) { - is OptionsSelectionInteractorGetSelectedFileAndQtspPartialState.Failure -> { + when (val response = optionsSelectionInteractor.getSelectedQtsp()) { + is OptionsSelectionInteractorGetSelectedQtspPartialState.Failure -> { setState { copy( - qtspServiceSelectionItem = null, error = ContentErrorConfig( onRetry = { setEvent(event) }, errorSubTitle = response.error.message, @@ -423,7 +420,7 @@ internal class OptionsSelectionViewModel( } } - is OptionsSelectionInteractorGetSelectedFileAndQtspPartialState.Success -> { + is OptionsSelectionInteractorGetSelectedQtspPartialState.Success -> { setState { copy( qtspServiceSelectionItem = SelectionOptionUi( @@ -466,45 +463,22 @@ internal class OptionsSelectionViewModel( } } - private fun createCertificateSelectionItemOnSelectCertificateStep(event: Event) { - when (val response = optionsSelectionInteractor.getSelectedFile()) { - is EudiRqesGetSelectedFilePartialState.Failure -> { - setState { - copy( - certificateSelectionItem = null, - error = ContentErrorConfig( - onRetry = { - setEvent(Event.DismissError) - setEvent(event) - }, - errorSubTitle = response.error.message, - onCancel = { - setEvent(Event.DismissError) - setEffect { Effect.Navigation.Finish } - } - ) - ) - } - } - - is EudiRqesGetSelectedFilePartialState.Success -> { - setState { - copy( - certificateSelectionItem = SelectionOptionUi( - overlineText = null, - mainText = resourceProvider.getLocalizedString(LocalizableKey.SelectSigningCertificateTitle), - subtitle = resourceProvider.getLocalizedString(LocalizableKey.SelectCertificateSubtitle), - leadingIcon = AppIcons.StepThree, - trailingIcon = AppIcons.KeyboardArrowRight, - enabled = true, - event = Event.CertificateSelectionItemPressed - ) - ) - } - setEffect { - Effect.OnCertificateSelectionItemCreated - } - } + private fun createCertificateSelectionItemOnSelectCertificateStep() { + setState { + copy( + certificateSelectionItem = SelectionOptionUi( + overlineText = null, + mainText = resourceProvider.getLocalizedString(LocalizableKey.SelectSigningCertificateTitle), + subtitle = resourceProvider.getLocalizedString(LocalizableKey.SelectCertificateSubtitle), + leadingIcon = AppIcons.StepThree, + trailingIcon = AppIcons.KeyboardArrowRight, + enabled = true, + event = Event.CertificateSelectionItemPressed + ) + ) + } + setEffect { + Effect.OnCertificateSelectionItemCreated } } diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestOptionsSelectionInteractor.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestOptionsSelectionInteractor.kt index a316246..6fe4b37 100644 --- a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestOptionsSelectionInteractor.kt +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestOptionsSelectionInteractor.kt @@ -233,7 +233,7 @@ class TestOptionsSelectionInteractor { } //endregion - //region getSelectedFileAndQtsp + //region getSelectedQtsp @Test fun `Given both file and qtsp are selected, When getSelectedFileAndQtsp is called, Then return Success`() { // Arrange @@ -243,12 +243,11 @@ class TestOptionsSelectionInteractor { .thenReturn(EudiRqesGetSelectedQtspPartialState.Success(qtsp = qtspData)) // Act - val result = interactor.getSelectedFileAndQtsp() + val result = interactor.getSelectedQtsp() // Assert - assertTrue(result is OptionsSelectionInteractorGetSelectedFileAndQtspPartialState.Success) - with(result as OptionsSelectionInteractorGetSelectedFileAndQtspPartialState.Success) { - assertEquals(documentData, selectedFile) + assertTrue(result is OptionsSelectionInteractorGetSelectedQtspPartialState.Success) + with(result as OptionsSelectionInteractorGetSelectedQtspPartialState.Success) { assertEquals(qtspData, selectedQtsp) } } @@ -256,18 +255,18 @@ class TestOptionsSelectionInteractor { @Test fun `Given selected file retrieval fails, When getSelectedFileAndQtsp is called, Then return Failure`() { // Arrange - val error = EudiRQESUiError(message = mockedPlainFailureMessage) + val error = EudiRQESUiError(message = mockedGenericErrorMessage) whenever(eudiController.getSelectedFile()) .thenReturn(EudiRqesGetSelectedFilePartialState.Failure(error)) // Act - val result = interactor.getSelectedFileAndQtsp() + val result = interactor.getSelectedQtsp() // Assert - assertTrue(result is OptionsSelectionInteractorGetSelectedFileAndQtspPartialState.Failure) + assertTrue(result is OptionsSelectionInteractorGetSelectedQtspPartialState.Failure) assertEquals( - error, - (result as OptionsSelectionInteractorGetSelectedFileAndQtspPartialState.Failure).error + error.message, + (result as OptionsSelectionInteractorGetSelectedQtspPartialState.Failure).error.message ) } @@ -281,13 +280,13 @@ class TestOptionsSelectionInteractor { .thenReturn(EudiRqesGetSelectedQtspPartialState.Failure(error = failureError)) // Act - val result = interactor.getSelectedFileAndQtsp() + val result = interactor.getSelectedQtsp() // Assert - assertTrue(result is OptionsSelectionInteractorGetSelectedFileAndQtspPartialState.Failure) + assertTrue(result is OptionsSelectionInteractorGetSelectedQtspPartialState.Failure) assertEquals( failureError, - (result as OptionsSelectionInteractorGetSelectedFileAndQtspPartialState.Failure).error + (result as OptionsSelectionInteractorGetSelectedQtspPartialState.Failure).error ) } //endregion diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/TestOptionsSelectionViewModel.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/TestOptionsSelectionViewModel.kt new file mode 100644 index 0000000..0952e5d --- /dev/null +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/presentation/ui/options_selection/TestOptionsSelectionViewModel.kt @@ -0,0 +1,1322 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European + * Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work + * except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the Licence for the specific language + * governing permissions and limitations under the Licence. + */ + +package eu.europa.ec.eudi.rqesui.presentation.ui.options_selection + +import android.net.Uri +import eu.europa.ec.eudi.rqes.CredentialInfo +import eu.europa.ec.eudi.rqes.core.RQESService +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesGetCredentialAuthorizationUrlPartialState +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesGetQtspsPartialState +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesGetSelectedFilePartialState +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesGetServiceAuthorizationUrlPartialState +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesSetSelectedQtspPartialState +import eu.europa.ec.eudi.rqesui.domain.entities.error.EudiRQESUiError +import eu.europa.ec.eudi.rqesui.domain.entities.localization.LocalizableKey +import eu.europa.ec.eudi.rqesui.domain.extension.toUri +import eu.europa.ec.eudi.rqesui.domain.interactor.OptionsSelectionInteractor +import eu.europa.ec.eudi.rqesui.domain.interactor.OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState +import eu.europa.ec.eudi.rqesui.domain.interactor.OptionsSelectionInteractorGetSelectedQtspPartialState +import eu.europa.ec.eudi.rqesui.domain.serializer.UiSerializer +import eu.europa.ec.eudi.rqesui.infrastructure.config.data.CertificateData +import eu.europa.ec.eudi.rqesui.infrastructure.config.data.DocumentData +import eu.europa.ec.eudi.rqesui.infrastructure.config.data.QtspData +import eu.europa.ec.eudi.rqesui.infrastructure.provider.ResourceProvider +import eu.europa.ec.eudi.rqesui.infrastructure.theme.values.ThemeColors +import eu.europa.ec.eudi.rqesui.presentation.entities.ModalOptionUi +import eu.europa.ec.eudi.rqesui.presentation.entities.SelectionOptionUi +import eu.europa.ec.eudi.rqesui.presentation.entities.config.OptionsSelectionUiConfig +import eu.europa.ec.eudi.rqesui.presentation.entities.config.ViewDocumentUiConfig +import eu.europa.ec.eudi.rqesui.presentation.navigation.SdkScreens +import eu.europa.ec.eudi.rqesui.presentation.navigation.helper.generateComposableArguments +import eu.europa.ec.eudi.rqesui.presentation.navigation.helper.generateComposableNavigationLink +import eu.europa.ec.eudi.rqesui.presentation.ui.component.AppIcons +import eu.europa.ec.eudi.rqesui.presentation.ui.component.wrap.BottomSheetTextData +import eu.europa.ec.eudi.rqesui.util.CoroutineTestRule +import eu.europa.ec.eudi.rqesui.util.mockedAuthorizationUrl +import eu.europa.ec.eudi.rqesui.util.mockedCertificateName +import eu.europa.ec.eudi.rqesui.util.mockedDocumentName +import eu.europa.ec.eudi.rqesui.util.mockedFetchCertificatesFailureMessage +import eu.europa.ec.eudi.rqesui.util.mockedLocalFileUri +import eu.europa.ec.eudi.rqesui.util.mockedPlainFailureMessage +import eu.europa.ec.eudi.rqesui.util.mockedQtspName +import eu.europa.ec.eudi.rqesui.util.mockedSerializedConfig +import eu.europa.ec.eudi.rqesui.util.mockedUri +import eu.europa.ec.eudi.rqesui.util.runFlowTest +import eu.europa.ec.eudi.rqesui.util.runTest +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.test.Test +import kotlin.test.assertNotNull + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class TestSelectQtspViewModel { + + @get:Rule + val coroutineRule = CoroutineTestRule() + + @Mock + private lateinit var optionsSelectionInteractor: OptionsSelectionInteractor + + @Mock + private lateinit var resourceProvider: ResourceProvider + + @Mock + private lateinit var uiSerializer: UiSerializer + + @Mock + private lateinit var qtspData: QtspData + + @Mock + private lateinit var rqesService: RQESService + + @Mock + private lateinit var documentData: DocumentData + + @Mock + private lateinit var authorizationUri: Uri + + @Mock + private lateinit var certificateData: CertificateData + + @Mock + private lateinit var credentialInfo: CredentialInfo + + private val documentFileUri = Uri.parse(mockedLocalFileUri) + + private lateinit var viewModel: OptionsSelectionViewModel + + private lateinit var autoCloseable: AutoCloseable + + private val deserializedConfig = OptionsSelectionUiConfig( + optionsSelectionScreenState = QTSP_SELECTION_STATE + ) + + @Before + fun setUp() { + autoCloseable = MockitoAnnotations.openMocks(this) + + whenever( + uiSerializer.fromBase64( + payload = mockedSerializedConfig, + model = OptionsSelectionUiConfig::class.java, + parser = OptionsSelectionUiConfig.Parser + ) + ).thenReturn(deserializedConfig) + + viewModel = OptionsSelectionViewModel( + optionsSelectionInteractor, + resourceProvider, + uiSerializer, + mockedSerializedConfig + ) + mockLocalizedStrings(resourceProvider) + } + + @After + fun after() { + autoCloseable.close() + } + + //region setInitialState + // Case 1 + // The function setInitialState() is called to initialize the ViewModel's state with default values. + // Case 1 Expected Result: + // 1. The title is set to the localized string for "SignDocument". + // 2. The bottomBarButtonAction is null indicating that it is not shown initially. + // 3. The bottom sheet content is set with a "ConfirmCancellation" structure containing localized + // strings for CancelSignProcessTitle, CancelSignProcessSubtitle, CancelSignProcessPrimaryText, + // CancelSignProcessSecondaryText. + @Test + fun `Given Case 1, When setInitialState is called, Then the expected result is returned`() { + // Act + viewModel.setInitialState() + + // Assert + val expectedState = State( + isLoading = false, + error = null, + isBottomSheetOpen = false, + title = resourceProvider.getLocalizedString(LocalizableKey.SignDocument), + documentSelectionItem = null, + bottomBarButtonAction = null, + sheetContent = OptionsSelectionBottomSheetContent.ConfirmCancellation( + bottomSheetTextData = BottomSheetTextData( + title = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessTitle), + message = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessSubtitle), + positiveButtonText = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessPrimaryText), + negativeButtonText = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessSecondaryText) + ) + ), + selectedQtspIndex = 0, + selectedCertificateIndex = 0, + config = deserializedConfig + ) + assertEquals(expectedState, viewModel.viewState.value) + } + //endregion + + //region setEvent, QTSP options related + // Case 1 + // setEvent with argument: Event.Initialize for state QTSP_SELECTION_STATE + // the ViewModel updates the `documentSelectionItem` in the state correctly. + // Case 1 Expected Result: + // 1. The `selectQtspInteractor.getSelectedFile()` function returns a successful state with `documentData`. + // 2. The `viewModel.setEvent(Event.Initialize)` function is called, triggering the creation of a `SelectionOptionUi`. + // 3. The `qtspServiceSelectionItem` in the ViewModel's state is populated with info to select a Qtsp + // 4. The `certificateSelectionItem` is null initially. + // containing the expected `documentData` and `View` action string. + @Test + fun `Given Case 1, When setEvent is called, Then the expected result is returned`() { + // Arrange + whenever(optionsSelectionInteractor.getSelectedFile()) + .thenReturn(EudiRqesGetSelectedFilePartialState.Success(file = documentData)) + + // Act + viewModel.setEvent( + Event.Initialize( + screenSelectionState = QTSP_SELECTION_STATE + ) + ) + + // Assert + assertEquals( + SelectionOptionUi( + overlineText = resourceProvider.getLocalizedString(LocalizableKey.SelectDocumentTitle), + mainText = documentData.documentName, + subtitle = resourceProvider.getLocalizedString(LocalizableKey.SelectDocumentSubtitle), + actionText = resourceProvider.getLocalizedString(LocalizableKey.View), + leadingIcon = AppIcons.StepOne, + leadingIconTint = ThemeColors.success, + trailingIcon = AppIcons.KeyboardArrowRight, + enabled = true, + event = Event.ViewDocumentItemPressed( + documentData = documentData + ) + ), + viewModel.viewState.value.documentSelectionItem + ) + assertEquals( + SelectionOptionUi( + overlineText = null, + mainText = resourceProvider.getLocalizedString(LocalizableKey.SelectSigningService), + subtitle = resourceProvider.getLocalizedString(LocalizableKey.SelectSigningServiceSubtitle), + actionText = null, + leadingIcon = AppIcons.StepTwo, + trailingIcon = AppIcons.KeyboardArrowRight, + enabled = true, + event = Event.RqesServiceSelectionItemPressed + ), + viewModel.viewState.value.qtspServiceSelectionItem + ) + assertEquals( + null, + viewModel.viewState.value.certificateSelectionItem + ) + } + + // Case 2 + // setEvent with argument: Event.Initialize for state CERTIFICATE_SELECTION_STATE + // The function `createSelectionItem` is tested to ensure that when it sets the success state, + // the ViewModel updates the `documentSelectionItem` in the state correctly. + // Case 2 Expected Result: + // 1. The `selectQtspInteractor.getSelectedFile()` function returns a successful state with `documentData`. + // 2. The `viewModel.setEvent(Event.Initialize)` function is called, triggering the creation of a `SelectionOptionUi`. + // 3. The `certificateSelectionItem` in the ViewModel's state is updated. + @Test + fun `Given Case 2, When setEvent is called, Then the expected result is returned`() { + // Arrange + whenever(optionsSelectionInteractor.getSelectedFile()) + .thenReturn(EudiRqesGetSelectedFilePartialState.Success(file = documentData)) + whenever(optionsSelectionInteractor.getSelectedQtsp()) + .thenReturn( + OptionsSelectionInteractorGetSelectedQtspPartialState.Success( + selectedQtsp = qtspData + ) + ) + + // Act + viewModel.setEvent( + Event.Initialize( + screenSelectionState = CERTIFICATE_SELECTION_STATE + ) + ) + + // Assert + assertEquals( + SelectionOptionUi( + overlineText = resourceProvider.getLocalizedString(LocalizableKey.SelectDocumentTitle), + mainText = documentData.documentName, + subtitle = resourceProvider.getLocalizedString(LocalizableKey.SelectDocumentSubtitle), + actionText = resourceProvider.getLocalizedString(LocalizableKey.View), + leadingIcon = AppIcons.StepOne, + leadingIconTint = ThemeColors.success, + trailingIcon = AppIcons.KeyboardArrowRight, + enabled = true, + event = Event.ViewDocumentItemPressed( + documentData = documentData + ) + ), + viewModel.viewState.value.documentSelectionItem + ) + assertEquals( + SelectionOptionUi( + overlineText = resourceProvider.getLocalizedString( + LocalizableKey.SigningService + ), + mainText = qtspData.name, + subtitle = resourceProvider.getLocalizedString(LocalizableKey.SelectSigningServiceSubtitle), + actionText = null, + leadingIcon = AppIcons.StepTwo, + leadingIconTint = ThemeColors.success, + trailingIcon = AppIcons.KeyboardArrowRight, + enabled = false, + event = Event.RqesServiceSelectionItemPressed + ), + viewModel.viewState.value.qtspServiceSelectionItem + ) + assertEquals( + SelectionOptionUi( + overlineText = null, + mainText = resourceProvider.getLocalizedString(LocalizableKey.SelectSigningCertificateTitle), + subtitle = resourceProvider.getLocalizedString(LocalizableKey.SelectCertificateSubtitle), + leadingIcon = AppIcons.StepThree, + trailingIcon = AppIcons.KeyboardArrowRight, + enabled = true, + event = Event.CertificateSelectionItemPressed + ), + viewModel.viewState.value.certificateSelectionItem + ) + } + + // Case 3 + // The function `setEvent` is tested to ensure that when the event `Event.BottomBarButtonPressed` is triggered + // Case 3 Expected Result: + // 1. The given uri should be opened and the activity should finish + @Test + fun `Given Case 3, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Act + viewModel.setEvent(Event.BottomBarButtonPressed(uri = mockedUri.toUri())) + + // Assert + viewModel.effect.runFlowTest { + assertEquals(Effect.OpenUrl(mockedUri.toUri()), awaitItem()) + assertEquals(Effect.Navigation.Finish, awaitItem()) + } + } + + // Case 4 + // Function setEvent() is called with an Event.DismissError event. + // Case 4 Expected Result: + // 1. The view state should have its error field set to null, indicating that the error has been cleared. + @Test + fun `Given Case 4, When setEvent is called, Then the expected result is returned`() { + // Act + viewModel.setEvent(Event.DismissError) + + // Assert + assertNull(viewModel.viewState.value.error) + } + + // Case 5 + // Function setEvent() is called with an Event.BottomSheet.UpdateBottomSheetState event, + // setting isOpen to true. + // Case 5 Expected Result: + // 1. The bottom sheet should be opened and the view state's `isBottomSheetOpen` field should be true. + @Test + fun `Given Case 5, When setEvent is called, Then the expected result is returned`() { + // Act + viewModel.setEvent(Event.BottomSheet.UpdateBottomSheetState(isOpen = true)) + + // Assert + assertTrue(viewModel.viewState.value.isBottomSheetOpen) + } + + // Case 6 + // Function setEvent() is called with an Event.BottomSheet.CancelSignProcess.PrimaryButtonPressed event. + // Case 6 Expected Result: + // 1. The bottom sheet should be closed and the view state's `isBottomSheetOpen` field should be false. + @Test + fun `Given Case 6, When setEvent is called, Then the expected result is returned`() { + // Act + viewModel.setEvent( + Event.BottomSheet.CancelSignProcess.PrimaryButtonPressed + ) + + // Assert + assertFalse(viewModel.viewState.value.isBottomSheetOpen) + } + + // Case 7 + // Function setEvent() is called with an Event.BottomSheet.CancelSignProcess.SecondaryButtonPressed event. + // Case 7 Expected Result: + // 1. The navigation effect should trigger a finish action. + @Test + fun `Given Case 7, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Act + viewModel.setEvent( + Event.BottomSheet.CancelSignProcess.SecondaryButtonPressed + ) + + // Assert + viewModel.effect.runFlowTest { + assertEquals(Effect.Navigation.Finish, awaitItem()) + } + } + + // Case 8 + // Function setEvent() is called with an Event.ViewDocumentItemPressed event, with + // documentData object as argument. + // Case 8 Expected Result: + // 1. The navigation effect should trigger a screen switch and the expected screen route + // based on the document data should be provided. + @Test + fun `Given Case 8, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + val documentData = + DocumentData(documentName = mockedDocumentName, uri = documentFileUri) + + // Act + viewModel.setEvent( + Event.ViewDocumentItemPressed(documentData) + ) + + // Assert + viewModel.effect.runFlowTest { + val expectedScreenRoute = mockScreenRouteGeneration(documentData) + val expectedEffect = + Effect.Navigation.SwitchScreen(screenRoute = expectedScreenRoute) + assertEquals(expectedEffect, awaitItem()) + } + } + + // Case 9 + // Function setEvent() is called with an Event.BottomSheet.QtspSelectedOnDoneButtonPressed event, + // with qtstData as argument. + // Case 9 Expected Result: + // 1. The BottomSheet should close, emitting Effect.CloseBottomSheet. + // 2. The Qtsp selection should update, emitting Effect.OnSelectedQtspUpdated. + @Test + fun `Given Case 9, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + whenever(optionsSelectionInteractor.updateQtspUserSelection(qtspData)).thenReturn( + EudiRqesSetSelectedQtspPartialState.Success(service = rqesService) + ) + + // Act + viewModel.setEvent(Event.BottomSheet.QtspSelectedOnDoneButtonPressed(qtspData)) + + // Assert + viewModel.effect.runFlowTest { + assertEquals(Effect.CloseBottomSheet, awaitItem()) + assertEquals(Effect.OnSelectedQtspUpdated(service = rqesService), awaitItem()) + } + } + + // Case 10 + // Function setEvent() is called with an Event.FetchServiceAuthorizationUrl event and an + // rqesService object as argument. + // Case 10 Expected Result: + // 1. The service authorization URL should be fetched, triggering Effect.OpenUrl with the authorization Uri. + @Test + fun `Given Case 10, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + val response = EudiRqesGetServiceAuthorizationUrlPartialState.Success( + authorizationUrl = mockedAuthorizationUrl.toUri() + ) + mockGetServiceAuthorizationUrlCall(response = response) + + // Act + viewModel.setEvent(Event.FetchServiceAuthorizationUrl(rqesService)) + + // Assert + viewModel.effect.runFlowTest { + assertTrue(awaitItem() is Effect.OpenUrl) + } + } + + // Case 11 + // Function setEvent(Event.Pop) is called to trigger the event of popping the current screen or action. + // Case 11 Expected Result: + // 1. The sheet content should be updated to the expected "ConfirmCancellation" content. + // 2. The bottom sheet should be shown (Effect.ShowBottomSheet). + @Test + fun `Given Case 11, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + val sheetContent = OptionsSelectionBottomSheetContent.ConfirmCancellation( + bottomSheetTextData = mockConfirmCancellationTextData() + ) + + // Act + viewModel.setEvent(Event.Pop) + + // Assert + assertEquals(sheetContent, viewModel.viewState.value.sheetContent) + viewModel.effect.runFlowTest { + val expectedEffect = Effect.ShowBottomSheet + assertEquals(expectedEffect, awaitItem()) + } + } + //endregion + + + //region setEvent, Certificate step related + // Case 1 + // Event CertificateSelected is triggered by calling setEvent() with a selected certificate index. + // Case 1 Expected Result: + // 1. The ViewModel's state is updated with the selectedCertificateIndex set to the value passed in the event. + @Test + fun `Given Case 1 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + val selectedIndex = 0 + + // Act + viewModel.setEvent( + Event.BottomSheet.CertificateIndexSelectedOnRadioButtonPressed(index = selectedIndex) + ) + + // Assert + assertEquals(selectedIndex, viewModel.viewState.value.selectedCertificateIndex) + } + + // Case 2 + // Events AuthorizeServiceAndFetchCertificates and BottomBarButtonPressed are triggered, simulating a + // scenario where certificates are successfully fetched and an authorization URL is returned. + // Case 2 Expected Result: + // 1. When BottomBarButtonPressed is triggered after successful certificate fetching, an OpenUrl effect is emitted. + // 2. The emitted URL matches the mocked authorization URI. + @Test + fun `Given Case 2 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + val mockedCertificates = listOf(certificateData) + val fetchCertificatesResponse = + OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState.Success( + certificates = mockedCertificates + ) + val authResponse = EudiRqesGetCredentialAuthorizationUrlPartialState.Success( + authorizationUrl = authorizationUri + ) + mockAuthorizeServiceAndFetchCertificatesCall(response = fetchCertificatesResponse) + mockGetCredentialAuthorizationUrlCall(response = authResponse) + + // Act + viewModel.setEvent(Event.AuthorizeServiceAndFetchCertificates) + viewModel.setEvent( + Event.BottomBarButtonPressed( + uri = authorizationUri + ) + ) + + // Assert + viewModel.effect.runFlowTest { + val effect = awaitItem() + assertTrue(effect is Effect.OpenUrl) + assertEquals(authorizationUri, (effect as Effect.OpenUrl).uri) + } + } + + // Case 3 + // Event AuthorizeServiceAndFetchCertificates is triggered, simulating a failure scenario where + // the interactor returns a Failure state with an error message. + // Case 3 Expected Result: + // 1. The ViewModel's state contains a non-null error object. + // 2. The error's subtitle matches the mocked failure message. + @Test + fun `Given Case 3 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + val response = + OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState.Failure( + EudiRQESUiError(message = mockedFetchCertificatesFailureMessage) + ) + mockAuthorizeServiceAndFetchCertificatesCall(response = response) + + // Act + viewModel.setEvent(Event.AuthorizeServiceAndFetchCertificates) + + // Assert + val viewState = viewModel.viewState.value + assertTrue(viewState.error != null) + assertEquals(mockedFetchCertificatesFailureMessage, viewState.error?.errorSubTitle) + } + + // Case 4 + // Function setEvent() is called with an Event.BottomSheet.CancelSignProcess.PrimaryButtonPressed event. + // Case 4 Expected Result: + // 1. The effect should trigger a CloseBottomSheet action, indicating that the bottom sheet will be closed. + @Test + fun `Given Case 4 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Act + viewModel.setEvent( + Event.BottomSheet.CancelSignProcess.PrimaryButtonPressed + ) + + // Assert + viewModel.effect.runFlowTest { + val effect = awaitItem() + assertTrue(effect is Effect.CloseBottomSheet) + } + } + + // Case 5 + // Function setEvent() is called with an Event.BottomSheet.CancelSignProcess.SecondaryButtonPressed event. + // Case 5 Expected Result: + // 1. The effect should trigger a Finish navigation action, indicating that the current screen will be finished. + @Test + fun `Given Case 5 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Act + viewModel.setEvent( + Event.BottomSheet.CancelSignProcess.SecondaryButtonPressed + ) + + // Assert + viewModel.effect.runFlowTest { + val effect = awaitItem() + assertTrue(effect is Effect.Navigation.Finish) + } + } + + // Case 6 + // Function setEvent() is called with an Event.Initialize for QTSP_SELECTION_STATE. + // Case 6 Expected Result: + // 1. The view state should reflect a non-null selectionItem with the correct documentData. + // 2. The effect should trigger Effect.OnSelectionItemCreated, indicating that the selection item + // was successfully created and set. + @Test + fun `Given Case 6 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + val mockSuccessState = EudiRqesGetSelectedFilePartialState.Success(file = documentData) + whenever(optionsSelectionInteractor.getSelectedFile()).thenReturn(mockSuccessState) + + // Act + viewModel.setEvent(Event.Initialize(screenSelectionState = QTSP_SELECTION_STATE)) + + // Assert + viewModel.viewStateHistory.runFlowTest { + val state = awaitItem() + assertEquals(documentData, state.documentSelectionItem?.event?.documentData) + } + } + + // Case 7 + // Function setEvent() is called with an Event.Pop argument. + // Case 7 Expected Result: + // 1. The effect should trigger Effect.ShowBottomSheet, indicating that a bottom sheet is + // displayed to the user. + @Test + fun `Given Case 7 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Act + viewModel.setEvent(Event.Pop) + + // Assert + viewModel.effect.runFlowTest { + assertEquals(Effect.ShowBottomSheet, awaitItem()) + } + } + + // Case 8 + // Function setEvent() is called with an Event.BottomSheet.UpdateBottomSheetState event, + // and argument isOpen with value of true. + // Case 8 Expected Result: + // 1. The view state should reflect that the bottom sheet is open, + // correspondingly isBottomSheetOpen state value should be true. + @Test + fun `Given Case 8 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Act + viewModel.setEvent( + Event.BottomSheet.UpdateBottomSheetState(isOpen = true) + ) + + // Assert + assertTrue(viewModel.viewState.value.isBottomSheetOpen) + } + + // Case 9 + // Function setEvent() is called with an Event.DismissError event. + // Case 9 Expected Result: + // 1. The view state should have its error field set to null, indicating that the error has been cleared. + @Test + fun `Given Case 9 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Act + viewModel.setEvent(Event.DismissError) + + // Assert + assertTrue(viewModel.viewState.value.error == null) + } + + // Case 10 + // Function setEvent() is called with an RqesServiceSelectionItemPressed event. + // Scenario: User is on the Certificate selection step and triggers QTSP selection + // User is on Certificate selection step, QTSP selection is triggered + // Then the bottom sheet with QTSP options should be displayed + // Case 10 Expected Result: + // Bottom sheet content is properly populated with QTSP options + // Bottom sheet text data is correctly set + // Bottom sheet is shown through effect + @Test + fun `Given Case 10 on Certificate Step, When setEvent is called, Then bottom sheet with QTSP options is shown`() = + coroutineRule.runTest { + // Arrange + val mockQtsps = listOf(qtspData) + val selectedIndex = 0 + whenever(qtspData.name).thenReturn(mockedQtspName) + whenever(optionsSelectionInteractor.getQtsps()) + .thenReturn(EudiRqesGetQtspsPartialState.Success(qtsps = mockQtsps)) + + // Act + viewModel.setEvent(Event.RqesServiceSelectionItemPressed) + + // Assert + val expectedBottomSheetContent = OptionsSelectionBottomSheetContent.SelectQTSP( + bottomSheetTextData = BottomSheetTextData( + title = resourceProvider.getLocalizedString(LocalizableKey.SelectServiceTitle), + message = resourceProvider.getLocalizedString(LocalizableKey.SelectServiceSubtitle), + positiveButtonText = resourceProvider.getLocalizedString(LocalizableKey.Done), + negativeButtonText = resourceProvider.getLocalizedString(LocalizableKey.Cancel), + ), + options = listOf( + ModalOptionUi( + title = mockedQtspName, + trailingIcon = null, + event = Event.BottomSheet.QtspSelectedOnDoneButtonPressed( + mockQtsps[selectedIndex] + ), + radioButtonSelected = true + ), + ), + selectedIndex = selectedIndex + ) + + assertEquals(expectedBottomSheetContent, viewModel.viewState.value.sheetContent) + viewModel.effect.runFlowTest { + assertEquals(Effect.ShowBottomSheet, awaitItem()) + } + } + + // Case 11 + // Function setEvent() is called with an Event.RqesServiceSelectionItemPressed event and + // EudiRqesGetQtspsPartialState.Failure is returned + // Case 11 Expected Result: + // 1. Corresponding error should be set into view state. + @Test + fun `Given Case 11 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + val errorMessage = mockedPlainFailureMessage + val failureState = EudiRqesGetQtspsPartialState.Failure( + error = EudiRQESUiError(message = errorMessage) + ) + whenever(optionsSelectionInteractor.getQtsps()) + .thenReturn(failureState) + + // Act + viewModel.setEvent(Event.RqesServiceSelectionItemPressed) + + // Assert + assertNotNull(viewModel.viewState.value.error) + } + + // Case 12 + // Function setEvent() is called with an Event.QtspSelectedOnDoneButtonPressed event on Certificate step and + // EudiRqesGetServiceAuthorizationUrlPartialStateFailure is returned + // Case 12 Expected Result: + // 1. Bottom sheet is closed (CloseBottomSheet effect is emitted) + // 2. Error state is set with correct error message + // 3. Error configuration includes retry and cancel actions + // 4. Error is properly dismissed when cancel is called + @Test + fun `Given Case 12 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + val failureState = EudiRqesSetSelectedQtspPartialState.Failure( + error = EudiRQESUiError(message = mockedPlainFailureMessage) + ) + whenever(optionsSelectionInteractor.updateQtspUserSelection(qtspData)).thenReturn( + failureState + ) + + viewModel.setEvent( + Event.BottomSheet.QtspSelectedOnDoneButtonPressed( + qtspData = qtspData + ) + ) + + // Assert + with(viewModel.viewState.value) { + // Verify error state + assertNotNull(error) + assertEquals(mockedPlainFailureMessage, error.errorSubTitle) + + // Test error configuration + viewModel.effect.runFlowTest { + assertEquals(Effect.CloseBottomSheet, awaitItem()) + } + + // Test retry action + error.onRetry?.invoke() + + // Test cancel action + error.onCancel() + assertNull(viewModel.viewState.value.error) + } + } + + // Case 13 + // Function setEvent() is called with an Event.CertificateSelectionItemPressed event and + // multiple certificates are available in the state + // Case 13 Expected Result: + // 1. Bottom sheet content is populated with list of available certificates + // 2. Each certificate option has correct title and selection state + // 3. Bottom sheet text data contains correct localized strings + // 4. First certificate is selected by default (selectedIndex = 0) + // 5. ShowBottomSheet effect is emitted + @Test + fun `Given Case 13 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + whenever(certificateData.name).thenReturn(mockedCertificateName) + val mockedOtherCertificate = "Certificate 2" + val mockCertificates = listOf( + certificateData, + CertificateData(name = mockedOtherCertificate, certificate = credentialInfo) + ) + + whenever(optionsSelectionInteractor.authorizeServiceAndFetchCertificates()) + .thenReturn( + OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState.Success( + certificates = mockCertificates + ) + ) + + // First fetch the certificates + viewModel.setEvent(Event.AuthorizeServiceAndFetchCertificates) + + // Act + viewModel.setEvent(Event.CertificateSelectionItemPressed) + + // Assert + val expectedBottomSheetContent = OptionsSelectionBottomSheetContent.SelectCertificate( + bottomSheetTextData = BottomSheetTextData( + title = resourceProvider.getLocalizedString(LocalizableKey.SigningCertificates), + message = resourceProvider.getLocalizedString(LocalizableKey.SelectSigningCertificateSubtitle), + positiveButtonText = resourceProvider.getLocalizedString(LocalizableKey.Done), + negativeButtonText = resourceProvider.getLocalizedString(LocalizableKey.Cancel), + ), + options = listOf( + ModalOptionUi( + title = mockedCertificateName, + trailingIcon = null, + event = Event.BottomSheet.CertificateSelectedOnDoneButtonPressed( + mockCertificates[0] + ), + radioButtonSelected = true + ), + ModalOptionUi( + title = mockedOtherCertificate, + trailingIcon = null, + event = Event.BottomSheet.CertificateSelectedOnDoneButtonPressed( + mockCertificates[1] + ), + radioButtonSelected = false + ) + ), + selectedIndex = 0 + ) + + assertEquals( + expectedBottomSheetContent, + viewModel.viewState.value.sheetContent + ) + + // Verify bottom sheet is shown + viewModel.effect.runFlowTest { + assertEquals(Effect.ShowBottomSheet, awaitItem()) + } + } + + // Case 14 + // Function setEvent() is called with an Event.BottomSheet.CertificateSelectedOnDoneButtonPressed event and + // EudiRqesGetCredentialAuthorizationUrlPartialState.Success is returned with authorization URL + // Case 14 Expected Result: + // 1. Bottom bar button is configured with correct text ("Continue") + // 2. Bottom bar button event contains the authorization URL + // 3. Bottom bar is made visible + // 4. No error state is present + // 5. Proper interaction with getCredentialAuthorizationUrl is verified + @Test + fun `Given Case 14 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + val authorizationUrl = Uri.parse(mockedAuthorizationUrl) + val mockedCertificates = listOf( + certificateData + ) + whenever(certificateData.name).thenReturn(mockedCertificateName) + whenever(optionsSelectionInteractor.authorizeServiceAndFetchCertificates()) + .thenReturn( + OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState.Success( + certificates = mockedCertificates + ) + ) + + whenever(optionsSelectionInteractor.getCredentialAuthorizationUrl(certificateData)) + .thenReturn( + EudiRqesGetCredentialAuthorizationUrlPartialState.Success( + authorizationUrl = authorizationUrl + ) + ) + + // First load certificates + viewModel.setEvent( + Event.AuthorizeServiceAndFetchCertificates + ) + + // Act + viewModel.setEvent( + Event.BottomSheet.CertificateSelectedOnDoneButtonPressed( + certificateData + ) + ) + + // Assert + viewModel.viewState.runFlowTest { + with(viewModel.viewState.value) { + assertNotNull(bottomBarButtonAction) + bottomBarButtonAction.let { button -> + assertEquals( + resourceProvider.getLocalizedString(LocalizableKey.Continue), + button.buttonText + ) + + assertEquals( + authorizationUrl, + (button.event).uri + ) + } + + // Bottom bar visibility + assertTrue(isBottomBarButtonVisible) + + // Verify no error state + assertNull(error) + } + } + + // Verify interactions + verify(optionsSelectionInteractor).getCredentialAuthorizationUrl(certificateData) + } + + // Case 15 + // Function setEvent() is called with complete certificate selection flow: + // 1. Initialize with CERTIFICATE_SELECTION_STATE + // 2. Fetch certificates successfully + // 3. Select certificate index + // 4. Show certificate selection + // 5. Complete certificate selection with CertificateSelectedOnDoneButtonPressed + // Case 15 Expected Result: + // 1. Certificate selection item is properly updated with: + // - Correct certificate name as main text + // - Success color for leading icon + // - Disabled state + // 2. Selected file and QTSP data are properly maintained + // 3. Complete flow executes without errors + @Test + fun `Given Case 15 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + whenever(optionsSelectionInteractor.getSelectedFile()) + .thenReturn(EudiRqesGetSelectedFilePartialState.Success(file = documentData)) + whenever(optionsSelectionInteractor.getSelectedQtsp()) + .thenReturn( + OptionsSelectionInteractorGetSelectedQtspPartialState.Success( + selectedQtsp = qtspData + ) + ) + + val authorizationUrl = Uri.parse(mockedAuthorizationUrl) + whenever(certificateData.name).thenReturn(mockedCertificateName) + + // Mock successful certificates fetch + val mockCertificates = listOf(certificateData) + whenever(optionsSelectionInteractor.authorizeServiceAndFetchCertificates()) + .thenReturn( + OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState.Success( + certificates = mockCertificates + ) + ) + + // Mock authorization URL fetch + whenever(optionsSelectionInteractor.getCredentialAuthorizationUrl(certificateData)) + .thenReturn( + EudiRqesGetCredentialAuthorizationUrlPartialState.Success( + authorizationUrl = authorizationUrl + ) + ) + + viewModel.setEvent(Event.Initialize(screenSelectionState = CERTIFICATE_SELECTION_STATE)) + viewModel.setEvent(Event.AuthorizeServiceAndFetchCertificates) + // Click certificate selection screen item + viewModel.setEvent(Event.CertificateSelectionItemPressed) + // Select certificate index + viewModel.setEvent(Event.BottomSheet.CertificateIndexSelectedOnRadioButtonPressed(0)) + + // Act + viewModel.setEvent( + Event.BottomSheet.CertificateSelectedOnDoneButtonPressed( + certificateData + ) + ) + + // Assert + with(viewModel.viewState.value) { + assertNotNull(certificateSelectionItem) + with(certificateSelectionItem) { + assertEquals(mockedCertificateName, mainText) + assertEquals(ThemeColors.success, leadingIconTint) + assertFalse(enabled) + } + } + } + + // Case 16 + // Function setEvent() is called with Event.Initialize(QTSP_SELECTION_STATE) and + // EudiRqesGetSelectedFilePartialState.Failure is returned when attempting to get selected file + // Case 16 Expected Result: + // 1. Error state is set with: + // - Correct error message + // - Working cancel action that: + // * Dismisses error + // * Triggers Navigation.Finish effect + // - Working retry action that: + // * Attempts to get selected file again + // 3. Error handling flow completes properly + @Test + fun `Given Case 16 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + val errorMessage = mockedPlainFailureMessage + whenever(optionsSelectionInteractor.getSelectedFile()) + .thenReturn( + EudiRqesGetSelectedFilePartialState.Failure( + error = EudiRQESUiError(message = errorMessage) + ) + ) + + // Act + viewModel.setEvent( + Event.Initialize(screenSelectionState = QTSP_SELECTION_STATE) + ) + + // Assert + with(viewModel.viewState.value) { + // Verify error state + assertNotNull(error) + with(error) { + // Check error message + assertEquals(errorMessage, errorSubTitle) + + // Test cancel action + onCancel() + + // Verify error is dismissed and navigation effect is triggered + viewModel.effect.runFlowTest { + val effect = awaitItem() + assertTrue(effect is Effect.Navigation.Finish) + } + + // Test retry action + onRetry?.invoke() + verify(optionsSelectionInteractor, times(4)).getSelectedFile() + } + } + } + + // Case 17 + // Function setEvent() is called with Event.BottomSheet.QtspIndexSelectedOnRadioButtonPressed and + // a specific index is selected in the QTSP selection bottom sheet + // Case 17 Expected Result: + // 1. Selected QTSP index in view state is updated to match the selected index + // 2. State update occurs without side effects + @Test + fun `Given Case 17 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + val indexSelected = 0 + + // Act + viewModel.setEvent( + Event.BottomSheet.QtspIndexSelectedOnRadioButtonPressed(index = indexSelected) + ) + + // Assert + assertEquals( + indexSelected, + viewModel.viewState.value.selectedQtspIndex + ) + } + + // Case 18 + // Function setEvent() is called with Event.Initialize(CERTIFICATE_SELECTION_STATE) and + // OptionsSelectionInteractorGetSelectedQtspPartialState.Failure is returned when attempting to get selected QTSP + // (while file selection succeeds) + // Case 18 Expected Result: + // 1. Error state is set with: + // - Correct error message from failure + // - Working cancel action + // - Working retry action that attempts to get selected QTSP again + // 2. File selection success state is maintained + @Test + fun `Given Case 18 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + whenever(optionsSelectionInteractor.getSelectedFile()) + .thenReturn(EudiRqesGetSelectedFilePartialState.Success(file = documentData)) + + val errorMessage = mockedPlainFailureMessage + whenever(optionsSelectionInteractor.getSelectedQtsp()) + .thenReturn( + OptionsSelectionInteractorGetSelectedQtspPartialState.Failure( + error = EudiRQESUiError(message = errorMessage) + ) + ) + + // Act + viewModel.setEvent( + Event.Initialize(screenSelectionState = CERTIFICATE_SELECTION_STATE) + ) + + // Assert + with(viewModel.viewState.value) { + // Assert error state + assertNotNull(error) + with(error) { + assertEquals(errorMessage, errorSubTitle) + // Test cancel action + onCancel() + // Test retry action + onRetry?.invoke() + } + } + } + + // Case 19 + // Function setEvent() is called with complete certificate selection flow but + // EudiRqesGetCredentialAuthorizationUrlPartialState.Failure is returned when fetching authorization URL + // Flow: + // 1. Successfully fetch certificates + // 2. Select certificate index + // 3. Attempt to get credential authorization URL (fails) + // Case 19 Expected Result: + // 1. Error state is set with: + // - Correct error message + // - Working retry action that: + // * Dismisses error + // * Attempts to get authorization URL again + // - Working cancel action that dismisses error + // 2. Bottom bar is properly handled: + // - Not visible during error state + // - Button action is null + // 3. Interactions are verified: + // - Authorization URL is requested correct number of times + @Test + fun `Given Case 19 on Certificate Step, When setEvent is called, Then the expected result is returned`() = + coroutineRule.runTest { + // Arrange + val errorMessage = mockedPlainFailureMessage + + // Mock certificate data + whenever(certificateData.name).thenReturn(mockedCertificateName) + + // Mock successful certificates fetch + val mockedCertificates = listOf(certificateData) + whenever(optionsSelectionInteractor.authorizeServiceAndFetchCertificates()) + .thenReturn( + OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState.Success( + certificates = mockedCertificates + ) + ) + + // Mock authorization URL fetch failure + whenever(optionsSelectionInteractor.getCredentialAuthorizationUrl(certificateData)) + .thenReturn( + EudiRqesGetCredentialAuthorizationUrlPartialState.Failure( + error = EudiRQESUiError(message = errorMessage) + ) + ) + + // Initialize certificates + viewModel.setEvent(Event.AuthorizeServiceAndFetchCertificates) + + // Select certificate index + val indexSelected = 0 + viewModel.setEvent( + Event.BottomSheet.CertificateIndexSelectedOnRadioButtonPressed(index = indexSelected) + ) + + // Act + viewModel.setEvent( + Event.BottomSheet.CertificateSelectedOnDoneButtonPressed( + certificateData + ) + ) + + // Assert + with(viewModel.viewState.value) { + // Verify error state + assertNotNull(error) + error.let { errorConfig -> + assertEquals(errorMessage, errorConfig.errorSubTitle) + + // Test retry action + errorConfig.onRetry?.invoke() + // Should dismiss error and retry the event + verify(optionsSelectionInteractor, times(2)) + .getCredentialAuthorizationUrl(certificateData) + + // Test cancel action + errorConfig.onCancel() + // After cancel, error should be dismissed + assertNull(viewModel.viewState.value.error) + } + + // Verify bottom bar is not visible in error state + assertFalse(isBottomBarButtonVisible) + assertNull(bottomBarButtonAction) + } + + // Verify interactions + verify(optionsSelectionInteractor, times(2)) + .getCredentialAuthorizationUrl(certificateData) + } + //endregion + + //region simple events being called and then effect being triggered + + @Test + fun `When BottomSheet CancelCertificateSelection event is sent, Then CloseBottomSheet effect is observed`() = + coroutineRule.runTest { + viewModel.setEvent(Event.BottomSheet.CancelCertificateSelection) + + viewModel.effect.runFlowTest { + val effect = awaitItem() + assertTrue(effect is Effect.CloseBottomSheet) + } + } + + @Test + fun `When Finish event is sent, Then Navigation Finish effect is observed`() = + coroutineRule.runTest { + viewModel.setEvent(Event.Finish) + + viewModel.effect.runFlowTest { + val effect = awaitItem() + assertTrue(effect is Effect.Navigation.Finish) + } + } + + @Test + fun `When BottomSheet CancelQtspSelection event is sent, Then CloseBottomSheet effect is observed`() = + coroutineRule.runTest { + viewModel.setEvent(Event.BottomSheet.CancelQtspSelection) + + viewModel.effect.runFlowTest { + val effect = awaitItem() + assertTrue(effect is Effect.CloseBottomSheet) + } + } + //end region + + + //region of helper functions + private fun mockLocalizedStrings(resourceProvider: ResourceProvider) { + LocalizableKey.entries.forEach { key -> + whenever(resourceProvider.getLocalizedString(key)) + .thenReturn(key.defaultTranslation()) + } + } + + private fun mockScreenRouteGeneration(documentData: DocumentData): String { + return generateComposableNavigationLink( + screen = SdkScreens.ViewDocument, + arguments = generateComposableArguments( + arguments = mapOf( + ViewDocumentUiConfig.serializedKeyName to uiSerializer.toBase64( + model = ViewDocumentUiConfig( + isSigned = true, + documentData = documentData + ), + parser = ViewDocumentUiConfig.Parser + ) + ) + ) + ) + } + + private suspend fun mockGetServiceAuthorizationUrlCall(response: EudiRqesGetServiceAuthorizationUrlPartialState) { + whenever(optionsSelectionInteractor.getServiceAuthorizationUrl(rqesService)) + .thenReturn(response) + } + + private fun mockConfirmCancellationTextData(): BottomSheetTextData { + return BottomSheetTextData( + title = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessTitle), + message = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessSubtitle), + positiveButtonText = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessPrimaryText), + negativeButtonText = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessSecondaryText), + ) + } + + private suspend fun mockAuthorizeServiceAndFetchCertificatesCall( + response: OptionsSelectionInteractorAuthorizeServiceAndFetchCertificatesPartialState + ) { + whenever(optionsSelectionInteractor.authorizeServiceAndFetchCertificates()) + .thenReturn(response) + } + + private suspend fun mockGetCredentialAuthorizationUrlCall(response: EudiRqesGetCredentialAuthorizationUrlPartialState) { + whenever(optionsSelectionInteractor.getCredentialAuthorizationUrl(certificateData)) + .thenReturn(response) + } + //endregion +} \ No newline at end of file diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/presentation/ui/select_certificate/TestSelectCertificateViewModel.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/presentation/ui/select_certificate/TestSelectCertificateViewModel.kt deleted file mode 100644 index 5a278ad..0000000 --- a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/presentation/ui/select_certificate/TestSelectCertificateViewModel.kt +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Copyright (c) 2023 European Commission - * - * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European - * Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work - * except in compliance with the Licence. - * - * You may obtain a copy of the Licence at: - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF - * ANY KIND, either express or implied. See the Licence for the specific language - * governing permissions and limitations under the Licence. - */ - -/** -package eu.europa.ec.eudi.rqesui.presentation.ui.select_certificate - - -import android.net.Uri -import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesGetCredentialAuthorizationUrlPartialState -import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesGetSelectedFilePartialState -import eu.europa.ec.eudi.rqesui.domain.entities.error.EudiRQESUiError -import eu.europa.ec.eudi.rqesui.domain.entities.localization.LocalizableKey -import eu.europa.ec.eudi.rqesui.infrastructure.config.data.CertificateData -import eu.europa.ec.eudi.rqesui.infrastructure.config.data.DocumentData -import eu.europa.ec.eudi.rqesui.infrastructure.provider.ResourceProvider -import eu.europa.ec.eudi.rqesui.util.CoroutineTestRule -import eu.europa.ec.eudi.rqesui.util.mockedFetchCertificatesFailureMessage -import eu.europa.ec.eudi.rqesui.util.runFlowTest -import eu.europa.ec.eudi.rqesui.util.runTest -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertNotNull -import junit.framework.TestCase.assertTrue -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.whenever -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import kotlin.test.Test - -@RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE) -class TestSelectCertificateViewModel { - @get:Rule - val coroutineRule = CoroutineTestRule() - - @Mock - private lateinit var selectCertificateInteractor: SelectCertificateInteractor - - @Mock - private lateinit var resourceProvider: ResourceProvider - - @Mock - private lateinit var authorizationUri: Uri - - @Mock - private lateinit var certificateData: CertificateData - - @Mock - private lateinit var documentData: DocumentData - - private lateinit var viewModel: SelectCertificateViewModel - - private lateinit var autoCloseable: AutoCloseable - - @Before - fun setUp() { - autoCloseable = MockitoAnnotations.openMocks(this) - viewModel = SelectCertificateViewModel(selectCertificateInteractor, resourceProvider) - - mockLocalizedStrings(resourceProvider) - } - - @After - fun after() { - autoCloseable.close() - } - - //region setInitialState - // Case 1 - // Function setInitialState() is called to initialize the ViewModel's state. - // Case 1 Expected Result: - // 1. The title is set to the localized string for SignDocument. - // 2. The subtitle is set to the localized string for SelectCertificateTitle. - // 3. The certificates section title is set to the localized string for SelectCertificateSubtitle. - // 4. The bottom bar button text is set to the localized string for Sign. - @Test - fun `Given Case 1, When setInitialState is called, Then the expected result is returned`() { - // Act - viewModel.setInitialState() - - // Assert - assertEquals( - resourceProvider.getLocalizedString(LocalizableKey.SignDocument), - viewModel.viewState.value.title - ) - assertEquals( - resourceProvider.getLocalizedString(LocalizableKey.SelectCertificateTitle), - viewModel.viewState.value.subtitle - ) - assertEquals( - resourceProvider.getLocalizedString(LocalizableKey.SelectCertificateSubtitle), - viewModel.viewState.value.certificatesSectionTitle - ) - assertEquals( - resourceProvider.getLocalizedString(LocalizableKey.Sign), - viewModel.viewState.value.bottomBarButtonText - ) - } - //endregion - - //region setEvent - // Case 1 - // Event CertificateSelected is triggered by calling setEvent() with a selected certificate index. - // Case 1 Expected Result: - // 1. The ViewModel's state is updated with the selectedCertificateIndex set to the value passed in the event. - @Test - fun `Given Case 1, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Arrange - val selectedIndex = 0 - - // Act - viewModel.setEvent( - Event.CertificateSelected(index = selectedIndex) - ) - - // Assert - assertEquals(selectedIndex, viewModel.viewState.value.selectedCertificateIndex) - } - - // Case 2 - // Events AuthorizeServiceAndFetchCertificates and BottomBarButtonPressed are triggered, simulating a - // scenario where certificates are successfully fetched and an authorization URL is returned. - // Case 2 Expected Result: - // 1. When BottomBarButtonPressed is triggered after successful certificate fetching, an OpenUrl effect is emitted. - // 2. The emitted URL matches the mocked authorization URI. - @Test - fun `Given Case 2, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Arrange - val mockedCertificates = listOf(certificateData) - val fetchCertificatesResponse = - SelectCertificateInteractorAuthorizeServiceAndFetchCertificatesPartialState.Success( - certificates = mockedCertificates - ) - val authResponse = EudiRqesGetCredentialAuthorizationUrlPartialState.Success( - authorizationUrl = authorizationUri - ) - mockAuthorizeServiceAndFetchCertificatesCall(response = fetchCertificatesResponse) - mockGetCredentialAuthorizationUrlCall(response = authResponse) - - // Act - viewModel.setEvent(Event.AuthorizeServiceAndFetchCertificates) - viewModel.setEvent(Event.BottomBarButtonPressed) - - // Assert - viewModel.effect.runFlowTest { - val effect = awaitItem() - assertTrue(effect is Effect.OpenUrl) - assertEquals(authorizationUri, (effect as Effect.OpenUrl).uri) - } - } - - // Case 3 - // Event AuthorizeServiceAndFetchCertificates is triggered, simulating a failure scenario where - // the interactor returns a Failure state with an error message. - // Case 3 Expected Result: - // 1. The ViewModel's state contains a non-null error object. - // 2. The error's subtitle matches the mocked failure message. - @Test - fun `Given Case 3, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Arrange - val response = - SelectCertificateInteractorAuthorizeServiceAndFetchCertificatesPartialState.Failure( - EudiRQESUiError(message = mockedFetchCertificatesFailureMessage) - ) - mockAuthorizeServiceAndFetchCertificatesCall(response = response) - - // Act - viewModel.setEvent(Event.AuthorizeServiceAndFetchCertificates) - - // Assert - val viewState = viewModel.viewState.value - assertTrue(viewState.error != null) - assertEquals(mockedFetchCertificatesFailureMessage, viewState.error?.errorSubTitle) - } - - // Case 4 - // Function setEvent() is called with an Event.BottomSheet.CancelSignProcess.PrimaryButtonPressed event. - // Case 4 Expected Result: - // 1. The effect should trigger a CloseBottomSheet action, indicating that the bottom sheet will be closed. - @Test - fun `Given Case 4, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Act - viewModel.setEvent( - Event.BottomSheet.CancelSignProcess.PrimaryButtonPressed - ) - - // Assert - viewModel.effect.runFlowTest { - val effect = awaitItem() - assertTrue(effect is Effect.CloseBottomSheet) - } - } - - // Case 5 - // Function setEvent() is called with an Event.BottomSheet.CancelSignProcess.SecondaryButtonPressed event. - // Case 5 Expected Result: - // 1. The effect should trigger a Finish navigation action, indicating that the current screen will be finished. - @Test - fun `Given Case 5, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Act - viewModel.setEvent(Event.BottomSheet.CancelSignProcess.SecondaryButtonPressed) - - // Assert - viewModel.effect.runFlowTest { - val effect = awaitItem() - assertTrue(effect is Effect.Navigation.Finish) - } - } - - // Case 6 - // Function setEvent() is called with an Event.Init. - // Case 6 Expected Result: - // 1. The view state should reflect a non-null selectionItem with the correct documentData. - // 2. The effect should trigger Effect.OnSelectionItemCreated, indicating that the selection item - // was successfully created and set. - @Test - fun `Given Case 6, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Arrange - val mockSuccessState = EudiRqesGetSelectedFilePartialState.Success(file = documentData) - whenever(selectCertificateInteractor.getSelectedFile()).thenReturn(mockSuccessState) - - // Act - viewModel.setEvent(Event.Init) - - // Assert - viewModel.viewStateHistory.runFlowTest { - val state = awaitItem() - assertNotNull(state.selectionItem) - assertEquals(documentData, state.selectionItem?.documentData) - } - - viewModel.effect.runFlowTest { - assertEquals(Effect.OnSelectionItemCreated, awaitItem()) - } - } - - // Case 7 - // Function setEvent() is called with an Event.Pop argument. - // Case 7 Expected Result: - // 1. The effect should trigger Effect.ShowBottomSheet, indicating that a bottom sheet is - // displayed to the user. - @Test - fun `Given Case 7, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Act - viewModel.setEvent(Event.Pop) - - // Assert - viewModel.effect.runFlowTest { - assertEquals(Effect.ShowBottomSheet, awaitItem()) - } - } - - // Case 8 - // Function setEvent() is called with an Event.BottomSheet.UpdateBottomSheetState event, - // and argument isOpen with value of true. - // Case 8 Expected Result: - // 1. The view state should reflect that the bottom sheet is open, - // correspondingly isBottomSheetOpen state value should be true. - @Test - fun `Given Case 8, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Act - viewModel.setEvent( - Event.BottomSheet.UpdateBottomSheetState(isOpen = true) - ) - - // Assert - assertTrue(viewModel.viewState.value.isBottomSheetOpen) - } - - // Case 9 - // Function setEvent() is called with an Event.DismissError event. - // Case 9 Expected Result: - // 1. The view state should have its error field set to null, indicating that the error has been cleared. - @Test - fun `Given Case 9, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Act - viewModel.setEvent(Event.DismissError) - - // Assert - assertTrue(viewModel.viewState.value.error == null) - } - //endregion - - //region of helper functions - private fun mockLocalizedStrings(resourceProvider: ResourceProvider) { - LocalizableKey.entries.forEach { key -> - whenever(resourceProvider.getLocalizedString(key)).thenReturn(key.defaultTranslation()) - } - } - - private suspend fun mockAuthorizeServiceAndFetchCertificatesCall( - response: SelectCertificateInteractorAuthorizeServiceAndFetchCertificatesPartialState - ) { - whenever(selectCertificateInteractor.authorizeServiceAndFetchCertificates()) - .thenReturn(response) - } - - private suspend fun mockGetCredentialAuthorizationUrlCall(response: EudiRqesGetCredentialAuthorizationUrlPartialState) { - whenever(selectCertificateInteractor.getCredentialAuthorizationUrl(certificateData)) - .thenReturn(response) - } - //endregion -} - */ \ No newline at end of file diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/presentation/ui/select_qtsp/TestSelectQtspViewModel.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/presentation/ui/select_qtsp/TestSelectQtspViewModel.kt deleted file mode 100644 index 66ca761..0000000 --- a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/presentation/ui/select_qtsp/TestSelectQtspViewModel.kt +++ /dev/null @@ -1,447 +0,0 @@ -/* - * Copyright (c) 2023 European Commission - * - * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European - * Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work - * except in compliance with the Licence. - * - * You may obtain a copy of the Licence at: - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF - * ANY KIND, either express or implied. See the Licence for the specific language - * governing permissions and limitations under the Licence. - */ - -/* - -package eu.europa.ec.eudi.rqesui.presentation.ui.select_qtsp - -import android.net.Uri -import eu.europa.ec.eudi.rqes.core.RQESService -import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesGetQtspsPartialState -import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesGetSelectedFilePartialState -import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesGetServiceAuthorizationUrlPartialState -import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesSetSelectedQtspPartialState -import eu.europa.ec.eudi.rqesui.domain.entities.error.EudiRQESUiError -import eu.europa.ec.eudi.rqesui.domain.entities.localization.LocalizableKey -import eu.europa.ec.eudi.rqesui.domain.extension.toUri -import eu.europa.ec.eudi.rqesui.domain.interactor.OptionsSelectionInteractor -import eu.europa.ec.eudi.rqesui.domain.serializer.UiSerializer -import eu.europa.ec.eudi.rqesui.infrastructure.config.data.DocumentData -import eu.europa.ec.eudi.rqesui.infrastructure.config.data.QtspData -import eu.europa.ec.eudi.rqesui.infrastructure.provider.ResourceProvider -import eu.europa.ec.eudi.rqesui.presentation.entities.ModalOptionUi -import eu.europa.ec.eudi.rqesui.presentation.entities.config.ViewDocumentUiConfig -import eu.europa.ec.eudi.rqesui.presentation.navigation.SdkScreens -import eu.europa.ec.eudi.rqesui.presentation.navigation.helper.generateComposableArguments -import eu.europa.ec.eudi.rqesui.presentation.navigation.helper.generateComposableNavigationLink -import eu.europa.ec.eudi.rqesui.presentation.ui.component.wrap.BottomSheetTextData -import eu.europa.ec.eudi.rqesui.util.CoroutineTestRule -import eu.europa.ec.eudi.rqesui.util.mockedAuthorizationUrl -import eu.europa.ec.eudi.rqesui.util.mockedDocumentName -import eu.europa.ec.eudi.rqesui.util.mockedLocalFileUri -import eu.europa.ec.eudi.rqesui.util.mockedPlainFailureMessage -import eu.europa.ec.eudi.rqesui.util.mockedQtspName -import eu.europa.ec.eudi.rqesui.util.runFlowTest -import eu.europa.ec.eudi.rqesui.util.runTest -import junit.framework.TestCase.assertNull -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.whenever -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -@RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE) -class TestSelectQtspViewModel { - - @get:Rule - val coroutineRule = CoroutineTestRule() - - @Mock - private lateinit var selectQtspInteractor: SelectQtspInteractor - - @Mock - private lateinit var resourceProvider: ResourceProvider - - @Mock - private lateinit var uiSerializer: UiSerializer - - @Mock - private lateinit var qtspData: QtspData - - @Mock - private lateinit var rqesService: RQESService - - @Mock - private lateinit var documentData: DocumentData - - private val documentFileUri = Uri.parse(mockedLocalFileUri) - - private lateinit var viewModel: SelectQtspViewModel - - private lateinit var autoCloseable: AutoCloseable - - @Before - fun setUp() { - autoCloseable = MockitoAnnotations.openMocks(this) - viewModel = SelectQtspViewModel(selectQtspInteractor, resourceProvider, uiSerializer) - mockLocalizedStrings(resourceProvider) - } - - @After - fun after() { - autoCloseable.close() - } - - //region setInitialState - // Case 1 - // The function setInitialState() is called to initialize the ViewModel's state with default values. - // Case 1 Expected Result: - // 1. The title is set to the localized string for "SignDocument". - // 2. The subtitle is set to the localized string for "ConfirmSelectionTitle". - // 3. The bottom bar button text is set to the localized string for "Sign". - // 4. The bottom sheet content is set with a "ConfirmCancellation" structure containing localized - // strings for CancelSignProcessTitle, CancelSignProcessSubtitle, CancelSignProcessPrimaryText, - // CancelSignProcessSecondaryText. - @Test - fun `Given Case 1, When setInitialState is called, Then the expected result is returned`() { - // Act - viewModel.setInitialState() - - // Assert - val expectedState = State( - isLoading = false, - selectionItem = null, - error = null, - isBottomSheetOpen = false, - title = resourceProvider.getLocalizedString(LocalizableKey.SignDocument), - subtitle = resourceProvider.getLocalizedString(LocalizableKey.ConfirmSelectionTitle), - bottomBarButtonText = resourceProvider.getLocalizedString(LocalizableKey.Sign), - sheetContent = SelectQtspBottomSheetContent.ConfirmCancellation( - bottomSheetTextData = BottomSheetTextData( - title = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessTitle), - message = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessSubtitle), - positiveButtonText = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessPrimaryText), - negativeButtonText = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessSecondaryText) - ) - ) - ) - assertEquals(expectedState, viewModel.viewState.value) - } - //endregion - - //region setEvent - // Case 1 - // The function `createSelectionItem` is tested to ensure that when it sets the success state, - // the ViewModel updates the `selectionItem` in the state correctly. - // Case 1 Expected Result: - // 1. The `selectQtspInteractor.getSelectedFile()` function returns a successful state with `documentData`. - // 2. The `viewModel.setEvent(Event.Init)` function is called, triggering the creation of a `SelectionItemUi`. - // 3. The `selectionItem` in the ViewModel's state is updated to reflect the successful selection, - // containing the expected `documentData` and `View` action string. - @Test - fun `Given Case 1, When setEvent is called, Then the expected result is returned`() { - // Arrange - whenever(selectQtspInteractor.getSelectedFile()) - .thenReturn(EudiRqesGetSelectedFilePartialState.Success(file = documentData)) - - // Act - viewModel.setEvent(Event.Init) - - // Assert - assertEquals( - SelectionItemUi( - documentData = documentData, - action = resourceProvider.getLocalizedString(LocalizableKey.View), - subtitle = null - ), - viewModel.viewState.value.selectionItem - ) - } - - // Case 2 - // The function `setEvent` is tested to ensure that when the event `Event.BottomBarButtonPressed` is triggered - // Case 2 Expected Result: - // 1. The `selectQtspInteractor.getQtsps()` function returns a successful state with a list of QTSP data. - // 2. The `viewModel.setEvent(Event.BottomBarButtonPressed)` function is called, triggering the update of the `sheetContent`. - // 3. The `sheetContent` in the ViewModel's state is updated to reflect a `SelectQtspBottomSheetContent.SelectQTSP`. - // 4. The `bottomSheetTextData` is set with localized string values for the title and message. - // 5. The `options` in the `sheetContent` are a list of `ModalOptionUi` objects, each corresponding to a QTSP data entry - @Test - fun `Given Case 2, When setEvent is called, Then the expected result is returned`() { - // Arrange - val mockedQtspDataList = listOf(qtspData) - whenever(qtspData.name).thenReturn(mockedQtspName) - whenever(selectQtspInteractor.getQtsps()) - .thenReturn(EudiRqesGetQtspsPartialState.Success(qtsps = mockedQtspDataList)) - - // Act - viewModel.setEvent(Event.BottomBarButtonPressed) - - // Assert - val bottomSheetOptions: List> = - mockedQtspDataList.map { qtspData -> - ModalOptionUi( - title = qtspData.name, - trailingIcon = null, - event = Event.BottomSheet.QtspSelected(qtspData) - ) - } - val sheetContent = SelectQtspBottomSheetContent.SelectQTSP( - bottomSheetTextData = BottomSheetTextData( - title = resourceProvider.getLocalizedString(LocalizableKey.SelectServiceTitle), - message = resourceProvider.getLocalizedString(LocalizableKey.SelectServiceSubtitle), - ), - options = bottomSheetOptions - ) - - assertEquals(sheetContent, viewModel.viewState.value.sheetContent) - } - - // Case 3 - // The function `setEvent` is tested to ensure that when the event `Event.FetchServiceAuthorizationUrl` is triggered - // and a failure response is returned from the `mockGetServiceAuthorizationUrlCall` - // Case 3 Expected Result: - // 1. The `mockGetServiceAuthorizationUrlCall` is called with a response that simulates a failure, - // returning an error. - // 2. The `setEvent` call with event of `Event.FetchServiceAuthorizationUrl` triggers a service authorization fetch action. - // 3. The `viewModel.viewState.value.error` is updated with the error returned in the failure response. - // 4. The test asserts that the `error` in the `viewState` is not null, ensuring that the failure - // is correctly reflected in the ViewModel's state. - @Test - fun `Given Case 3, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Arrange - val response = EudiRqesGetServiceAuthorizationUrlPartialState.Failure( - error = EudiRQESUiError(mockedPlainFailureMessage) - ) - mockGetServiceAuthorizationUrlCall(response = response) - - // Act - viewModel.setEvent(Event.FetchServiceAuthorizationUrl(rqesService)) - - // Assert - assertNotNull(viewModel.viewState.value.error) - } - - // Case 4 - // Function setEvent() is called with an Event.Finish event. - // Case 4 Expected Result: - // 1. The effect should trigger a navigation finish action. - @Test - fun `Given Case 4, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Act - viewModel.setEvent(Event.Finish) - - // Assert - viewModel.effect.runFlowTest { - assertEquals(Effect.Navigation.Finish, awaitItem()) - } - } - - // Case 5 - // Function setEvent() is called with an Event.DismissError event. - // Case 5 Expected Result: - // 1. The view state should have its error field set to null, indicating that the error has been cleared. - @Test - fun `Given Case 5, When setEvent is called, Then the expected result is returned`() { - // Act - viewModel.setEvent(Event.DismissError) - - // Assert - assertNull(viewModel.viewState.value.error) - } - - // Case 6 - // Function setEvent() is called with an Event.BottomSheet.UpdateBottomSheetState event, - // setting isOpen to true. - // Case 6 Expected Result: - // 1. The bottom sheet should be opened and the view state's `isBottomSheetOpen` field should be true. - @Test - fun `Given Case 6, When setEvent is called, Then the expected result is returned`() { - // Act - viewModel.setEvent(Event.BottomSheet.UpdateBottomSheetState(isOpen = true)) - - // Assert - assertTrue(viewModel.viewState.value.isBottomSheetOpen) - } - - // Case 7 - // Function setEvent() is called with an Event.BottomSheet.CancelSignProcess.PrimaryButtonPressed event. - // Case 7 Expected Result: - // 1. The bottom sheet should be closed and the view state's `isBottomSheetOpen` field should be false. - @Test - fun `Given Case 7, When setEvent is called, Then the expected result is returned`() { - // Act - viewModel.setEvent(Event.BottomSheet.CancelSignProcess.PrimaryButtonPressed) - - // Assert - assertFalse(viewModel.viewState.value.isBottomSheetOpen) - } - - // Case 8 - // Function setEvent() is called with an Event.BottomSheet.CancelSignProcess.SecondaryButtonPressed event. - // Case 8 Expected Result: - // 1. The navigation effect should trigger a finish action. - @Test - fun `Given Case 8, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Act - viewModel.setEvent(Event.BottomSheet.CancelSignProcess.SecondaryButtonPressed) - - // Assert - viewModel.effect.runFlowTest { - assertEquals(Effect.Navigation.Finish, awaitItem()) - } - } - - // Case 9 - // Function setEvent() is called with an Event.ViewDocument event, with - // documentData object as argument. - // Case 9 Expected Result: - // 1. The navigation effect should trigger a screen switch and the expected screen route - // based on the document data should be provided. - @Test - fun `Given Case 9, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Arrange - val documentData = - DocumentData(documentName = mockedDocumentName, uri = documentFileUri) - - // Act - viewModel.setEvent(Event.ViewDocument(documentData)) - - // Assert - viewModel.effect.runFlowTest { - val expectedScreenRoute = mockScreenRouteGeneration(documentData) - val expectedEffect = - Effect.Navigation.SwitchScreen(screenRoute = expectedScreenRoute) - assertEquals(expectedEffect, awaitItem()) - } - } - - // Case 10 - // Function setEvent() is called with an Event.BottomSheet.QtspSelected event, with - // qtstData as argument. - // Case 10 Expected Result: - // 1. The BottomSheet should close, emitting Effect.CloseBottomSheet. - // 2. The Qtsp selection should update, emitting Effect.OnSelectedQtspUpdated. - @Test - fun `Given Case 10, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Arrange - whenever(selectQtspInteractor.updateQtspUserSelection(qtspData)).thenReturn( - EudiRqesSetSelectedQtspPartialState.Success(service = rqesService) - ) - - // Act - viewModel.setEvent(Event.BottomSheet.QtspSelected(qtspData)) - - // Assert - viewModel.effect.runFlowTest { - assertEquals(Effect.CloseBottomSheet, awaitItem()) - assertEquals(Effect.OnSelectedQtspUpdated(service = rqesService), awaitItem()) - } - } - - // Case 11 - // Function setEvent() is called with an Event.FetchServiceAuthorizationUrl event and an - // rqesService object as argument. - // Case 11 Expected Result: - // 1. The service authorization URL should be fetched, triggering Effect.OpenUrl with the authorization Uri. - @Test - fun `Given Case 11, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Arrange - val response = EudiRqesGetServiceAuthorizationUrlPartialState.Success( - authorizationUrl = mockedAuthorizationUrl.toUri() - ) - mockGetServiceAuthorizationUrlCall(response = response) - - // Act - viewModel.setEvent(Event.FetchServiceAuthorizationUrl(rqesService)) - - // Assert - viewModel.effect.runFlowTest { - assertTrue(awaitItem() is Effect.OpenUrl) - } - } - - // Case 12 - // Function setEvent(Event.Pop) is called to trigger the event of popping the current screen or action. - // Case 12 Expected Result: - // 1. The sheet content should be updated to the expected "ConfirmCancellation" content. - // 2. The bottom sheet should be shown (Effect.ShowBottomSheet). - @Test - fun `Given Case 12, When setEvent is called, Then the expected result is returned`() = - coroutineRule.runTest { - // Arrange - val sheetContent = SelectQtspBottomSheetContent.ConfirmCancellation( - bottomSheetTextData = mockConfirmCancellationTextData() - ) - - // Act - viewModel.setEvent(Event.Pop) - - // Assert - assertEquals(sheetContent, viewModel.viewState.value.sheetContent) - viewModel.effect.runFlowTest { - val expectedEffect = Effect.ShowBottomSheet - assertEquals(expectedEffect, awaitItem()) - } - } - //endregion - - //region of helper functions - private fun mockLocalizedStrings(resourceProvider: ResourceProvider) { - LocalizableKey.entries.forEach { key -> - whenever(resourceProvider.getLocalizedString(key)).thenReturn(key.defaultTranslation()) - } - } - - private fun mockScreenRouteGeneration(documentData: DocumentData): String { - return generateComposableNavigationLink( - screen = SdkScreens.ViewDocument, - arguments = generateComposableArguments( - arguments = mapOf( - ViewDocumentUiConfig.serializedKeyName to uiSerializer.toBase64( - model = ViewDocumentUiConfig( - isSigned = true, - documentData = documentData - ), - parser = ViewDocumentUiConfig.Parser - ) - ) - ) - ) - } - - private fun mockConfirmCancellationTextData(): BottomSheetTextData { - return BottomSheetTextData( - title = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessTitle), - message = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessSubtitle), - positiveButtonText = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessPrimaryText), - negativeButtonText = resourceProvider.getLocalizedString(LocalizableKey.CancelSignProcessSecondaryText), - ) - } - - private suspend fun mockGetServiceAuthorizationUrlCall(response: EudiRqesGetServiceAuthorizationUrlPartialState) { - whenever(selectQtspInteractor.getServiceAuthorizationUrl(rqesService)) - .thenReturn(response) - } - //endregion -} - */ \ No newline at end of file