diff --git a/.run/Run All Unit Tests.run.xml b/.run/Run All Unit Tests.run.xml new file mode 100644 index 0000000..b9d29b3 --- /dev/null +++ b/.run/Run All Unit Tests.run.xml @@ -0,0 +1,44 @@ + + + + + + + + + true + true + + + + false + true + + + \ No newline at end of file diff --git a/.run/Run Kover report.run.xml b/.run/Run Kover report.run.xml new file mode 100644 index 0000000..48063ee --- /dev/null +++ b/.run/Run Kover report.run.xml @@ -0,0 +1,41 @@ + + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index a6b46fe..09f91d6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,4 +22,5 @@ plugins { alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.maven.publish) apply false + alias(libs.plugins.kotlin.kover) apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d3abaad..096c8db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ eudiRqesCore = "0.0.3-SNAPSHOT" agp = "8.7.2" kotlin = "2.0.21" +kotlinxCoroutines = "1.8.1" coreKtx = "1.15.0" junit = "4.13.2" junitVersion = "1.2.1" @@ -20,6 +21,10 @@ koinAnnotations = "1.4.0" ksp = "2.0.21-1.0.26" gson = "2.11.0" mavenPublish = "0.27.0" +mockito = "5.12.0" +mockitoKotlin = "5.3.1" +kover = "0.7.5" +robolectric = "4.11.1" [libraries] eudi-lib-android-rqes-core = { module = "eu.europa.ec.eudi:eudi-lib-android-rqes-core", version.ref = "eudiRqesCore" } @@ -48,6 +53,10 @@ koin-test = { group = "io.insert-koin", name = "koin-android-test", version.ref koin-annotations = { group = "io.insert-koin", name = "koin-annotations", version.ref = "koinAnnotations" } koin-ksp = { group = "io.insert-koin", name = "koin-ksp-compiler", version.ref = "koinAnnotations" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } +mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } +mockito-kotlin = { group = "org.mockito.kotlin", name = "mockito-kotlin", version.ref = "mockitoKotlin" } +kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } +robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } @@ -56,4 +65,5 @@ kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "ko android-library = { id = "com.android.library", version.ref = "agp" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } -maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } \ No newline at end of file +maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } +kotlin-kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } \ No newline at end of file diff --git a/rqes-ui-sdk/build.gradle.kts b/rqes-ui-sdk/build.gradle.kts index c64d5bb..adf92f2 100644 --- a/rqes-ui-sdk/build.gradle.kts +++ b/rqes-ui-sdk/build.gradle.kts @@ -23,6 +23,7 @@ plugins { alias(libs.plugins.ksp) alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.maven.publish) + alias(libs.plugins.kotlin.kover) } val NAMESPACE: String by project @@ -103,6 +104,10 @@ dependencies { // Test Dependencies testImplementation(libs.junit) testImplementation(libs.koin.test) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.robolectric) } // Compile time check @@ -125,4 +130,28 @@ mavenPublishing { url = "${POM_SCM_URL}/actions" } } +} + +koverReport { + filters { + excludes { + packages( + "*.ksp.*", + "*.di", + "*.router", + "*.serializer", + "*.config", + "*.config.*", + "*.infrastructure.*", + "*.infrastructure", + "*.ui.component.*", + "*.ui.component", + "*.ui.container", + "*.ui.container.*", + ) + classes( + "*Screen*", + ) + } + } } \ No newline at end of file diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSelectCertificateInteractor.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSelectCertificateInteractor.kt new file mode 100644 index 0000000..adc0464 --- /dev/null +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSelectCertificateInteractor.kt @@ -0,0 +1,387 @@ +/* + * 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.domain.interactor + +import eu.europa.ec.eudi.rqes.core.RQESService +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesAuthorizeServicePartialState +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesController +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesGetCertificatesPartialState +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesGetCredentialAuthorizationUrlPartialState +import eu.europa.ec.eudi.rqesui.domain.entities.error.EudiRQESUiError +import eu.europa.ec.eudi.rqesui.domain.extension.toUri +import eu.europa.ec.eudi.rqesui.infrastructure.config.data.CertificateData +import eu.europa.ec.eudi.rqesui.infrastructure.provider.ResourceProvider +import eu.europa.ec.eudi.rqesui.util.CoroutineTestRule +import eu.europa.ec.eudi.rqesui.util.mockedAuthorizationUrl +import eu.europa.ec.eudi.rqesui.util.mockedExceptionWithMessage +import eu.europa.ec.eudi.rqesui.util.mockedGenericErrorMessage +import eu.europa.ec.eudi.rqesui.util.mockedPlainFailureMessage +import eu.europa.ec.eudi.rqesui.util.runTest +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class TestSelectCertificateInteractor { + + @get:Rule + val coroutineRule = CoroutineTestRule() + + @Mock + private lateinit var eudiController: EudiRqesController + + @Mock + private lateinit var resourceProvider: ResourceProvider + + @Mock + private lateinit var rqesServiceAuthorized: RQESService.Authorized + + @Mock + private lateinit var certificateData: CertificateData + + private lateinit var closeable: AutoCloseable + + private lateinit var interactor: SelectCertificateInteractor + + @Before + fun setUp() { + closeable = MockitoAnnotations.openMocks(this) + interactor = SelectCertificateInteractorImpl(resourceProvider, eudiController) + whenever(resourceProvider.genericErrorMessage()) + .thenReturn(mockedGenericErrorMessage) + } + + @After + fun after() { + closeable.close() + } + + //region getSelectedFile + // Case 1: + // 1. Interactor's getSelectedFile is called. + // Case 1 Expected Result: + // 1. eudiController's getSelectedFile is called exactly once. + @Test + fun `Verify that getSelectedFile on the interactor calls getSelectedFile on the eudiController`() { + // When + interactor.getSelectedFile() + + // Then + verify(eudiController, times(1)) + .getSelectedFile() + } + //endregion + + //region authorizeServiceAndFetchCertificates + // Case 1: Testing when both service authorization and fetching certificates are successful + // Case 1 Expected Result: + // 1. The interactor should return a success result that contains a list of certificates. + // 2. The returned result should be of type `SelectCertificateInteractorAuthorizeServiceAndFetchCertificatesPartialState.Success`. + // 3. The `eudiController`'s `setAuthorizedService` method should be called once with `rqesServiceAuthorized`, + @Test + fun `Given Case 1, When authorizeServiceAndFetchCertificates is called, Then Case 1 expected result is returned`() = + coroutineRule.runTest { + // Arrange + val listOfCertificates = listOf(certificateData) + mockAuthorizeServiceCall( + response = EudiRqesAuthorizeServicePartialState.Success( + authorizedService = rqesServiceAuthorized + ) + ) + mockGetAvailableCertificatesCall( + response = EudiRqesGetCertificatesPartialState.Success( + certificates = listOfCertificates + ) + ) + + // Act + val result = interactor.authorizeServiceAndFetchCertificates() + + // Assert + assertEquals( + SelectCertificateInteractorAuthorizeServiceAndFetchCertificatesPartialState.Success( + certificates = listOfCertificates + ), + result + ) + verify(eudiController, times(1)) + .setAuthorizedService(rqesServiceAuthorized) + } + + // Case 2: Testing when service authorization fails + // Case 2 Expected Result: + // 1. The interactor should return a failure result when the service authorization fails. + // 2. The returned result should be of type `SelectCertificateInteractorAuthorizeServiceAndFetchCertificatesPartialState.Failure`. + // 3. The failure should contain the mocked error from the service authorization failure. + // 4. The eudiController's `authorizeService` method should be called exactly once + @Test + fun `Given Case 2, When authorizeServiceAndFetchCertificates is called, Then Case 2 expected result is returned`() = + coroutineRule.runTest { + // Arrange + val mockError = EudiRQESUiError(message = mockedPlainFailureMessage) + mockAuthorizeServiceCall( + response = EudiRqesAuthorizeServicePartialState.Failure( + error = mockError + ) + ) + + // Act + val result = interactor.authorizeServiceAndFetchCertificates() + + // Assert + assertEquals( + SelectCertificateInteractorAuthorizeServiceAndFetchCertificatesPartialState.Failure( + error = mockError + ), + result + ) + verify(eudiController, times(1)) + .authorizeService() + } + + // Case 3: Testing when an exception is thrown during the service authorization process + // Case 3 Expected Result: + // 1. The interactor should return a failure result when the service authorization throws an exception. + // 2. The returned result should be of type `SelectCertificateInteractorAuthorizeServiceAndFetchCertificatesPartialState.Failure`. + // 3. The failure should contain the thrown exception. + // 4. The error message in the returned failure result should match the message of the thrown exception. + @Test + fun `Given Case 3, When authorizeServiceAndFetchCertificates is called, Then Case 3 expected result is returned`() = + coroutineRule.runTest { + // Arrange + whenever(eudiController.authorizeService()) + .thenThrow(mockedExceptionWithMessage) + + // Act + val result = interactor.authorizeServiceAndFetchCertificates() + + // Assert + assertTrue(result is SelectCertificateInteractorAuthorizeServiceAndFetchCertificatesPartialState.Failure) + assertEquals( + mockedExceptionWithMessage.message, + (result as SelectCertificateInteractorAuthorizeServiceAndFetchCertificatesPartialState.Failure).error.message + ) + } + + // Case 4: Testing when fetching certificates fails + // Case 4 Expected Result: + // 1. The interactor should return a failure result when fetching certificates fails. + // 2. The returned result should be of type `SelectCertificateInteractorAuthorizeServiceAndFetchCertificatesPartialState.Failure`. + // 3. The failure should contain the error from `EudiRqesGetCertificatesPartialState.Failure`. + @Test + fun `Given Case 4, When authorizeServiceAndFetchCertificates is called, Then Case 4 expected result is returned`() = + coroutineRule.runTest { + // Arrange + val mockError = EudiRQESUiError(message = mockedPlainFailureMessage) + mockAuthorizeServiceCall( + response = EudiRqesAuthorizeServicePartialState.Success( + authorizedService = rqesServiceAuthorized + ) + ) + mockGetAvailableCertificatesCall( + response = EudiRqesGetCertificatesPartialState.Failure( + error = mockError + ) + ) + + // Act + val result = interactor.authorizeServiceAndFetchCertificates() + + // Assert + assertEquals( + SelectCertificateInteractorAuthorizeServiceAndFetchCertificatesPartialState.Failure( + error = mockError + ), + result + ) + } + //endregion + + //region getCredentialAuthorizationUrl + // Case 1: Testing when getCredentialAuthorizationUrl successfully returns an authorization URL + // Case 1 Expected Result: + // 1. The interactor should call getCredentialAuthorizationUrl and return a success response containing the authorization URL. + // 2. The returned result should be of type `EudiRqesGetCredentialAuthorizationUrlPartialState.Success`. + // 3. The authorization Uri returned should match the mocked `mockedAuthorizationUrl` converted to Uri. + @Test + fun `Given Case 1, When getCredentialAuthorizationUrl is called, Then Case 1 expected result is returned`() = + coroutineRule.runTest { + // Arrange + val successResponse = + EudiRqesGetCredentialAuthorizationUrlPartialState.Success( + authorizationUrl = mockedAuthorizationUrl.toUri() + ) + + whenever(eudiController.getAuthorizedService()) + .thenReturn(rqesServiceAuthorized) + mockGetCredentialAuthorizationUrlCall(response = successResponse) + + // Act + val result = interactor.getCredentialAuthorizationUrl(certificate = certificateData) + + // Assert + assertEquals(successResponse, result) + } + + // Case 2: Testing when getCredentialAuthorizationUrl fails due to an error + // Case 2 Expected Result: + // 1. The interactor should call getCredentialAuthorizationUrl and return a failure response with a `mockedPlainFailureMessage`. + // 2. The returned result should be of type `EudiRqesGetCredentialAuthorizationUrlPartialState.Failure`. + // 3. The error in the returned result should match the `mockFailureError` passed in the failure response. + @Test + fun `Given Case 2, When getCredentialAuthorizationUrl is called, Then Case 2 expected result is returned`() = + coroutineRule.runTest { + // Arrange + val failureError = EudiRQESUiError( + message = mockedPlainFailureMessage + ) + val failureResponse = + EudiRqesGetCredentialAuthorizationUrlPartialState.Failure(error = failureError) + + whenever(eudiController.getAuthorizedService()) + .thenReturn(rqesServiceAuthorized) + mockGetCredentialAuthorizationUrlCall( + response = failureResponse + ) + + // Act + val result = interactor.getCredentialAuthorizationUrl(certificate = certificateData) + + // Assert + assertTrue(result is EudiRqesGetCredentialAuthorizationUrlPartialState.Failure) + assertEquals( + failureError, + (result as EudiRqesGetCredentialAuthorizationUrlPartialState.Failure).error + ) + } + + // Case 3: Testing when getCredentialAuthorizationUrl throws an exception + // Case 3 Expected Result: + // 1. The interactor should call getCredentialAuthorizationUrl and handle the exception thrown. + // 2. The returned result should be of type `EudiRqesGetCredentialAuthorizationUrlPartialState.Failure`. + // 3. The error message in the returned result should match the message of the thrown exception. + @Test + fun `Given Case 3, When getCredentialAuthorizationUrl is called, Then Case 3 expected result is returned`() = + coroutineRule.runTest { + // Arrange + whenever(eudiController.getAuthorizedService()) + .thenReturn(rqesServiceAuthorized) + whenever( + eudiController.getCredentialAuthorizationUrl( + authorizedService = rqesServiceAuthorized, + certificateData = certificateData + ) + ).thenThrow(mockedExceptionWithMessage) + + // Act + val result = interactor.getCredentialAuthorizationUrl(certificate = certificateData) + + // Assert + assertTrue(result is EudiRqesGetCredentialAuthorizationUrlPartialState.Failure) + assertEquals( + mockedExceptionWithMessage.message, + (result as EudiRqesGetCredentialAuthorizationUrlPartialState.Failure).error.message + ) + } + + // Case 4: + // Testing when the `getCredentialAuthorizationUrl` method is called, and the `getAuthorizedService` + // returns `null`. This simulates a case where no authorized service is available, triggering a fallback + // to a generic error message. + // Case 4 Expected Result: + // 1. The interactor should return a failure response of type `EudiRqesGetCredentialAuthorizationUrlPartialState.Failure`. + // 2. The error message in the failure response should match the `mockedGenericErrorMessage`. + @Test + fun `Given Case 4, When getCredentialAuthorizationUrl is called, Then Case 4 expected result is returned`() = + coroutineRule.runTest { + // Arrange + whenever(eudiController.getAuthorizedService()) + .thenReturn(null) + + // Act + val result = interactor.getCredentialAuthorizationUrl(certificate = certificateData) + + // Assert + assertTrue(result is EudiRqesGetCredentialAuthorizationUrlPartialState.Failure) + assertEquals( + mockedGenericErrorMessage, + (result as EudiRqesGetCredentialAuthorizationUrlPartialState.Failure).error.message + ) + } + + // Case 5 Description: + // Testing when the `getCredentialAuthorizationUrl` method is called and the `getAuthorizedService` + // returns a valid service but the `getCredentialAuthorizationUrl` method itself returns `null`. + // This simulates a case where the service does not provide a valid authorization URL. + // Case 5:Expected Result: + // 1. The interactor should return a failure response of type `EudiRqesGetCredentialAuthorizationUrlPartialState.Failure`. + // 2. The error message in the failure response should match the `mockedGenericErrorMessage`. + @Test + fun `Given Case 5, When getCredentialAuthorizationUrl is called, Then Case 5 expected result is returned`() = + coroutineRule.runTest { + // Arrange + whenever(eudiController.getAuthorizedService()) + .thenReturn(rqesServiceAuthorized) + whenever( + eudiController.getCredentialAuthorizationUrl( + authorizedService = rqesServiceAuthorized, + certificateData = certificateData + ) + ).thenReturn(null) + + // Act + val result = interactor.getCredentialAuthorizationUrl(certificate = certificateData) + + // Assert + assertTrue(result is EudiRqesGetCredentialAuthorizationUrlPartialState.Failure) + assertEquals( + mockedGenericErrorMessage, + (result as EudiRqesGetCredentialAuthorizationUrlPartialState.Failure).error.message + ) + } + //endregion getCredentialAuthorizationUrl + + //region helper functions + private suspend fun mockAuthorizeServiceCall(response: EudiRqesAuthorizeServicePartialState) { + whenever(eudiController.authorizeService()).thenReturn(response) + } + + private suspend fun mockGetAvailableCertificatesCall(response: EudiRqesGetCertificatesPartialState) { + whenever(eudiController.getAvailableCertificates(rqesServiceAuthorized)).thenReturn(response) + } + + private suspend fun mockGetCredentialAuthorizationUrlCall(response: EudiRqesGetCredentialAuthorizationUrlPartialState) { + whenever( + eudiController.getCredentialAuthorizationUrl( + authorizedService = rqesServiceAuthorized, + certificateData = certificateData + ) + ).thenReturn(response) + } + //endregion +} + diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSelectQtspInteractor.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSelectQtspInteractor.kt new file mode 100644 index 0000000..eee07e1 --- /dev/null +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSelectQtspInteractor.kt @@ -0,0 +1,117 @@ +/* + * 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.domain.interactor + +import eu.europa.ec.eudi.rqes.core.RQESService +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesController +import eu.europa.ec.eudi.rqesui.infrastructure.config.data.QtspData +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +class TestSelectQtspInteractor { + + @Mock + private lateinit var eudiController: EudiRqesController + + @Mock + private lateinit var qtspData: QtspData + + @Mock + private lateinit var rqesService: RQESService + + private lateinit var closeable: AutoCloseable + + private lateinit var interactor: SelectQtspInteractor + + @Before + fun setUp() { + closeable = MockitoAnnotations.openMocks(this) + interactor = SelectQtspInteractorImpl(eudiController) + } + + @After + fun after() { + closeable.close() + } + + //region getQtsps + // Case 1: + // 1. Interactor's getQtsps is called. + // Case 1 Expected Result: + // 1. eudiController's getQtsps is called exactly once. + @Test + fun `Verify that When getQtsps is called, Then getQtsps is executed on the eudiController`() { + // When + interactor.getQtsps() + + // Then + verify(eudiController, times(1)) + .getQtsps() + } + //endregion + + //region getSelectedFile + // Case 1: + // 1. Interactor's getSelectedFile is called. + // Case 1 Expected Result: + // 1. eudiController's getSelectedFile is called exactly once. + @Test + fun `Verify that When getSelectedFile is called, Then getSelectedFile is executed on the eudiController`() { + // When + interactor.getSelectedFile() + + // Then + verify(eudiController, times(1)) + .getSelectedFile() + } + //endregion + + //region updateQtspUserSelection + // Case 1: + // 1. Interactor's updateQtspUserSelection is called. + // Case 1 Expected Result: + // 1. eudiController's setSelectedQtsp is called exactly once. + @Test + fun `Verify that When updateQtspUserSelection is called, Then setSelectedQtsp is executed on the eudiController`() { + interactor.updateQtspUserSelection(qtspData) + + verify(eudiController, times(1)) + .setSelectedQtsp(qtspData) + } + //endregion + + //region getServiceAuthorizationUrl + // Case 1: + // 1. Interactor's getServiceAuthorizationUrl is called. + // Case 1 Expected Result: + // 1. eudiController's getServiceAuthorizationUrl is called exactly once. + @Test + fun `Verify that When getServiceAuthorizationUrl is called, Then getServiceAuthorizationUrl is executed on the eudiController`() = + runTest { + interactor.getServiceAuthorizationUrl(rqesService) + + verify(eudiController, times(1)) + .getServiceAuthorizationUrl(rqesService) + } + //endregion +} \ No newline at end of file diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSuccessInteractor.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSuccessInteractor.kt new file mode 100644 index 0000000..77b33c6 --- /dev/null +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSuccessInteractor.kt @@ -0,0 +1,432 @@ +/* + * 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.domain.interactor + +import android.content.ContentResolver +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.provider.OpenableColumns +import eu.europa.ec.eudi.rqes.core.RQESService +import eu.europa.ec.eudi.rqes.core.SignedDocuments +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesAuthorizeCredentialPartialState +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesController +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesGetSelectedFilePartialState +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesGetSelectedQtspPartialState +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesSaveSignedDocumentsPartialState +import eu.europa.ec.eudi.rqesui.domain.controller.EudiRqesSignDocumentsPartialState +import eu.europa.ec.eudi.rqesui.domain.entities.error.EudiRQESUiError +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.util.CoroutineTestRule +import eu.europa.ec.eudi.rqesui.util.mockedDocumentName +import eu.europa.ec.eudi.rqesui.util.mockedExceptionWithMessage +import eu.europa.ec.eudi.rqesui.util.mockedExceptionWithNoMessage +import eu.europa.ec.eudi.rqesui.util.mockedGenericErrorMessage +import eu.europa.ec.eudi.rqesui.util.mockedPlainFailureMessage +import eu.europa.ec.eudi.rqesui.util.runTest +import junit.framework.TestCase.assertEquals +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 kotlin.test.Test + +@RunWith(RobolectricTestRunner::class) +class TestSuccessInteractor { + + @get:Rule + val coroutineRule = CoroutineTestRule() + + private lateinit var interactor: SuccessInteractor + + @Mock + private lateinit var resourceProvider: ResourceProvider + + @Mock + private lateinit var eudiRqesController: EudiRqesController + + @Mock + private lateinit var credentialAuthorized: RQESService.CredentialAuthorized + + @Mock + private lateinit var signedDocuments: SignedDocuments + + @Mock + private lateinit var qtspData: QtspData + + @Mock + private lateinit var documentData: DocumentData + + @Mock + private lateinit var context: Context + + @Mock + private lateinit var contentResolver: ContentResolver + + @Mock + private lateinit var cursor: Cursor + + private lateinit var closeable: AutoCloseable + + private val documentFileUri = Uri.parse("content://example.provider/documents/document.pdf") + + @Before + fun setUp() { + closeable = MockitoAnnotations.openMocks(this) + interactor = SuccessInteractorImpl(resourceProvider, eudiRqesController) + whenever(resourceProvider.genericErrorMessage()) + .thenReturn(mockedGenericErrorMessage) + whenever(resourceProvider.provideContext()).thenReturn(context) + } + + @After + fun after() { + closeable.close() + } + + //region getSelectedFileAndQtsp + // Case 1: Testing when `getSelectedFileAndQtsp` successfully returns a file and Qtsp + // Case 1 Expected Result: + // 1. The interactor should call `getSelectedFileAndQtsp` and successfully retrieve both the file and the Qtsp data. + // 2. The returned result should be of type `SuccessInteractorGetSelectedFileAndQtspPartialState.Success`. + // 3. The `selectedFile` in the returned result should match the mock `documentData`. + // 4. The `selectedQtsp` in the returned result should match the mock `qtspData`. + @Test + fun `Given Case 1, When getSelectedFileAndQtsp is called, Then Case 1 expected result is returned`() { + // Arrange + mockGetSelectedFileCall(event = EudiRqesGetSelectedFilePartialState.Success(file = documentData)) + mockGetSelectedQtspCall(event = EudiRqesGetSelectedQtspPartialState.Success(qtsp = qtspData)) + + // Act + val result = interactor.getSelectedFileAndQtsp() + + // Assert + assertTrue(result is SuccessInteractorGetSelectedFileAndQtspPartialState.Success) + val success = result as SuccessInteractorGetSelectedFileAndQtspPartialState.Success + assertEquals(documentData, success.selectedFile) + assertEquals(qtspData, success.selectedQtsp) + } + + // Case 2: Testing when `getSelectedFileAndQtsp` fails to retrieve the file and Qtsp + // Case 2 Expected Result: + // 1. The interactor should call `getSelectedFileAndQtsp` and handle a failure when retrieving the file. + // 2. The returned result should be of type `Failure` from `SuccessInteractorGetSelectedFileAndQtspPartialState`. + // 3. The `error` in the returned result should match the mocked `EudiRQESUiError` with the failure message. + @Test + fun `Given Case 2, When getSelectedFileAndQtsp is called, Then Case 2 expected result is returned`() { + // Arrange + val error = EudiRQESUiError(message = mockedPlainFailureMessage) + mockGetSelectedFileCall(event = EudiRqesGetSelectedFilePartialState.Failure(error = error)) + + // Act + val result = interactor.getSelectedFileAndQtsp() + + // Assert + assertTrue(result is SuccessInteractorGetSelectedFileAndQtspPartialState.Failure) + assertEquals( + error, + (result as SuccessInteractorGetSelectedFileAndQtspPartialState.Failure).error + ) + } + + // Case 3: Testing when `getSelectedFileAndQtsp` successfully retrieves the file, + // but fails to retrieve the Qtsp. + // Case 3 Expected Result: + // 1. The interactor should successfully retrieve the file. + // 2. The interactor should fail when retrieving the Qtsp and handle the error. + // 3. The returned result should be of type Failure of `SuccessInteractorGetSelectedFileAndQtspPartialState`. + // 4. The `error` in the returned result should match the mocked `EudiRQESUiError` with the failure message. + @Test + fun `Given Case 3, When getSelectedFileAndQtsp is called, Then Case 3 expected result is returned`() { + // Arrange + val error = EudiRQESUiError(message = mockedPlainFailureMessage) + mockGetSelectedFileCall(event = EudiRqesGetSelectedFilePartialState.Success(file = documentData)) + mockGetSelectedQtspCall(event = EudiRqesGetSelectedQtspPartialState.Failure(error = error)) + + // Act + val result = interactor.getSelectedFileAndQtsp() + + // Assert + assertTrue(result is SuccessInteractorGetSelectedFileAndQtspPartialState.Failure) + assertEquals( + error, + (result as SuccessInteractorGetSelectedFileAndQtspPartialState.Failure).error + ) + } + + // Case 4: + // This test case ensures that the `getOrElse` fallback logic is executed when an exception + // is thrown during the `getSelectedFile` call + // Expected Result: + // 1. The exception is caught and processed within the `getOrElse` block. + // 2. The result should be a `Failure` with the exception's message included in the error. + @Test + fun `Given Case 4, When getSelectedFileAndQtsp is called, Then Case 4 expected result is returned`() { + // Arrange + whenever(eudiRqesController.getSelectedFile()) + .thenThrow(mockedExceptionWithMessage) + + // Act + val result = interactor.getSelectedFileAndQtsp() + + // Assert + assertTrue(result is SuccessInteractorGetSelectedFileAndQtspPartialState.Failure) + val failure = result as SuccessInteractorGetSelectedFileAndQtspPartialState.Failure + assertEquals(mockedExceptionWithMessage.message, failure.error.message) + } + + // Case 5: + // This test case ensures that the `getOrElse` fallback logic is executed when an exception + // without a message is thrown during the `getSelectedFile` call. It verifies that the + // returned `Failure` state contains the generic error message as the error. + // Expected Result: + // 1. The exception is caught and processed within the `getOrElse` block. + // 2. The result should be a `Failure` with the mocked generic error message. + @Test + fun `Given Case 5, When getSelectedFileAndQtsp is called, Then Case 5 expected result is returned`() { + // Arrange + whenever(eudiRqesController.getSelectedFile()) + .thenThrow(mockedExceptionWithNoMessage) + + // Act + val result = interactor.getSelectedFileAndQtsp() + + // Assert + assertTrue(result is SuccessInteractorGetSelectedFileAndQtspPartialState.Failure) + val failure = result as SuccessInteractorGetSelectedFileAndQtspPartialState.Failure + assertEquals(mockedGenericErrorMessage, failure.error.message) + } + //endregion + + //region signAndSaveDocument + // Case 1 Description: + // This test verifies the `signAndSaveDocument` function under a success scenario. + // The method is expected to successfully execute the following steps: + // 1. Authorize a credential. + // 2. Sign the document(s) using the authorized credential. + // 3. Save the signed document(s) to the provided location. + // Case 1 Expected Result: + // The function should return a `SuccessInteractorSignAndSaveDocumentPartialState.Success` state + // with the correct saved document data, and all operations should complete successfully. + @Test + fun `Given Case 1, When signAndSaveDocument is called, Then Case 1 expected result is returned`() = + coroutineRule.runTest { + // Arrange + val documentsUri = listOf(documentFileUri) + mockAuthorizeCredentialCall( + response = EudiRqesAuthorizeCredentialPartialState.Success( + authorizedCredential = credentialAuthorized + ) + ) + mockSignDocumentsCall( + response = EudiRqesSignDocumentsPartialState.Success( + signedDocuments = signedDocuments + ) + ) + mockSaveSignedDocumentsCall( + documentName = mockedDocumentName, + signedDocuments = signedDocuments, + event = EudiRqesSaveSignedDocumentsPartialState.Success(savedDocumentsUri = documentsUri) + ) + mockGetFileNameFromUri() + + // Act + val result = interactor.signAndSaveDocument(mockedDocumentName) + + // Assert + assertTrue(result is SuccessInteractorSignAndSaveDocumentPartialState.Success) + } + + // Case 2: Testing when `signAndSaveDocument` fails during the credential authorization step. + // Case 2 Expected Result: + // 1. The interactor should attempt to authorize the credential via `authorizeCredential`. + // 2. The authorization should fail and return an error. + // 3. The returned result from `signAndSaveDocument` should be of type `Failure` from `SuccessInteractorSignAndSaveDocumentPartialState`. + // 4. The `error` in the returned result should match the mocked error. + @Test + fun `Given Case 2, When signAndSaveDocument is called, Then Case 2 expected result is returned`() = + coroutineRule.runTest { + // Arrange + val error = EudiRQESUiError(message = mockedPlainFailureMessage) + mockAuthorizeCredentialCall( + response = EudiRqesAuthorizeCredentialPartialState.Failure( + error = error + ) + ) + + // Act + val result = interactor.signAndSaveDocument(mockedDocumentName) + + // Assert + assertTrue(result is SuccessInteractorSignAndSaveDocumentPartialState.Failure) + assertEquals( + error, + (result as SuccessInteractorSignAndSaveDocumentPartialState.Failure).error + ) + } + + // Case 3: Testing when `signAndSaveDocument` succeeds in credential authorization but fails during document signing. + // Case 3 Expected Result: + // 1. The interactor should first authorize the credential successfully via `authorizeCredential`. + // 2. After authorization, the document signing should be attempted, but it fails. + // 3. The failure should return an error. + // 4. The returned result from `signAndSaveDocument` should be of type `Failure` of `SuccessInteractorSignAndSaveDocumentPartialState`. + // 5. The error in the returned result should match the mocked error. + @Test + fun `Given Case 3, When signAndSaveDocument is called, Then Case 3 expected result is returned`() = + coroutineRule.runTest { + // Arrange + val error = EudiRQESUiError(message = mockedPlainFailureMessage) + mockAuthorizeCredentialCall( + response = EudiRqesAuthorizeCredentialPartialState.Success( + credentialAuthorized + ) + ) + mockSignDocumentsCall(response = EudiRqesSignDocumentsPartialState.Failure(error = error)) + + // Act + val result = interactor.signAndSaveDocument(mockedDocumentName) + + // Assert + assertTrue(result is SuccessInteractorSignAndSaveDocumentPartialState.Failure) + assertEquals( + error, + (result as SuccessInteractorSignAndSaveDocumentPartialState.Failure).error + ) + } + + // Case 4: Testing when `signAndSaveDocument` successfully goes ahead with an authorized credential, + // signs the document but fails to save the signed document. + // Case 4 Expected Result: + // 1. The interactor should first successfully continue with an authorized credential. + // 2. After successful authorization, the document should be signed successfully. + // 3. After signing, the interactor attempts to save the signed document, but this fails. + // 4. The failure during the save operation should return an error (`EudiRQESUiError`). + // 5. The result from `signAndSaveDocument` should be of type `Failure` of `SuccessInteractorSignAndSaveDocumentPartialState`. + // 6. The `error` in the returned result should match the mocked error. + @Test + fun `Given Case 4, When signAndSaveDocument is called, Then Case 4 expected result is returned`() = + coroutineRule.runTest { + // Arrange + val error = EudiRQESUiError(message = mockedPlainFailureMessage) + mockAuthorizeCredentialCall( + response = EudiRqesAuthorizeCredentialPartialState.Success( + authorizedCredential = credentialAuthorized + ) + ) + mockSignDocumentsCall( + response = EudiRqesSignDocumentsPartialState.Success( + signedDocuments = signedDocuments + ) + ) + mockSaveSignedDocumentsCall( + documentName = mockedDocumentName, + signedDocuments = signedDocuments, + event = EudiRqesSaveSignedDocumentsPartialState.Failure(error = error) + ) + + // Act + val result = interactor.signAndSaveDocument(mockedDocumentName) + + // Assert + assertTrue(result is SuccessInteractorSignAndSaveDocumentPartialState.Failure) + assertEquals( + error, + (result as SuccessInteractorSignAndSaveDocumentPartialState.Failure).error + ) + } + + // Case 5: + // This test verifies the `signAndSaveDocument` function under a failure scenario. + // The method is expected to handle an exception thrown by the `authorizeCredential` call gracefully. + // Case 5 Expected Result: + // The function should return a `SuccessInteractorSignAndSaveDocumentPartialState.Failure` state + // with an error message matching the mocked exception's message. + @Test + fun `Given Case 5, When signAndSaveDocument is called, Then Case 5 expected result is returned`() = + coroutineRule.runTest { + // Arrange + whenever(eudiRqesController.authorizeCredential()).thenThrow( + mockedExceptionWithMessage + ) + + // Act + val result = interactor.signAndSaveDocument(mockedDocumentName) + + // Assert + assertTrue(result is SuccessInteractorSignAndSaveDocumentPartialState.Failure) + val failureState = result as SuccessInteractorSignAndSaveDocumentPartialState.Failure + assertTrue(failureState.error.message == mockedExceptionWithMessage.message) + } + //endregion + + //region helper functions + private fun mockGetSelectedFileCall(event: EudiRqesGetSelectedFilePartialState) { + whenever(eudiRqesController.getSelectedFile()).thenReturn(event) + } + + private fun mockGetSelectedQtspCall(event: EudiRqesGetSelectedQtspPartialState) { + whenever(eudiRqesController.getSelectedQtsp()).thenReturn(event) + } + + private suspend fun mockAuthorizeCredentialCall(response: EudiRqesAuthorizeCredentialPartialState) { + whenever(eudiRqesController.authorizeCredential()).thenReturn( + response + ) + } + + private suspend fun mockSignDocumentsCall(response: EudiRqesSignDocumentsPartialState) { + whenever(eudiRqesController.signDocuments(credentialAuthorized)) + .thenReturn(response) + } + + private suspend fun mockSaveSignedDocumentsCall( + documentName: String, + signedDocuments: SignedDocuments, + event: EudiRqesSaveSignedDocumentsPartialState + ) { + whenever( + eudiRqesController.saveSignedDocuments( + originalDocumentName = documentName, + signedDocuments = signedDocuments + ) + ).thenReturn(event) + } + + private fun mockGetFileNameFromUri() { + whenever(context.contentResolver).thenReturn(contentResolver) + whenever( + contentResolver.query(documentFileUri, null, null, null, null) + ).thenReturn(cursor) + whenever(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)).thenReturn(0) + whenever(cursor.moveToFirst()).thenReturn(true) + whenever(cursor.getString(0)).thenReturn(mockedDocumentName) + } + //endregion +} + + + + + diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/presentation/extension/TestUriExtensionsKt.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/presentation/extension/TestUriExtensionsKt.kt new file mode 100644 index 0000000..b0e6169 --- /dev/null +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/presentation/extension/TestUriExtensionsKt.kt @@ -0,0 +1,80 @@ +/* + * 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.extension + +import android.content.ContentResolver +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.provider.OpenableColumns +import eu.europa.ec.eudi.rqesui.util.mockedDocumentName +import junit.framework.TestCase.assertEquals +import org.junit.After +import org.junit.Before +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.Test + +class TestUriExtensionsKt { + + @Mock + private lateinit var fileUri: Uri + + @Mock + private lateinit var context: Context + + @Mock + private lateinit var contentResolver: ContentResolver + + @Mock + private lateinit var cursor: Cursor + + private lateinit var closeable: AutoCloseable + + @Before + fun setUp() { + closeable = MockitoAnnotations.openMocks(this) + } + + @After + fun after() { + closeable.close() + } + + //region getFileName + @Test + fun `When getFileName is called, Then assert the expected file name is returned`() { + whenever(context.contentResolver).thenReturn(contentResolver) + whenever( + contentResolver.query( + fileUri, null, null, null, null + ) + ).thenReturn(cursor) + + whenever(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)).thenReturn(0) + whenever(cursor.moveToFirst()).thenReturn(true) + whenever(cursor.getString(0)).thenReturn(mockedDocumentName) + + val fileNameResult = fileUri.getFileName(context).getOrThrow() + + assertEquals(mockedDocumentName, fileNameResult) + verify(cursor).close() + } + //endregion +} \ No newline at end of file diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/ExampleUnitTest.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/util/Constants.kt similarity index 61% rename from rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/ExampleUnitTest.kt rename to rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/util/Constants.kt index 24abae7..edafae2 100644 --- a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/ExampleUnitTest.kt +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/util/Constants.kt @@ -14,19 +14,12 @@ * governing permissions and limitations under the Licence. */ -package eu.europa.ec.eudi.rqesui +package eu.europa.ec.eudi.rqesui.util -import org.junit.Assert.assertEquals -import org.junit.Test +const val mockedPlainFailureMessage = "Failure message" +const val mockedGenericErrorMessage = "resourceProvider's genericErrorMessage" +const val mockedAuthorizationUrl = "https://endpoint.com/mockedAuthorizationUrl" +const val mockedDocumentName = "Document.pdf" -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file +val mockedExceptionWithMessage = RuntimeException("Exception to test interactor.") +val mockedExceptionWithNoMessage = RuntimeException() \ No newline at end of file diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/util/CoroutineTestRule.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/util/CoroutineTestRule.kt new file mode 100644 index 0000000..3cddbf0 --- /dev/null +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/util/CoroutineTestRule.kt @@ -0,0 +1,32 @@ +/* + * 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.util + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.rules.TestWatcher + +class CoroutineTestRule( + private val testDispatcher: TestDispatcher = StandardTestDispatcher(), + val testScope: TestScope = TestScope(testDispatcher) +) : TestWatcher() + +fun CoroutineTestRule.runTest(block: suspend CoroutineScope.() -> Unit): Unit = + testScope.runTest { block() } \ No newline at end of file