diff --git a/assembly-logic/src/main/java/eu/europa/ec/assemblylogic/Application.kt b/assembly-logic/src/main/java/eu/europa/ec/assemblylogic/Application.kt index 46547fe81..9422434aa 100644 --- a/assembly-logic/src/main/java/eu/europa/ec/assemblylogic/Application.kt +++ b/assembly-logic/src/main/java/eu/europa/ec/assemblylogic/Application.kt @@ -21,11 +21,6 @@ import eu.europa.ec.analyticslogic.controller.AnalyticsController import eu.europa.ec.assemblylogic.di.setupKoin import eu.europa.ec.corelogic.config.WalletCoreConfig import eu.europa.ec.eudi.wallet.EudiWallet -import eu.europa.ec.resourceslogic.theme.ThemeManager -import eu.europa.ec.resourceslogic.theme.templates.ThemeDimensTemplate -import eu.europa.ec.resourceslogic.theme.values.ThemeColors -import eu.europa.ec.resourceslogic.theme.values.ThemeShapes -import eu.europa.ec.resourceslogic.theme.values.ThemeTypography import org.koin.android.ext.android.inject class Application : Application() { @@ -38,27 +33,12 @@ class Application : Application() { setupKoin() initializeReporting() initializeEudiWallet() - initializeTheme() } private fun initializeReporting() { analyticsController.initialize(this) } - private fun initializeTheme() { - ThemeManager.Builder() - .withLightColors(ThemeColors.lightColors) - .withDarkColors(ThemeColors.darkColors) - .withTypography(ThemeTypography.typo) - .withShapes(ThemeShapes.shapes) - .withDimensions( - ThemeDimensTemplate( - screenPadding = 10.0 - ) - ) - .build() - } - private fun initializeEudiWallet() { EudiWallet.init( applicationContext, diff --git a/common-feature/src/main/java/eu/europa/ec/commonfeature/util/TestsData.kt b/common-feature/src/main/java/eu/europa/ec/commonfeature/util/TestsData.kt index d2d016cb2..994697b38 100644 --- a/common-feature/src/main/java/eu/europa/ec/commonfeature/util/TestsData.kt +++ b/common-feature/src/main/java/eu/europa/ec/commonfeature/util/TestsData.kt @@ -34,9 +34,13 @@ import eu.europa.ec.eudi.iso18013.transfer.DocItem import eu.europa.ec.eudi.iso18013.transfer.DocRequest import eu.europa.ec.eudi.iso18013.transfer.ReaderAuth import eu.europa.ec.eudi.iso18013.transfer.RequestDocument +import eu.europa.ec.eudi.wallet.issue.openid4vci.Offer.TxCodeSpec import eu.europa.ec.uilogic.component.AppIcons import eu.europa.ec.uilogic.component.InfoTextWithNameAndImageData import eu.europa.ec.uilogic.component.InfoTextWithNameAndValueData +import eu.europa.ec.uilogic.config.ConfigNavigation +import eu.europa.ec.uilogic.config.NavigationType +import eu.europa.ec.uilogic.navigation.DashboardScreens @VisibleForTesting(otherwise = VisibleForTesting.NONE) object TestsData { @@ -78,8 +82,21 @@ object TestsData { const val mockedDocumentHasExpired = false const val mockedUserAuthentication = false const val mockedVerifierName = "EUDIW Verifier" + const val mockedIssuerName = "EUDIW Issuer" const val mockedRequestRequiredFieldsTitle = "Verification Data" const val mockedRequestElementIdentifierNotAvailable = "Not available" + const val mockedOfferedDocumentName = "Offered Document" + const val mockedOfferedDocumentDocType = "mocked_offered_document_doc_type" + const val mockedTxCodeSpecFourDigits = 4 + const val mockedSuccessTitle = "Success title" + const val mockedSuccessSubtitle = "Success subtitle" + const val mockedSuccessContentDescription = "Content description" + const val mockedIssuanceErrorMessage = "Issuance error message" + const val mockedInvalidCodeFormatMessage = "Invalid code format message" + const val mockedWalletActivationErrorMessage = "Wallet activation error message" + const val mockedPrimaryButtonText = "Primary button text" + const val mockedRouteArguments = "mockedRouteArguments" + const val mockedTxCode = "mockedTxCode" const val mockedPidDocType = "eu.europa.ec.eudi.pid.1" const val mockedPidNameSpace = "eu.europa.ec.eudi.pid.1" @@ -476,6 +493,24 @@ object TestsData { available = true ) + val mockedConfigNavigationTypePop = ConfigNavigation(navigationType = NavigationType.Pop) + val mockedConfigNavigationTypePush = ConfigNavigation( + navigationType = NavigationType.PushRoute( + route = DashboardScreens.Dashboard.screenRoute + ) + ) + val mockedConfigNavigationTypePopToScreen = ConfigNavigation( + navigationType = NavigationType.PopTo( + screen = DashboardScreens.Dashboard + ) + ) + + val mockedOfferTxCodeSpecFourDigits = + TxCodeSpec( + inputMode = TxCodeSpec.InputMode.NUMERIC, + length = mockedTxCodeSpecFourDigits + ) + val mockedOptionalFieldsForPidWithBasicFields = listOf( TestFieldUi( elementIdentifier = "family_name", diff --git a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentViewModel.kt b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentViewModel.kt index 8d1f145a9..8d417340d 100644 --- a/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentViewModel.kt +++ b/issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/document/add/AddDocumentViewModel.kt @@ -106,7 +106,7 @@ class AddDocumentViewModel( @InjectedParam private val flowType: IssuanceFlowUiConfig, ) : MviViewModel() { - var issuanceJob: Job? = null + private var issuanceJob: Job? = null override fun setInitialState(): State = State( navigatableAction = getNavigatableAction(flowType), diff --git a/issuance-feature/src/test/java/eu/europa/ec/issuancefeature/interactor/document/TestAddDocumentInteractor.kt b/issuance-feature/src/test/java/eu/europa/ec/issuancefeature/interactor/document/TestAddDocumentInteractor.kt index ceb7b74fe..6a79df9cf 100644 --- a/issuance-feature/src/test/java/eu/europa/ec/issuancefeature/interactor/document/TestAddDocumentInteractor.kt +++ b/issuance-feature/src/test/java/eu/europa/ec/issuancefeature/interactor/document/TestAddDocumentInteractor.kt @@ -21,19 +21,30 @@ import eu.europa.ec.authenticationlogic.controller.authentication.BiometricsAvai import eu.europa.ec.authenticationlogic.controller.authentication.DeviceAuthenticationResult import eu.europa.ec.authenticationlogic.model.BiometricCrypto import eu.europa.ec.commonfeature.config.IssuanceFlowUiConfig +import eu.europa.ec.commonfeature.config.SuccessUIConfig import eu.europa.ec.commonfeature.interactor.DeviceAuthenticationInteractor import eu.europa.ec.commonfeature.util.TestsData.mockedAgeOptionItemUi +import eu.europa.ec.commonfeature.util.TestsData.mockedConfigNavigationTypePopToScreen +import eu.europa.ec.commonfeature.util.TestsData.mockedConfigNavigationTypePush import eu.europa.ec.commonfeature.util.TestsData.mockedMdlOptionItemUi import eu.europa.ec.commonfeature.util.TestsData.mockedPhotoIdOptionItemUi import eu.europa.ec.commonfeature.util.TestsData.mockedPidId import eu.europa.ec.commonfeature.util.TestsData.mockedPidOptionItemUi +import eu.europa.ec.commonfeature.util.TestsData.mockedPrimaryButtonText +import eu.europa.ec.commonfeature.util.TestsData.mockedRouteArguments import eu.europa.ec.commonfeature.util.TestsData.mockedSampleDataOptionItemUi +import eu.europa.ec.commonfeature.util.TestsData.mockedSuccessContentDescription +import eu.europa.ec.commonfeature.util.TestsData.mockedSuccessSubtitle +import eu.europa.ec.commonfeature.util.TestsData.mockedSuccessTitle +import eu.europa.ec.commonfeature.util.TestsData.mockedUriPath1 import eu.europa.ec.corelogic.controller.AddSampleDataPartialState import eu.europa.ec.corelogic.controller.IssuanceMethod import eu.europa.ec.corelogic.controller.IssueDocumentPartialState import eu.europa.ec.corelogic.controller.WalletCoreDocumentsController import eu.europa.ec.corelogic.model.DocumentIdentifier +import eu.europa.ec.resourceslogic.R import eu.europa.ec.resourceslogic.provider.ResourceProvider +import eu.europa.ec.resourceslogic.theme.values.ThemeColors import eu.europa.ec.testfeature.MockResourceProviderForStringCalls.mockDocumentTypeUiToUiNameCall import eu.europa.ec.testfeature.mockedExceptionWithMessage import eu.europa.ec.testfeature.mockedExceptionWithNoMessage @@ -43,6 +54,7 @@ import eu.europa.ec.testlogic.extension.runFlowTest import eu.europa.ec.testlogic.extension.runTest import eu.europa.ec.testlogic.extension.toFlow import eu.europa.ec.testlogic.rule.CoroutineTestRule +import eu.europa.ec.uilogic.component.AppIcons import eu.europa.ec.uilogic.serializer.UiSerializer import junit.framework.TestCase.assertEquals import org.junit.After @@ -296,7 +308,7 @@ class TestAddDocumentInteractor { // Case 1 Expected Result: // deviceAuthenticationInteractor.authenticateWithBiometrics called once. @Test - fun `Given case 1, When handleUserAuth is called, Then Case 1 expected result is returned`() { + fun `Given Case 1, When handleUserAuth is called, Then Case 1 expected result is returned`() { // Given mockBiometricsAvailabilityResponse( response = BiometricsAvailability.CanAuthenticate @@ -321,7 +333,7 @@ class TestAddDocumentInteractor { // Case 2 Expected Result: // deviceAuthenticationInteractor.authenticateWithBiometrics called once. @Test - fun `Given case 2, When handleUserAuth is called, Then Case 2 expected result is returned`() { + fun `Given Case 2, When handleUserAuth is called, Then Case 2 expected result is returned`() { // Given mockBiometricsAvailabilityResponse( response = BiometricsAvailability.NonEnrolled @@ -346,7 +358,7 @@ class TestAddDocumentInteractor { // Case 3 Expected Result: // resultHandler.onAuthenticationFailure called once. @Test - fun `Given case 3, When handleUserAuth is called, Then Case 3 expected result is returned`() { + fun `Given Case 3, When handleUserAuth is called, Then Case 3 expected result is returned`() { // Given val mockedOnAuthenticationFailure: () -> Unit = {} whenever(resultHandler.onAuthenticationFailure) @@ -371,6 +383,102 @@ class TestAddDocumentInteractor { } //endregion + //region buildGenericSuccessRouteForDeferred + + // Case 1: + // 1. ConfigNavigation with NavigationType.PushRoute + // 2. string resources mocked + @Test + fun `Given Case 1, When buildGenericSuccessRouteForDeferred is called, Then the expected string result is returned`() { + // Given + mockDocumentIssuanceStrings() + + val config = SuccessUIConfig( + headerConfig = mockedTripleObject.first, + content = resourceProvider.getString(R.string.issuance_add_document_deferred_success_subtitle), + imageConfig = mockedTripleObject.second, + buttonConfig = listOf( + SuccessUIConfig.ButtonConfig( + text = mockedTripleObject.third, + style = SuccessUIConfig.ButtonConfig.Style.PRIMARY, + navigation = mockedConfigNavigationTypePush + ) + ), + onBackScreenToNavigate = mockedConfigNavigationTypePush + ) + + whenever( + uiSerializer.toBase64( + model = config, + parser = SuccessUIConfig.Parser + ) + ).thenReturn(mockedRouteArguments) + + val flowType = IssuanceFlowUiConfig.NO_DOCUMENT + + // When + val result = interactor.buildGenericSuccessRouteForDeferred(flowType = flowType) + + // Then + val expectedResult = "SUCCESS?successConfig=$mockedRouteArguments" + assertEquals(expectedResult, result) + } + + // Case 2: + // 1. ConfigNavigation with NavigationType.PopRoute + // 2. string resources mocked + @Test + fun `When buildGenericSuccessRouteForDeferred (PopRoute) is called, then the expected string result is returned`() { + // Given + mockDocumentIssuanceStrings() + + val config = SuccessUIConfig( + headerConfig = mockedTripleObject.first, + content = resourceProvider.getString(R.string.issuance_add_document_deferred_success_subtitle), + imageConfig = mockedTripleObject.second, + buttonConfig = listOf( + SuccessUIConfig.ButtonConfig( + text = mockedTripleObject.third, + style = SuccessUIConfig.ButtonConfig.Style.PRIMARY, + navigation = mockedConfigNavigationTypePopToScreen + ) + ), + onBackScreenToNavigate = mockedConfigNavigationTypePopToScreen + ) + + whenever( + uiSerializer.toBase64( + model = config, + parser = SuccessUIConfig.Parser + ) + ).thenReturn(mockedRouteArguments) + + val flowType = IssuanceFlowUiConfig.EXTRA_DOCUMENT + + // When + val result = interactor.buildGenericSuccessRouteForDeferred(flowType = flowType) + + // Then + val expectedResult = "SUCCESS?successConfig=$mockedRouteArguments" + assertEquals(expectedResult, result) + } + //endregion + + //region resumeOpenId4VciWithAuthorization + + // Case of resumeOpenId4VciWithAuthorization being called on the interactor + // the expected result is the resumeOpenId4VciWithAuthorization function to be executed on + // the walletCoreDocumentsController + @Test + fun `When interactor resumeOpenId4VciWithAuthorization is called, Then resumeOpenId4VciWithAuthorization should be invoked on the controller`() { + // When + interactor.resumeOpenId4VciWithAuthorization(mockedUriPath1) + + verify(walletCoreDocumentsController, times(1)) + .resumeOpenId4VciWithAuthorization(mockedUriPath1) + } + //endregion + //region helper functions private fun mockBiometricsAvailabilityResponse(response: BiometricsAvailability) { whenever(deviceAuthenticationInteractor.getBiometricsAvailability(listener = any())) @@ -379,5 +487,34 @@ class TestAddDocumentInteractor { bioAvailability(response) } } + + private fun mockDocumentIssuanceStrings() { + whenever(resourceProvider.getString(R.string.issuance_add_document_deferred_success_title)) + .thenReturn(mockedSuccessTitle) + whenever(resourceProvider.getString(R.string.issuance_add_document_deferred_success_primary_button_text)) + .thenReturn(mockedPrimaryButtonText) + whenever(resourceProvider.getString(AppIcons.ClockTimer.contentDescriptionId)) + .thenReturn(mockedSuccessContentDescription) + whenever(resourceProvider.getString(R.string.issuance_add_document_deferred_success_subtitle)) + .thenReturn(mockedSuccessSubtitle) + } + //endregion + + //region mocked objects + private val mockedTripleObject by lazy { + Triple( + first = SuccessUIConfig.HeaderConfig( + title = resourceProvider.getString(R.string.issuance_add_document_deferred_success_title), + color = ThemeColors.warning + ), + second = SuccessUIConfig.ImageConfig( + type = SuccessUIConfig.ImageConfig.Type.DRAWABLE, + drawableRes = AppIcons.ClockTimer.resourceId, + tint = ThemeColors.warning, + contentDescription = resourceProvider.getString(AppIcons.ClockTimer.contentDescriptionId) + ), + third = resourceProvider.getString(R.string.issuance_add_document_deferred_success_primary_button_text) + ) + } //endregion } \ No newline at end of file diff --git a/issuance-feature/src/test/java/eu/europa/ec/issuancefeature/interactor/document/TestDocumentOfferInteractor.kt b/issuance-feature/src/test/java/eu/europa/ec/issuancefeature/interactor/document/TestDocumentOfferInteractor.kt new file mode 100644 index 000000000..e0dca4f2b --- /dev/null +++ b/issuance-feature/src/test/java/eu/europa/ec/issuancefeature/interactor/document/TestDocumentOfferInteractor.kt @@ -0,0 +1,1181 @@ +/* + * 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.issuancefeature.interactor.document + +import android.content.Context +import eu.europa.ec.authenticationlogic.controller.authentication.BiometricsAvailability +import eu.europa.ec.authenticationlogic.controller.authentication.DeviceAuthenticationResult +import eu.europa.ec.authenticationlogic.model.BiometricCrypto +import eu.europa.ec.commonfeature.config.SuccessUIConfig +import eu.europa.ec.commonfeature.interactor.DeviceAuthenticationInteractor +import eu.europa.ec.commonfeature.ui.request.model.DocumentItemUi +import eu.europa.ec.commonfeature.util.TestsData.mockedConfigNavigationTypePop +import eu.europa.ec.commonfeature.util.TestsData.mockedDocUiNamePid +import eu.europa.ec.commonfeature.util.TestsData.mockedInvalidCodeFormatMessage +import eu.europa.ec.commonfeature.util.TestsData.mockedIssuanceErrorMessage +import eu.europa.ec.commonfeature.util.TestsData.mockedIssuerName +import eu.europa.ec.commonfeature.util.TestsData.mockedOfferTxCodeSpecFourDigits +import eu.europa.ec.commonfeature.util.TestsData.mockedOfferedDocumentDocType +import eu.europa.ec.commonfeature.util.TestsData.mockedOfferedDocumentName +import eu.europa.ec.commonfeature.util.TestsData.mockedPendingMdlUi +import eu.europa.ec.commonfeature.util.TestsData.mockedPendingPidUi +import eu.europa.ec.commonfeature.util.TestsData.mockedPidId +import eu.europa.ec.commonfeature.util.TestsData.mockedPrimaryButtonText +import eu.europa.ec.commonfeature.util.TestsData.mockedRouteArguments +import eu.europa.ec.commonfeature.util.TestsData.mockedSuccessContentDescription +import eu.europa.ec.commonfeature.util.TestsData.mockedSuccessSubtitle +import eu.europa.ec.commonfeature.util.TestsData.mockedSuccessTitle +import eu.europa.ec.commonfeature.util.TestsData.mockedTxCode +import eu.europa.ec.commonfeature.util.TestsData.mockedTxCodeSpecFourDigits +import eu.europa.ec.commonfeature.util.TestsData.mockedUriPath1 +import eu.europa.ec.commonfeature.util.TestsData.mockedWalletActivationErrorMessage +import eu.europa.ec.corelogic.controller.IssueDocumentsPartialState +import eu.europa.ec.corelogic.controller.ResolveDocumentOfferPartialState +import eu.europa.ec.corelogic.controller.WalletCoreDocumentsController +import eu.europa.ec.corelogic.model.DocType +import eu.europa.ec.corelogic.model.DocumentIdentifier +import eu.europa.ec.eudi.wallet.document.DocumentId +import eu.europa.ec.eudi.wallet.document.IssuedDocument +import eu.europa.ec.eudi.wallet.issue.openid4vci.Offer +import eu.europa.ec.eudi.wallet.issue.openid4vci.Offer.TxCodeSpec +import eu.europa.ec.resourceslogic.R +import eu.europa.ec.resourceslogic.provider.ResourceProvider +import eu.europa.ec.resourceslogic.theme.values.ThemeColors +import eu.europa.ec.testfeature.mockedExceptionWithMessage +import eu.europa.ec.testfeature.mockedExceptionWithNoMessage +import eu.europa.ec.testfeature.mockedGenericErrorMessage +import eu.europa.ec.testfeature.mockedMainPid +import eu.europa.ec.testfeature.mockedPidDocName +import eu.europa.ec.testfeature.mockedPidDocType +import eu.europa.ec.testfeature.mockedPlainFailureMessage +import eu.europa.ec.testlogic.extension.runFlowTest +import eu.europa.ec.testlogic.extension.runTest +import eu.europa.ec.testlogic.extension.toFlow +import eu.europa.ec.testlogic.rule.CoroutineTestRule +import eu.europa.ec.uilogic.component.AppIcons +import eu.europa.ec.uilogic.serializer.UiSerializer +import junit.framework.TestCase.assertEquals +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class TestDocumentOfferInteractor { + + @get:Rule + val coroutineRule = CoroutineTestRule() + + @Mock + private lateinit var walletCoreDocumentsController: WalletCoreDocumentsController + + @Mock + private lateinit var deviceAuthenticationInteractor: DeviceAuthenticationInteractor + + @Mock + private lateinit var resourceProvider: ResourceProvider + + @Mock + private lateinit var uiSerializer: UiSerializer + + @Mock + private lateinit var resultHandler: DeviceAuthenticationResult + + @Mock + private lateinit var context: Context + + private lateinit var interactor: DocumentOfferInteractor + + private lateinit var closeable: AutoCloseable + + private lateinit var biometricCrypto: BiometricCrypto + + @Before + fun before() { + closeable = MockitoAnnotations.openMocks(this) + + interactor = DocumentOfferInteractorImpl( + walletCoreDocumentsController = walletCoreDocumentsController, + deviceAuthenticationInteractor = deviceAuthenticationInteractor, + resourceProvider = resourceProvider, + uiSerializer = uiSerializer + ) + biometricCrypto = BiometricCrypto(cryptoObject = null) + + whenever(resourceProvider.genericErrorMessage()).thenReturn(mockedGenericErrorMessage) + } + + @After + fun after() { + closeable.close() + } + + //region resolveDocumentOffer + + // Case 1: + // 1. walletCoreDocumentsController.resolveDocumentOffer() returns ResolveDocumentOfferPartialState.Success with: + // - empty response.offer.offeredDocuments + + // Case 1 Expected Result: + // ResolveDocumentOfferInteractorPartialState.NoDocument state with: + // - the issuer name + @Test + fun `Given Case 1, When resolveDocumentOffer is called, Then Case 1 Expected Result is returned`() = + coroutineRule.runTest { + // Given + val mockedOffer = mockOffer( + issuerName = mockedIssuerName + ) + mockGetMainPidDocumentCall( + mainPid = mockedMainPid + ) + mockWalletDocumentsControllerResolveOfferEventEmission( + event = ResolveDocumentOfferPartialState.Success( + offer = mockedOffer + ) + ) + + // When + interactor.resolveDocumentOffer(mockedUriPath1).runFlowTest { + val expectedResult = ResolveDocumentOfferInteractorPartialState.NoDocument( + issuerName = mockedOffer.issuerName + ) + // Then + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 2: + // 1. walletCoreDocumentsController.resolveDocumentOffer() returns ResolveDocumentOfferPartialState.Success with: + // - valid response.offer.txCodeSpec?.inputMode (TxCodeSpec.InputMode.NUMERIC), + // - invalid response.offer.txCodeSpec?.length (2), and + // - response.offer.offeredDocuments has only one Offer.OfferedDocument item that its docType is not supported. + + // Case 2 Expected Result: + // ResolveDocumentOfferInteractorPartialState.Failure state, with: + // - an invalid code format error message + @Test + fun `Given Case 2, When resolveDocumentOffer is called, Then Case 2 Expected Result is returned`() = + coroutineRule.runTest { + // Given + val mockedTxCodeSpecLength = 2 + val mockedOffer = mockOffer( + issuerName = mockedIssuerName, + offeredDocuments = mockedOfferedDocumentsList, + txCodeSpec = mockOfferTxCodeSpec( + inputMode = TxCodeSpec.InputMode.NUMERIC, + length = mockedTxCodeSpecLength + ) + ) + + val codeMinLength = 4 + val codeMaxLength = 6 + whenever( + resourceProvider.getString( + R.string.issuance_document_offer_error_invalid_txcode_format, + codeMinLength, + codeMaxLength + ) + ).thenReturn(mockedInvalidCodeFormatMessage) + + mockWalletDocumentsControllerResolveOfferEventEmission( + event = ResolveDocumentOfferPartialState.Success(mockedOffer) + ) + + // When + interactor.resolveDocumentOffer(mockedUriPath1).runFlowTest { + val expectedResult = ResolveDocumentOfferInteractorPartialState.Failure( + errorMessage = mockedInvalidCodeFormatMessage + ) + // Then + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 3: + // 1. walletCoreDocumentsController.resolveDocumentOffer() returns ResolveDocumentOfferPartialState.Success with: + // - invalid response.offer.txCodeSpec?.inputMode (TxCodeSpec.InputMode.TEXT), + // - valid response.offer.txCodeSpec?.length (4), and + // - response.offer.offeredDocuments has only one Offer.OfferedDocument item that its docType is not supported. + + // Case 3 Expected Result: + // ResolveDocumentOfferInteractorPartialState.Failure state, with: + // - an invalid code format error message + @Test + fun `Given Case 3, When resolveDocumentOffer is called, Then Case 3 Expected Result is returned`() = + coroutineRule.runTest { + // Given + val mockedTxCodeSpecLength = 4 + val mockedOffer = mockOffer( + issuerName = mockedIssuerName, + offeredDocuments = mockedOfferedDocumentsList, + txCodeSpec = mockOfferTxCodeSpec( + inputMode = TxCodeSpec.InputMode.TEXT, + length = mockedTxCodeSpecLength + ) + ) + + val codeMinLength = 4 + val codeMaxLength = 6 + whenever( + resourceProvider.getString( + R.string.issuance_document_offer_error_invalid_txcode_format, + codeMinLength, + codeMaxLength + ) + ).thenReturn(mockedInvalidCodeFormatMessage) + + mockWalletDocumentsControllerResolveOfferEventEmission( + event = ResolveDocumentOfferPartialState.Success(mockedOffer) + ) + + // When + interactor.resolveDocumentOffer(mockedUriPath1).runFlowTest { + val expectedResult = ResolveDocumentOfferInteractorPartialState.Failure( + errorMessage = mockedInvalidCodeFormatMessage + ) + // Then + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 4: + // 1. walletCoreDocumentsController.resolveDocumentOffer() returns ResolveDocumentOfferPartialState.Success with: + // - valid response.offer.txCodeSpec?.inputMode (TxCodeSpec.InputMode.NUMERIC), + // - valid response.offer.txCodeSpec?.length (4), and + // - response.offer.offeredDocuments has only one Offer.OfferedDocument item that its docType is not supported. + // 2. walletCoreDocumentsController.getMainPidDocument() returns not null (i.e. hasMainPid == true). + // 3. no PID in Offer (i.e hasPidInOffer == false). + + // Case 4 Expected Result: + // ResolveDocumentOfferInteractorPartialState.Success state, with: + // - DocumentUiItem list, with non-localized document names + // - issuer name + // - and txCodeLength + @Test + fun `Given Case 4, When resolveDocumentOffer is called, Then Case 4 Expected Result is returned`() = + coroutineRule.runTest { + // Given + val mockedOffer = mockOffer( + issuerName = mockedIssuerName, + offeredDocuments = mockedOfferedDocumentsList, + txCodeSpec = mockedOfferTxCodeSpecFourDigits + ) + mockGetMainPidDocumentCall( + mainPid = mockedMainPid + ) + mockWalletDocumentsControllerResolveOfferEventEmission( + event = ResolveDocumentOfferPartialState.Success(mockedOffer) + ) + + // When + interactor.resolveDocumentOffer(mockedUriPath1).runFlowTest { + val expectedList = mockedOfferedDocumentsList.map { + DocumentItemUi(title = mockedOfferedDocumentName) + } + val expectedResult = ResolveDocumentOfferInteractorPartialState.Success( + documents = expectedList, + issuerName = mockedIssuerName, + txCodeLength = mockedOfferTxCodeSpecFourDigits.length + ) + // Then + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 5: + // 1. walletCoreDocumentsController.resolveDocumentOffer() returns ResolveDocumentOfferPartialState.Success with: + // - valid response.offer.txCodeSpec?.inputMode (TxCodeSpec.InputMode.NUMERIC), + // - valid response.offer.txCodeSpec?.length (4), and + // - response.offer.offeredDocuments has only one Offer.OfferedDocument item that its docType is supported. + // 2. walletCoreDocumentsController.getMainPidDocument() returns null (i.e. hasMainPid == false). + // 3. a PID in Offer (i.e hasPidInOffer == true). + + // Case 5 Expected Result: + // ResolveDocumentOfferInteractorPartialState.Success state, with: + // - DocumentUiItem list, with localized document names + // - issuer name + // - and txCodeLength + @Test + fun `Given Case 5, When resolveDocumentOffer is called, Then Case 5 Expected Result is returned`() = + coroutineRule.runTest { + // Given + val mockedOffer = mockOffer( + issuerName = mockedIssuerName, + offeredDocuments = listOf( + mockOfferedDocument( + name = mockedOfferedDocumentName, + docType = mockedPidDocType + ) + ), + txCodeSpec = mockedOfferTxCodeSpecFourDigits + ) + mockGetMainPidDocumentCall( + mainPid = null + ) + whenever(resourceProvider.getString(R.string.pid)) + .thenReturn(mockedPidDocName) + + mockWalletDocumentsControllerResolveOfferEventEmission( + event = ResolveDocumentOfferPartialState.Success(mockedOffer) + ) + + // When + interactor.resolveDocumentOffer(mockedUriPath1).runFlowTest { + val expectedDocumentsUiList = listOf( + DocumentItemUi(mockedPidDocName) + ) + val expectedResult = ResolveDocumentOfferInteractorPartialState.Success( + documents = expectedDocumentsUiList, + issuerName = mockedIssuerName, + txCodeLength = mockedOffer.txCodeSpec?.length + ) + + // Then + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 6: + // 1. walletCoreDocumentsController.resolveDocumentOffer() returns ResolveDocumentOfferPartialState.Success with: + // - valid response.offer.txCodeSpec?.inputMode (TxCodeSpec.InputMode.NUMERIC), + // - valid response.offer.txCodeSpec?.length (4), and + // - response.offer.offeredDocuments has only one Offer.OfferedDocument item that its docType is not supported. + // 2. walletCoreDocumentsController.getMainPidDocument() returns null (i.e. hasMainPid == false). + // 3. no PID in Offer (i.e hasPidInOffer == false). + + // Case 6 Expected Result: + // ResolveDocumentOfferInteractorPartialState.Failure state, with: + // - an invalid wallet activation error message + @Test + fun `Given Case 6, When resolveDocumentOffer is called, Then Case 6 Expected Result is returned`() = + coroutineRule.runTest { + // Given + val mockedOffer = mockOffer( + issuerName = mockedIssuerName, + offeredDocuments = mockedOfferedDocumentsList, + txCodeSpec = mockedOfferTxCodeSpecFourDigits + ) + mockGetMainPidDocumentCall( + mainPid = null + ) + + whenever(resourceProvider.getString(R.string.issuance_document_offer_error_missing_pid_text)) + .thenReturn(mockedWalletActivationErrorMessage) + mockWalletDocumentsControllerResolveOfferEventEmission( + event = ResolveDocumentOfferPartialState.Success(mockedOffer) + ) + + // When + interactor.resolveDocumentOffer(mockedUriPath1).runFlowTest { + val expectedResult = ResolveDocumentOfferInteractorPartialState.Failure( + errorMessage = mockedWalletActivationErrorMessage + ) + // Then + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 7: + // 1. walletCoreDocumentsController.resolveDocumentOffer() returns + // ResolveDocumentOfferPartialState.Failure with: + // a plain failure message + + // Case 7 Expected Result: + // ResolveDocumentOfferInteractorPartialState.Failure state, with: + // - the same failure message + @Test + fun `Given Case 7, When resolveDocumentOffer is called, Then Case 7 Expected Result is returned`() = + coroutineRule.runTest { + // Given + mockWalletDocumentsControllerResolveOfferEventEmission( + event = ResolveDocumentOfferPartialState.Failure(mockedPlainFailureMessage) + ) + + // When + interactor.resolveDocumentOffer(mockedUriPath1).runFlowTest { + val expectedResult = ResolveDocumentOfferInteractorPartialState.Failure( + errorMessage = mockedPlainFailureMessage + ) + //Then + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 8: + // 1. walletCoreDocumentsController.resolveDocumentOffer() throws: + // a RuntimeException with a message + + // Case 8 Expected Result: + // ResolveDocumentOfferInteractorPartialState.Failure state, with: + // - the exception's localized message + @Test + fun `Given Case 8, When resolveDocumentOffer is called, Then Case 8 Expected Result is returned`() = + coroutineRule.runTest { + // Given + whenever(walletCoreDocumentsController.resolveDocumentOffer(mockedUriPath1)) + .thenThrow(mockedExceptionWithMessage) + + // When + interactor.resolveDocumentOffer(mockedUriPath1).runFlowTest { + // Then + val expectedResult = ResolveDocumentOfferInteractorPartialState.Failure( + errorMessage = mockedExceptionWithMessage.localizedMessage!! + ) + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 9: + // 1. walletCoreDocumentsController.resolveDocumentOffer() throws: + // a RuntimeException without message + + // Case 9 Expected Result: + // ResolveDocumentOfferInteractorPartialState.Failure state, with: + // - the generic error message + @Test + fun `Given Case 9, When resolveDocumentOffer is called, Then Case 9 Expected Result is returned`() = + coroutineRule.runTest { + // Given + whenever(walletCoreDocumentsController.resolveDocumentOffer(mockedUriPath1)) + .thenThrow(mockedExceptionWithNoMessage) + + // When + interactor.resolveDocumentOffer(mockedUriPath1).runFlowTest { + // Then + val expectedResult = ResolveDocumentOfferInteractorPartialState.Failure( + errorMessage = mockedGenericErrorMessage + ) + assertEquals(expectedResult, awaitItem()) + } + } + //endregion + + //region issueDocuments + + // Case 1: + // 1. walletCoreDocumentsController.issueDocumentsByOfferUri emits + // IssueDocumentsPartialState.Failure with: + // mockedPlainFailureMessage as the error message + + // Case 1 Expected Result: + // IssueDocumentsInteractorPartialState.Failure state, with: + // - errorMessage equal to mockedPlainFailureMessage + @Test + fun `Given Case 1, When issueDocuments is called, Then Case 1 Expected Result is returned`() = + coroutineRule.runTest { + // Given + mockWalletDocumentsControllerIssueByUriEventEmission( + event = IssueDocumentsPartialState.Failure( + errorMessage = mockedPlainFailureMessage + ) + ) + + // When + interactor.issueDocuments( + offerUri = mockedUriPath1, + issuerName = mockedIssuerName, + navigation = mockedConfigNavigationTypePop, + txCode = mockedTxCode + ).runFlowTest { + // Then + val expectedResult = IssueDocumentsInteractorPartialState.Failure( + errorMessage = mockedPlainFailureMessage + ) + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 2: + // 1. walletCoreDocumentsController.issueDocumentsByOfferUri emits + // IssueDocumentsPartialState.Failure with: + // mockedPlainFailureMessage as the error message + // 2. The controller issueDocumentsByOfferUri is called with a mocked offerUri and null txCode + + // Case 2 Expected Result: + // IssueDocumentsInteractorPartialState.Failure state, with: + // - errorMessage equal to mockedPlainFailureMessage. + @Test + fun `Given Case 2, When issueDocuments is called, Then Case 2 Expected Result is returned`() = + coroutineRule.runTest { + // Given + val failureResponse = IssueDocumentsPartialState.Failure( + errorMessage = mockedPlainFailureMessage + ) + mockWalletDocumentsControllerIssueByUriEventEmission( + event = failureResponse, + txCode = null + ) + + // When + interactor.issueDocuments( + offerUri = mockedUriPath1, + issuerName = mockedIssuerName, + navigation = mockedConfigNavigationTypePop + ).runFlowTest { + val expectedResult = IssueDocumentsInteractorPartialState.Failure( + errorMessage = mockedPlainFailureMessage + ) + // Then + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 3: + // 1. walletCoreDocumentsController.issueDocumentsByOfferUri emits + // IssueDocumentsPartialState.UserAuthRequired with: + // biometricCrypto object and resultHandler as DeviceAuthenticationResult + // 2. required arguments are mocked + + // Case 3 Expected Result: + // IssueDocumentsInteractorPartialState.UserAuthRequired state, with parameters of: + // - biometricCrypto object and resultHandler as DeviceAuthenticationResult + @Test + fun `Given Case 3, When issueDocuments is called, Then Case 3 Expected Result is returned`() = + coroutineRule.runTest { + // Given + whenever(resourceProvider.getString(R.string.issuance_generic_error)) + .thenReturn(mockedIssuanceErrorMessage) + whenever( + resourceProvider.getString( + R.string.issuance_document_offer_success_subtitle, + mockedIssuerName + ) + ).thenReturn(mockedSuccessSubtitle) + + mockWalletDocumentsControllerIssueByUriEventEmission( + event = IssueDocumentsPartialState.UserAuthRequired( + crypto = biometricCrypto, + resultHandler = resultHandler + ) + ) + + // When + interactor.issueDocuments( + offerUri = mockedUriPath1, + issuerName = mockedIssuerName, + navigation = mockedConfigNavigationTypePop, + txCode = mockedTxCode + ).runFlowTest { + // Then + val expectedResult = IssueDocumentsInteractorPartialState.UserAuthRequired( + crypto = biometricCrypto, + resultHandler = resultHandler + ) + // Then + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 4: + // 1. walletCoreDocumentsController.issueDocumentsByOfferUri emits + // IssueDocumentsPartialState.Success with: + // 1. required strings are mocked + // 2. uiSerializer.toBase64() serializes the mockedSuccessUiConfig into mockedArguments + + // Case 4 Expected Result: + // IssueDocumentsInteractorPartialState.Success state, with: + // - successRoute equal to "SUCCESS?successConfig=mockedArguments" + @Test + fun `Given Case 4, When issueDocuments is called, Then Case 4 Expected Result is returned`() = + coroutineRule.runTest { + // Given + mockWalletDocumentsControllerIssueByUriEventEmission( + event = IssueDocumentsPartialState.Success( + documentIds = listOf(mockedPidId) + ) + ) + + mockIssuanceDocumentOfferSuccessStrings() + whenever( + resourceProvider.getString( + R.string.issuance_document_offer_success_subtitle, + mockedIssuerName + ) + ).thenReturn(mockedSuccessSubtitle) + + val mockedSuccessUiConfig = SuccessUIConfig( + headerConfig = mockedTripleObject.first, + content = mockedSuccessSubtitle, + imageConfig = mockedTripleObject.second, + buttonConfig = listOf( + SuccessUIConfig.ButtonConfig( + text = mockedTripleObject.third, + style = SuccessUIConfig.ButtonConfig.Style.PRIMARY, + navigation = mockedConfigNavigationTypePop + ) + ), + onBackScreenToNavigate = mockedConfigNavigationTypePop + ) + + whenever( + uiSerializer.toBase64( + model = mockedSuccessUiConfig, + parser = SuccessUIConfig.Parser + ) + ).thenReturn(mockedRouteArguments) + + // When + interactor.issueDocuments( + offerUri = mockedUriPath1, + issuerName = mockedIssuerName, + navigation = mockedConfigNavigationTypePop, + txCode = mockedTxCode + ).runFlowTest { + val expectedResult = IssueDocumentsInteractorPartialState.Success( + successRoute = "SUCCESS?successConfig=$mockedRouteArguments" + ) + + // Then + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 5: + // 1. walletCoreDocumentsController.issueDocumentsByOfferUri emits + // IssueDocumentsPartialState.DeferredSuccess with: + // mocked deferred documents + // 2. required strings are mocked + // 3. triple object with warning tint color + // 4. uiSerializer.toBase64() serializes the mockedSuccessUiConfig into mockedRouteArguments + + // Case 5 Expected Result: + // IssueDocumentsInteractorPartialState.DeferredSuccess state, with: + // - successRoute equal to "SUCCESS?successConfig=mockedArguments" + @Test + fun `Given Case 5, When issueDocuments is called, Then Case 5 Expected Result is returned`() = + coroutineRule.runTest { + // Given + whenever( + resourceProvider.getString( + R.string.issuance_document_offer_deferred_success_subtitle, + mockedIssuerName + ) + ).thenReturn(mockedSuccessSubtitle) + + mockIssuanceDocumentOfferDeferredSuccessStrings() + mockWalletDocumentsControllerIssueByUriEventEmission( + event = IssueDocumentsPartialState.DeferredSuccess( + deferredDocuments = mockDeferredDocumentsMap() + ) + ) + + val mockedTripleObject = Triple( + first = SuccessUIConfig.HeaderConfig( + title = resourceProvider.getString(R.string.issuance_document_offer_deferred_success_title), + color = ThemeColors.warning + ), + second = SuccessUIConfig.ImageConfig( + type = SuccessUIConfig.ImageConfig.Type.DRAWABLE, + drawableRes = AppIcons.ClockTimer.resourceId, + tint = ThemeColors.warning, + contentDescription = resourceProvider.getString(AppIcons.ClockTimer.contentDescriptionId) + ), + third = resourceProvider.getString(R.string.issuance_document_offer_deferred_success_primary_button_text) + ) + + val config = SuccessUIConfig( + headerConfig = mockedTripleObject.first, + content = mockedSuccessSubtitle, + imageConfig = mockedTripleObject.second, + buttonConfig = listOf( + SuccessUIConfig.ButtonConfig( + text = mockedTripleObject.third, + style = SuccessUIConfig.ButtonConfig.Style.PRIMARY, + navigation = mockedConfigNavigationTypePop + ) + ), + onBackScreenToNavigate = mockedConfigNavigationTypePop + ) + + whenever( + uiSerializer.toBase64( + model = config, + parser = SuccessUIConfig.Parser + ) + ).thenReturn(mockedRouteArguments) + + // When + interactor.issueDocuments( + offerUri = mockedUriPath1, + issuerName = mockedIssuerName, + navigation = mockedConfigNavigationTypePop, + txCode = mockedTxCode + ).runFlowTest { + val expectedResult = IssueDocumentsInteractorPartialState.DeferredSuccess( + successRoute = "SUCCESS?successConfig=$mockedRouteArguments" + ) + + // Then + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 6: + // 1. walletCoreDocumentsController.issueDocumentsByOfferUri emits + // IssueDocumentsPartialState.PartialSuccess with: + // - successfully issued documentIds. + // - nonIssuedDocuments map containing mockDeferredPendingDocId1 to mockDeferredPendingType1 + // and mockDeferredPendingDocId2 to mockDeferredPendingType2. + // 2. nonIssuedDocsNames is formed by combining the document types of non-issued documents: + // "eu.europa.ec.eudi.pid.1, org.iso.18013.5.1.mDL" + // 3. mocked string resources + // 7. uiSerializer.toBase64() serializes the SuccessUIConfig object into mockedArguments. + + // Case 6 Expected Result: + // IssueDocumentsInteractorPartialState.Success state, with: + // - successRoute equal to "SUCCESS?successConfig=mockedArguments" + @Test + fun `Given Case 6, When issueDocuments is called, Then Case 6 Expected Result is returned`() = + coroutineRule.runTest { + // Given + val mockSuccessfullyIssuedDocId = "0000" + + val mockDeferredPendingDocId1 = mockedPendingPidUi.documentId + val mockDeferredPendingType1 = mockedPendingPidUi.documentIdentifier.docType + + val mockDeferredPendingDocId2 = mockedPendingMdlUi.documentId + val mockDeferredPendingType2 = mockedPendingMdlUi.documentIdentifier.docType + + val nonIssuedDeferredDocuments: Map = mapOf( + mockDeferredPendingDocId1 to mockDeferredPendingType1, + mockDeferredPendingDocId2 to mockDeferredPendingType2 + ) + + val nonIssuedDocsNames = + "${mockedPendingPidUi.documentIdentifier.docType}, ${mockedPendingMdlUi.documentIdentifier.docType}" + + whenever( + resourceProvider.getString( + R.string.issuance_document_offer_partial_success_subtitle, + mockedIssuerName, + nonIssuedDocsNames + ) + ).thenReturn(mockedSuccessSubtitle) + + mockWalletDocumentsControllerIssueByUriEventEmission( + event = IssueDocumentsPartialState.PartialSuccess( + documentIds = listOf(mockSuccessfullyIssuedDocId), + nonIssuedDocuments = nonIssuedDeferredDocuments + ) + ) + + mockIssuanceDocumentOfferSuccessStrings() + whenever(resourceProvider.getString(R.string.content_description_success)) + .thenReturn(mockedSuccessContentDescription) + + val config = SuccessUIConfig( + headerConfig = mockedTripleObject.first, + content = mockedSuccessSubtitle, + imageConfig = mockedTripleObject.second, + buttonConfig = listOf( + SuccessUIConfig.ButtonConfig( + text = mockedTripleObject.third, + style = SuccessUIConfig.ButtonConfig.Style.PRIMARY, + navigation = mockedConfigNavigationTypePop + ) + ), + onBackScreenToNavigate = mockedConfigNavigationTypePop + ) + + whenever( + uiSerializer.toBase64( + model = config, + parser = SuccessUIConfig.Parser + ) + ).thenReturn(mockedRouteArguments) + + interactor.issueDocuments( + offerUri = mockedUriPath1, + issuerName = mockedIssuerName, + navigation = mockedConfigNavigationTypePop, + txCode = mockedTxCode + ).runFlowTest { + val expectedResult = IssueDocumentsInteractorPartialState.Success( + successRoute = "SUCCESS?successConfig=$mockedRouteArguments" + ) + + // Then + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 7: + // 1. walletCoreDocumentsController.issueDocumentsByOfferUri emits IssueDocumentsPartialState.PartialSuccess + // 2. The interactor is called with the given offerUri, issuerName, navigation and txCode. + + // Case 7 Expected Result: + // IssueDocumentsInteractorPartialState.Success state, with: + // - successRoute equal to "SUCCESS?successConfig=mockedArguments". + @Test + fun `Given Case 7, When issueDocuments is called, Then Case 7 Expected Result is returned`() = + coroutineRule.runTest { + // Given + val mockSuccessfullyIssuedDocId = "0000" + + val mockDeferredPendingDocId1 = mockedPidDocType + val mockDeferredPendingType1 = mockedPendingPidUi.documentIdentifier.docType + val nonIssuedDeferredDocuments: Map = mapOf( + mockDeferredPendingDocId1 to mockDeferredPendingType1 + ) + + val nonIssuedDocsNames = mockedDocUiNamePid + whenever(resourceProvider.getString(R.string.pid)).thenReturn(nonIssuedDocsNames) + whenever( + resourceProvider.getString( + R.string.issuance_document_offer_partial_success_subtitle, + mockedIssuerName, + nonIssuedDocsNames + ) + ).thenReturn(mockedSuccessSubtitle) + + mockWalletDocumentsControllerIssueByUriEventEmission( + event = IssueDocumentsPartialState.PartialSuccess( + documentIds = listOf(mockSuccessfullyIssuedDocId), + nonIssuedDocuments = nonIssuedDeferredDocuments + ) + ) + + mockIssuanceDocumentOfferSuccessStrings() + whenever(resourceProvider.getString(R.string.content_description_success)) + .thenReturn(mockedSuccessContentDescription) + + whenever( + uiSerializer.toBase64( + model = mockedSuccessUiConfig, + parser = SuccessUIConfig.Parser + ) + ).thenReturn(mockedRouteArguments) + + // When + interactor.issueDocuments( + offerUri = mockedUriPath1, + issuerName = mockedIssuerName, + navigation = mockedConfigNavigationTypePop, + txCode = mockedTxCode + ).runFlowTest { + val expectedResult = IssueDocumentsInteractorPartialState.Success( + successRoute = "SUCCESS?successConfig=$mockedRouteArguments" + ) + + // Then + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 8: + // 1. walletCoreDocumentsController.issueDocumentsByOfferUri throws an exception with a message. + + // Case 8 Expected Result: + // IssueDocumentsInteractorPartialState.Failure state, with: + // - errorMessage equal to exception's localized message. + @Test + fun `Given Case 8, When issueDocuments is called, Then Case 8 Expected Result is returned`() = + coroutineRule.runTest { + // Given + whenever( + walletCoreDocumentsController.issueDocumentsByOfferUri( + offerUri = mockedUriPath1, + txCode = mockedTxCode + ) + ).thenThrow(mockedExceptionWithMessage) + + // When + interactor.issueDocuments( + offerUri = mockedUriPath1, + issuerName = mockedIssuerName, + navigation = mockedConfigNavigationTypePop, + txCode = mockedTxCode + ).runFlowTest { + val expectedResult = IssueDocumentsInteractorPartialState.Failure( + errorMessage = mockedExceptionWithMessage.localizedMessage!! + ) + // Then + assertEquals(expectedResult, awaitItem()) + } + } + + // Case 9: + // 1. walletCoreDocumentsController.issueDocumentsByOfferUri() throws an exception with no message. + + // Case 9 Expected Result: + // IssueDocumentsInteractorPartialState.Failure state, with: + // - the generic error message. + @Test + fun `Given Case 9, When issueDocuments is called, Then Case 9 Expected Result is returned`() = + coroutineRule.runTest { + // Given + whenever( + walletCoreDocumentsController.issueDocumentsByOfferUri( + offerUri = mockedUriPath1, + txCode = mockedTxCode + ) + ).thenThrow(mockedExceptionWithNoMessage) + + // When + interactor.issueDocuments( + offerUri = mockedUriPath1, + issuerName = mockedIssuerName, + navigation = mockedConfigNavigationTypePop, + txCode = mockedTxCode + ).runFlowTest { + val expectedResult = IssueDocumentsInteractorPartialState.Failure( + errorMessage = mockedGenericErrorMessage + ) + // Then + assertEquals(expectedResult, awaitItem()) + } + } + //endregion + + //region handleUserAuthentication + // + // Case 1: + // 1. deviceAuthenticationInteractor.getBiometricsAvailability returns: + // BiometricsAvailability.CanAuthenticate + + // Case 1 Expected Result: + // deviceAuthenticationInteractor.authenticateWithBiometrics called once. + @Test + fun `Given Case 1, When handleUserAuthentication is called, Then Case 1 expected result is returned`() { + // Given + mockBiometricsAvailabilityResponse( + response = BiometricsAvailability.CanAuthenticate + ) + + // When + interactor.handleUserAuthentication( + context = context, + crypto = biometricCrypto, + resultHandler = resultHandler + ) + + // Then + verify(deviceAuthenticationInteractor, times(1)) + .authenticateWithBiometrics( + context = context, + crypto = biometricCrypto, + resultHandler = resultHandler + ) + } + + // Case 2: + // 1. deviceAuthenticationInteractor.getBiometricsAvailability returns: + // BiometricsAvailability.NonEnrolled + + // Case 2 Expected Result: + // deviceAuthenticationInteractor.authenticateWithBiometrics called once. + @Test + fun `Given Case 2, When handleUserAuthentication is called, Then Case 2 expected result is returned`() { + // Given + mockBiometricsAvailabilityResponse( + response = BiometricsAvailability.NonEnrolled + ) + + // When + interactor.handleUserAuthentication( + context = context, + crypto = biometricCrypto, + resultHandler = resultHandler + ) + + // Then + verify(deviceAuthenticationInteractor, times(1)) + .authenticateWithBiometrics( + context = context, + crypto = biometricCrypto, + resultHandler = resultHandler + ) + } + + // Case 3: + // 1. deviceAuthenticationInteractor.getBiometricsAvailability returns: + // BiometricsAvailability.Failure + + // Case 3 Expected Result: + // resultHandler.onAuthenticationFailure called once. + @Test + fun `Given Case 3, When handleUserAuthentication is called, Then Case 3 expected result is returned`() { + // Given + val mockedOnAuthenticationFailure: () -> Unit = {} + whenever(resultHandler.onAuthenticationFailure) + .thenReturn(mockedOnAuthenticationFailure) + + mockBiometricsAvailabilityResponse( + response = BiometricsAvailability.Failure( + errorMessage = mockedPlainFailureMessage + ) + ) + + // When + interactor.handleUserAuthentication( + context = context, + crypto = biometricCrypto, + resultHandler = resultHandler + ) + + // Then + verify(resultHandler, times(1)) + .onAuthenticationFailure + } + + //endregion + + //region resumeOpenId4VciWithAuthorization + @Test + fun `when interactor resumeOpenId4VciWithAuthorization is called, then resumeOpenId4VciWithAuthorization should be invoked on the controller`() { + interactor.resumeOpenId4VciWithAuthorization(mockedUriPath1) + + verify(walletCoreDocumentsController, times(1)) + .resumeOpenId4VciWithAuthorization(mockedUriPath1) + } + //endregion + + //region helper functions + private fun mockGetMainPidDocumentCall(mainPid: IssuedDocument?) { + whenever(walletCoreDocumentsController.getMainPidDocument()) + .thenReturn(mainPid) + } + + private fun mockWalletDocumentsControllerResolveOfferEventEmission(event: ResolveDocumentOfferPartialState) { + whenever(walletCoreDocumentsController.resolveDocumentOffer(mockedUriPath1)) + .thenReturn(event.toFlow()) + } + + private fun mockWalletDocumentsControllerIssueByUriEventEmission( + event: IssueDocumentsPartialState, + txCode: String? = mockedTxCode + ) { + whenever( + walletCoreDocumentsController.issueDocumentsByOfferUri( + offerUri = mockedUriPath1, + txCode = txCode + ) + ).thenReturn(event.toFlow()) + } + + private fun mockBiometricsAvailabilityResponse(response: BiometricsAvailability) { + whenever(deviceAuthenticationInteractor.getBiometricsAvailability(listener = any())) + .thenAnswer { + val bioAvailability = it.getArgument<(BiometricsAvailability) -> Unit>(0) + bioAvailability(response) + } + } + + private fun mockDeferredDocumentsMap(): Map { + val mockDeferredPendingDocId1 = mockedPendingPidUi.documentId + val mockDeferredPendingType1 = mockedPendingPidUi.documentIdentifier.docType + + val mockDeferredPendingDocId2 = mockedPendingMdlUi.documentId + val mockDeferredPendingType2 = mockedPendingMdlUi.documentIdentifier.docType + + return mapOf( + mockDeferredPendingDocId1 to mockDeferredPendingType1, + mockDeferredPendingDocId2 to mockDeferredPendingType2 + ) + } + + private fun mockIssuanceDocumentOfferSuccessStrings() { + whenever(resourceProvider.getString(R.string.issuance_document_offer_success_title)) + .thenReturn(mockedSuccessTitle) + whenever(resourceProvider.getString(R.string.issuance_document_offer_success_primary_button_text)) + .thenReturn(mockedPrimaryButtonText) + } + + private fun mockIssuanceDocumentOfferDeferredSuccessStrings() { + whenever(resourceProvider.getString(R.string.issuance_document_offer_deferred_success_title)) + .thenReturn(mockedSuccessTitle) + whenever(resourceProvider.getString(R.string.issuance_document_offer_deferred_success_primary_button_text)) + .thenReturn(mockedPrimaryButtonText) + } + + private fun mockOffer( + issuerName: String, + offeredDocuments: List = listOf(), + txCodeSpec: TxCodeSpec? = mockOfferTxCodeSpec() + ): Offer { + return mock(Offer::class.java).apply { + whenever(this.issuerName).thenReturn(issuerName) + whenever(this.offeredDocuments).thenReturn(offeredDocuments) + whenever(this.txCodeSpec).thenReturn(txCodeSpec) + } + } + + private fun mockOfferedDocument( + name: String = mockedOfferedDocumentName, + docType: String = mockedOfferedDocumentDocType + ): Offer.OfferedDocument { + return mock(Offer.OfferedDocument::class.java).apply { + whenever(this.name).thenReturn(name) + whenever(this.docType).thenReturn(docType) + } + } + + private fun mockOfferTxCodeSpec( + inputMode: TxCodeSpec.InputMode = TxCodeSpec.InputMode.NUMERIC, + length: Int? = mockedTxCodeSpecFourDigits, + description: String? = null + ): TxCodeSpec { + return TxCodeSpec(inputMode, length, description) + } + //endregion + + //region mocked objects + private val mockedOfferedDocumentsList = listOf( + mockOfferedDocument(docType = DocumentIdentifier.SAMPLE.docType) + ) + + private val mockedTripleObject by lazy { + Triple( + first = SuccessUIConfig.HeaderConfig( + title = resourceProvider.getString(R.string.issuance_document_offer_success_title), + color = ThemeColors.success + ), + second = SuccessUIConfig.ImageConfig( + type = SuccessUIConfig.ImageConfig.Type.DEFAULT, + drawableRes = null, + tint = ThemeColors.success, + contentDescription = resourceProvider.getString(R.string.content_description_success) + ), + third = + resourceProvider.getString(R.string.issuance_document_offer_success_primary_button_text) + ) + } + + private val mockedSuccessUiConfig by lazy { + SuccessUIConfig( + headerConfig = mockedTripleObject.first, + content = mockedSuccessSubtitle, + imageConfig = mockedTripleObject.second, + buttonConfig = listOf( + SuccessUIConfig.ButtonConfig( + text = mockedTripleObject.third, + style = SuccessUIConfig.ButtonConfig.Style.PRIMARY, + navigation = mockedConfigNavigationTypePop + ) + ), + onBackScreenToNavigate = mockedConfigNavigationTypePop + ) + } + //endregion +} \ No newline at end of file diff --git a/resources-logic/src/main/java/eu/europa/ec/resourceslogic/theme/ThemeManager.kt b/resources-logic/src/main/java/eu/europa/ec/resourceslogic/theme/ThemeManager.kt index 889ac04cc..c9e9a811b 100644 --- a/resources-logic/src/main/java/eu/europa/ec/resourceslogic/theme/ThemeManager.kt +++ b/resources-logic/src/main/java/eu/europa/ec/resourceslogic/theme/ThemeManager.kt @@ -36,6 +36,9 @@ import eu.europa.ec.resourceslogic.theme.templates.ThemeShapesTemplate import eu.europa.ec.resourceslogic.theme.templates.ThemeShapesTemplate.Companion.toShapes import eu.europa.ec.resourceslogic.theme.templates.ThemeTypographyTemplate import eu.europa.ec.resourceslogic.theme.templates.ThemeTypographyTemplate.Companion.toTypography +import eu.europa.ec.resourceslogic.theme.values.ThemeColors +import eu.europa.ec.resourceslogic.theme.values.ThemeShapes +import eu.europa.ec.resourceslogic.theme.values.ThemeTypography class ThemeManager { /** @@ -106,9 +109,16 @@ class ThemeManager { val instance: ThemeManager get() { if (this::_instance.isInitialized.not()) { - throw RuntimeException( - "Theme manager not initialized. Initialize via ThemeManager builder first." - ) + _instance = Builder() + .withLightColors(ThemeColors.lightColors) + .withDarkColors(ThemeColors.darkColors) + .withTypography(ThemeTypography.typo) + .withShapes(ThemeShapes.shapes) + .withDimensions( + ThemeDimensTemplate( + screenPadding = 10.0 + ) + ).build() } return _instance @@ -120,7 +130,7 @@ class ThemeManager { */ fun ThemeManager.build(builder: Builder): ThemeManager { set = ThemeSet( - isInDarkMode = builder.isInDarkMode ?: false, + isInDarkMode = builder.isInDarkMode == true, lightColors = builder.lightColors.toColorScheme(), darkColors = builder.darkColors.toColorScheme(), typo = builder.typography.toTypography(), diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/preview/PreviewTheme.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/preview/PreviewTheme.kt index eef61fd59..4593f0dd4 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/preview/PreviewTheme.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/preview/PreviewTheme.kt @@ -18,25 +18,10 @@ package eu.europa.ec.uilogic.component.preview import androidx.compose.runtime.Composable import eu.europa.ec.resourceslogic.theme.ThemeManager -import eu.europa.ec.resourceslogic.theme.templates.ThemeDimensTemplate -import eu.europa.ec.resourceslogic.theme.values.ThemeColors -import eu.europa.ec.resourceslogic.theme.values.ThemeShapes -import eu.europa.ec.resourceslogic.theme.values.ThemeTypography @Composable fun PreviewTheme( content: @Composable () -> Unit ) { - ThemeManager.Builder() - .withLightColors(ThemeColors.lightColors) - .withDarkColors(ThemeColors.darkColors) - .withTypography(ThemeTypography.typo) - .withShapes(ThemeShapes.shapes) - .withDimensions( - ThemeDimensTemplate( - screenPadding = 10.0 - ) - ) - .build() ThemeManager.instance.Theme { content() } } \ No newline at end of file diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/snackbar/Snackbar.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/snackbar/Snackbar.kt index 9c58f2d62..481508ce9 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/snackbar/Snackbar.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/snackbar/Snackbar.kt @@ -30,7 +30,6 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState diff --git a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapIcon.kt b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapIcon.kt index 3a350d12d..9a02f6716 100644 --- a/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapIcon.kt +++ b/ui-logic/src/main/java/eu/europa/ec/uilogic/component/wrap/WrapIcon.kt @@ -18,7 +18,6 @@ package eu.europa.ec.uilogic.component.wrap import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape