From ceb7b11486370cd6c0e645e5931b9821c7323d76 Mon Sep 17 00:00:00 2001 From: Christos Kaitatzis Date: Mon, 7 Oct 2024 13:12:25 +0300 Subject: [PATCH 1/2] Unit tests provided for proximity feature module interactors Covering implementation of ProximityLoadingInteractor, modification for increased coverage in TestProximityQRInteractor and TestProximityRequestInteractor Signed-off-by: Christos Kaitatzis --- .../TestProximityLoadingInteractor.kt | 323 ++++++++++++++++++ .../interactor/TestProximityQRInteractor.kt | 24 +- .../TestProximityRequestInteractor.kt | 16 +- 3 files changed, 343 insertions(+), 20 deletions(-) create mode 100644 proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityLoadingInteractor.kt diff --git a/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityLoadingInteractor.kt b/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityLoadingInteractor.kt new file mode 100644 index 00000000..250a6892 --- /dev/null +++ b/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityLoadingInteractor.kt @@ -0,0 +1,323 @@ +/* + * 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.proximityfeature.interactor + +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.interactor.DeviceAuthenticationInteractor +import eu.europa.ec.commonfeature.util.TestsData.mockedUriPath1 +import eu.europa.ec.corelogic.controller.WalletCorePartialState +import eu.europa.ec.corelogic.controller.WalletCorePresentationController +import eu.europa.ec.testfeature.mockedPlainFailureMessage +import eu.europa.ec.testlogic.extension.expectNoEvents +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 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.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.net.URI + +class TestProximityLoadingInteractor { + + @get:Rule + val coroutineRule = CoroutineTestRule() + + @Mock + private lateinit var walletCorePresentationController: WalletCorePresentationController + + @Mock + private lateinit var deviceAuthenticationInteractor: DeviceAuthenticationInteractor + + @Mock + private lateinit var resultHandler: DeviceAuthenticationResult + + @Mock + private lateinit var context: Context + + private lateinit var interactor: ProximityLoadingInteractor + + private lateinit var crypto: BiometricCrypto + + private lateinit var closeable: AutoCloseable + + @Before + fun before() { + closeable = MockitoAnnotations.openMocks(this) + + interactor = ProximityLoadingInteractorImpl( + walletCorePresentationController = walletCorePresentationController, + deviceAuthenticationInteractor = deviceAuthenticationInteractor + ) + + crypto = BiometricCrypto(cryptoObject = null) + } + + @After + fun after() { + closeable.close() + } + + //region observeResponse + // Case 1 + // 1. walletCorePresentationController.events emits: + // TransferEventPartialState.Failure, with an error message. + + // Case 1 Expected Result: + // ProximityLoadingObserveResponsePartialState.Failure state, with the same error message. + @Test + fun `Given Case 1, When observeResponse is called, Then Case 1 Expected Result is returned`() { + coroutineRule.runTest { + // Given + mockWalletCorePresentationControllerEventEmission( + event = WalletCorePartialState.Failure( + error = mockedPlainFailureMessage + ) + ) + + // When + interactor.observeResponse() + .runFlowTest { + // Then + assertEquals( + ProximityLoadingObserveResponsePartialState.Failure( + error = mockedPlainFailureMessage + ), + awaitItem() + ) + } + } + } + + // Case 2 + // 1. walletCorePresentationController.events emits: + // WalletCorePartialState.Success + + // Case 2 Expected Result: + // PresentationLoadingObserveResponsePartialState.Success. + @Test + fun `Given Case 2, When observeResponse is called, Then Case 2 Expected Result is returned`() { + coroutineRule.runTest { + // Given + mockWalletCorePresentationControllerEventEmission( + event = WalletCorePartialState.Success + ) + + // When + interactor.observeResponse() + .runFlowTest { + val expectedResult = ProximityLoadingObserveResponsePartialState.Success + // Then + assertEquals( + expectedResult, + awaitItem() + ) + } + } + } + + // Case 3: + // 1. walletCorePresentationController.events emits an event that we do not want to react to, i.e: + // TransferEventPartialState.Redirect + + // Case 3 Expected Result is that no events are emitted + @Test + fun `Given Case 3, When observeResponse is called, Then Case 3 Expected Result is returned`() { + coroutineRule.runTest { + // Given + mockEmissionOfIntentionallyNotHandledEvent() + + // When + interactor.observeResponse() + .expectNoEvents() + } + } + + // Case 4: + // 1. walletCorePresentationController.events emits: + // WalletCorePartialState.UserAuthenticationRequired. + + // Case 4 Expected Result: + // PresentationLoadingObserveResponsePartialState.UserAuthenticationRequired. + @Test + fun `Given Case 4, When observeResponse is called, Then Case 4 Expected Result is returned`() { + coroutineRule.runTest { + mockWalletCorePresentationControllerEventEmission( + event = WalletCorePartialState.UserAuthenticationRequired( + crypto = crypto, + resultHandler = resultHandler + ) + ) + + // When + interactor.observeResponse() + .runFlowTest { + val expectedResult = + ProximityLoadingObserveResponsePartialState.UserAuthenticationRequired( + crypto = crypto, + resultHandler = resultHandler + ) + 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 = crypto, + resultHandler = resultHandler + ) + + // Then + verify(deviceAuthenticationInteractor, times(1)) + .authenticateWithBiometrics(context, crypto, 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 = crypto, + resultHandler = resultHandler + ) + + // Then + verify(deviceAuthenticationInteractor, times(1)) + .authenticateWithBiometrics(context, crypto, resultHandler) + } + + // Case 3: + // 1. deviceAuthenticationInteractor.getBiometricsAvailability returns: + // BiometricsAvailability.Failure with message + + // 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 + mockBiometricsAvailabilityResponse( + response = BiometricsAvailability.Failure( + errorMessage = mockedPlainFailureMessage + ) + ) + val mockedOnAuthenticationFailure: () -> Unit = {} + whenever(resultHandler.onAuthenticationFailure) + .thenReturn(mockedOnAuthenticationFailure) + + // When + interactor.handleUserAuthentication( + context = context, + crypto = crypto, + resultHandler = resultHandler + ) + + // Then + verify(resultHandler, times(1)) + .onAuthenticationFailure + } + + //endregion + + //region stopPresentation + + @Test + fun `when interactor stopPresentation is called then it delegates to walletCoreInteractor stopPresentation`() { + // When + interactor.stopPresentation() + + // Then + verify(walletCorePresentationController, times(1)) + .stopPresentation() + } + + //endregion + + //region verifierName + + @Test + fun `when interactor verifierName is called, then verifierName should be invoked on the controller`() { + // When + interactor.verifierName + + // Then + verify(walletCorePresentationController, times(1)) + .verifierName + } + + //endregion + + //region helper functions + private fun mockWalletCorePresentationControllerEventEmission(event: WalletCorePartialState) { + whenever(walletCorePresentationController.observeSentDocumentsRequest()) + .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 mockEmissionOfIntentionallyNotHandledEvent() { + whenever(walletCorePresentationController.observeSentDocumentsRequest()).thenReturn( + WalletCorePartialState.Redirect(uri = URI(mockedUriPath1)).toFlow() + ) + } + //endregion +} \ No newline at end of file diff --git a/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityQRInteractor.kt b/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityQRInteractor.kt index d896c25a..5262994c 100644 --- a/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityQRInteractor.kt +++ b/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityQRInteractor.kt @@ -37,7 +37,7 @@ import eu.europa.ec.testlogic.rule.CoroutineTestRule import eu.europa.ec.uilogic.container.EudiComponentActivity import eu.europa.ec.uilogic.navigation.DashboardScreens import junit.framework.TestCase.assertEquals -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import org.junit.After import org.junit.Before import org.junit.Rule @@ -398,18 +398,18 @@ class TestProximityQRInteractor { private fun mockEmissionOfIntentionallyNotHandledEvents() { whenever(walletCorePresentationController.events) .thenReturn( - flow { - emit(TransferEventPartialState.Connecting) - emit( - TransferEventPartialState.RequestReceived( - requestData = emptyList(), - verifierName = null, - verifierIsTrusted = false - ) + flowOf( + TransferEventPartialState.Connecting, + TransferEventPartialState.RequestReceived( + requestData = emptyList(), + verifierName = null, + verifierIsTrusted = false + ), + TransferEventPartialState.ResponseSent, + TransferEventPartialState.Redirect( + uri = URI("") ) - emit(TransferEventPartialState.ResponseSent) - emit(TransferEventPartialState.Redirect(uri = URI(""))) - } + ) ) } diff --git a/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityRequestInteractor.kt b/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityRequestInteractor.kt index 445e0908..6fc381ef 100644 --- a/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityRequestInteractor.kt +++ b/proximity-feature/src/test/java/eu/europa/ec/proximityfeature/interactor/TestProximityRequestInteractor.kt @@ -53,7 +53,7 @@ import eu.europa.ec.testlogic.extension.toFlow import eu.europa.ec.testlogic.rule.CoroutineTestRule import eu.europa.ec.uilogic.navigation.DashboardScreens import junit.framework.TestCase.assertEquals -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import org.junit.After import org.junit.Before import org.junit.Rule @@ -696,13 +696,13 @@ class TestProximityRequestInteractor { private fun mockEmissionOfIntentionallyNotHandledEvents() { whenever(walletCorePresentationController.events) .thenReturn( - flow { - emit(TransferEventPartialState.Connected) - emit(TransferEventPartialState.Connecting) - emit(TransferEventPartialState.QrEngagementReady("")) - emit(TransferEventPartialState.Redirect(uri = URI(""))) - emit(TransferEventPartialState.ResponseSent) - } + flowOf( + TransferEventPartialState.Connected, + TransferEventPartialState.Connecting, + TransferEventPartialState.QrEngagementReady(""), + TransferEventPartialState.Redirect(uri = URI("")), + TransferEventPartialState.ResponseSent, + ) ) } From 8f3753c2a03959f022cdf3b7a71f5a04bfa17b87 Mon Sep 17 00:00:00 2001 From: Giannis Stamatopoulos Date: Thu, 10 Oct 2024 18:03:31 +0300 Subject: [PATCH 2/2] Review corrections --- .../TestPresentationLoadingInteractor.kt | 8 ++++++-- .../TestPresentationRequestInteractor.kt | 16 ++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/presentation-feature/src/test/java/eu/europa/ec/presentationfeature/interactor/TestPresentationLoadingInteractor.kt b/presentation-feature/src/test/java/eu/europa/ec/presentationfeature/interactor/TestPresentationLoadingInteractor.kt index fa229dbc..0b1fb597 100644 --- a/presentation-feature/src/test/java/eu/europa/ec/presentationfeature/interactor/TestPresentationLoadingInteractor.kt +++ b/presentation-feature/src/test/java/eu/europa/ec/presentationfeature/interactor/TestPresentationLoadingInteractor.kt @@ -257,7 +257,7 @@ class TestPresentationLoadingInteractor { // Case 3: // 1. deviceAuthenticationInteractor.getBiometricsAvailability returns: - // BiometricsAvailability.Failed + // BiometricsAvailability.Failure // Case 3 Expected Result: // resultHandler.onAuthenticationFailure called once. @@ -291,8 +291,12 @@ class TestPresentationLoadingInteractor { @Test fun `when interactor stopPresentation is called then it delegates to walletCoreInteractor stopPresentation`() { + // When interactor.stopPresentation() - verify(walletCorePresentationController, times(1)).stopPresentation() + + // Then + verify(walletCorePresentationController, times(1)) + .stopPresentation() } //endregion diff --git a/presentation-feature/src/test/java/eu/europa/ec/presentationfeature/interactor/TestPresentationRequestInteractor.kt b/presentation-feature/src/test/java/eu/europa/ec/presentationfeature/interactor/TestPresentationRequestInteractor.kt index 7c716323..c2465067 100644 --- a/presentation-feature/src/test/java/eu/europa/ec/presentationfeature/interactor/TestPresentationRequestInteractor.kt +++ b/presentation-feature/src/test/java/eu/europa/ec/presentationfeature/interactor/TestPresentationRequestInteractor.kt @@ -48,7 +48,7 @@ import eu.europa.ec.testlogic.extension.runTest import eu.europa.ec.testlogic.extension.toFlow import eu.europa.ec.testlogic.rule.CoroutineTestRule import junit.framework.TestCase.assertEquals -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import org.junit.After import org.junit.Before import org.junit.Rule @@ -420,13 +420,13 @@ class TestPresentationRequestInteractor { private fun mockEmissionOfIntentionallyNotHandledEvents() { whenever(walletCorePresentationController.events) .thenReturn( - flow { - emit(TransferEventPartialState.Connected) - emit(TransferEventPartialState.Connecting) - emit(TransferEventPartialState.QrEngagementReady("")) - emit(TransferEventPartialState.Redirect(uri = URI(""))) - emit(TransferEventPartialState.ResponseSent) - } + flowOf( + TransferEventPartialState.Connected, + TransferEventPartialState.Connecting, + TransferEventPartialState.QrEngagementReady(""), + TransferEventPartialState.Redirect(uri = URI("")), + TransferEventPartialState.ResponseSent, + ) ) }