From ebee5a4f8d6075270856ab71637e2f2343f46ae9 Mon Sep 17 00:00:00 2001 From: Christos Kaitatzis Date: Wed, 20 Nov 2024 13:08:02 +0200 Subject: [PATCH 1/9] Providing unit tests for SelectQTSPInteractor - Added Mockito and Kover dependencies - Covering interactor cases, commenting given - Set Gradle Kover configuration to generate html test report Signed-off-by: Christos Kaitatzis --- .run/Run Kover report.run.xml | 41 ++++++ build.gradle.kts | 1 + gradle/libs.versions.toml | 10 +- rqes-ui-sdk/build.gradle.kts | 4 + .../europa/ec/eudi/rqesui/ExampleUnitTest.kt | 32 ----- .../interactor/TestSelectQtspInteractor.kt | 117 ++++++++++++++++++ 6 files changed, 172 insertions(+), 33 deletions(-) create mode 100644 .run/Run Kover report.run.xml delete mode 100644 rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/ExampleUnitTest.kt create mode 100644 rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSelectQtspInteractor.kt 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..fd6ef40 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,9 @@ 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" [libraries] eudi-lib-android-rqes-core = { module = "eu.europa.ec.eudi:eudi-lib-android-rqes-core", version.ref = "eudiRqesCore" } @@ -48,6 +52,9 @@ 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" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } @@ -56,4 +63,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..8d9edbf 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,9 @@ dependencies { // Test Dependencies testImplementation(libs.junit) testImplementation(libs.koin.test) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.kotlinx.coroutines.test) } // Compile time check 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/ExampleUnitTest.kt deleted file mode 100644 index 24abae7..0000000 --- a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/ExampleUnitTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2023 European Commission - * - * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European - * Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work - * except in compliance with the Licence. - * - * You may obtain a copy of the Licence at: - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF - * ANY KIND, either express or implied. See the Licence for the specific language - * governing permissions and limitations under the Licence. - */ - -package eu.europa.ec.eudi.rqesui - -import org.junit.Assert.assertEquals -import org.junit.Test - -/** - * 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 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 From d93f47e702aac5393069b01e61383e1c34bf962d Mon Sep 17 00:00:00 2001 From: Christos Kaitatzis Date: Wed, 20 Nov 2024 14:35:38 +0200 Subject: [PATCH 2/9] Created unit tests for SelectCertificateInteractor - Added Robolectric dependency, set test to utilize it - Covering interactor cases, provided commenting Signed-off-by: Christos Kaitatzis --- gradle/libs.versions.toml | 2 + rqes-ui-sdk/build.gradle.kts | 1 + .../eu/europa/ec/eudi/rqesui/Constants.kt | 22 ++ .../ec/eudi/rqesui/base/TestApplication.kt | 21 ++ .../TestSelectCertificateInteractor.kt | 299 ++++++++++++++++++ .../rqesui/test_rule/CoroutineTestRule.kt | 32 ++ 6 files changed, 377 insertions(+) create mode 100644 rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt create mode 100644 rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/base/TestApplication.kt create mode 100644 rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSelectCertificateInteractor.kt create mode 100644 rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/test_rule/CoroutineTestRule.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fd6ef40..096c8db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ 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" } @@ -55,6 +56,7 @@ 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" } diff --git a/rqes-ui-sdk/build.gradle.kts b/rqes-ui-sdk/build.gradle.kts index 8d9edbf..0b9e826 100644 --- a/rqes-ui-sdk/build.gradle.kts +++ b/rqes-ui-sdk/build.gradle.kts @@ -107,6 +107,7 @@ dependencies { testImplementation(libs.mockito.core) testImplementation(libs.mockito.kotlin) testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.robolectric) } // Compile time check diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt new file mode 100644 index 0000000..78ac5ab --- /dev/null +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt @@ -0,0 +1,22 @@ +/* + * 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 + +const val mockedPlainFailureMessage = "failure message" +const val mockedAuthorizationUrl = "https://endpoint.com/mockedAuthorizationUrl" + +val mockedExceptionWithMessage = RuntimeException("Exception to test interactor.") \ No newline at end of file diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/base/TestApplication.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/base/TestApplication.kt new file mode 100644 index 0000000..ad184d2 --- /dev/null +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/base/TestApplication.kt @@ -0,0 +1,21 @@ +/* + * 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.base + +import android.app.Application + +class TestApplication : Application() \ 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..647cdc4 --- /dev/null +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSelectCertificateInteractor.kt @@ -0,0 +1,299 @@ +/* + * 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.base.TestApplication +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.mockedAuthorizationUrl +import eu.europa.ec.eudi.rqesui.mockedExceptionWithMessage +import eu.europa.ec.eudi.rqesui.mockedPlainFailureMessage +import eu.europa.ec.eudi.rqesui.test_rule.CoroutineTestRule +import eu.europa.ec.eudi.rqesui.test_rule.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 +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(application = TestApplication::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) + } + + @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() + } + + //region authorizeServiceAndFetchCertificates + // 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 + ) + } + //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(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(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 (`mockedExceptionWithMessage`). + @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(certificateData) + + // Assert + assertTrue(result is EudiRqesGetCredentialAuthorizationUrlPartialState.Failure) + assertEquals( + mockedExceptionWithMessage.message, + (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/test_rule/CoroutineTestRule.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/test_rule/CoroutineTestRule.kt new file mode 100644 index 0000000..c1e7ecc --- /dev/null +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/test_rule/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.test_rule + +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 From f504ba637c88346ca5c675db635b420f40d962c8 Mon Sep 17 00:00:00 2001 From: Christos Kaitatzis Date: Thu, 21 Nov 2024 15:17:13 +0200 Subject: [PATCH 3/9] Unit tests for SuccessInteractor, also equivalent test for getFileName function Signed-off-by: Christos Kaitatzis --- .../eu/europa/ec/eudi/rqesui/Constants.kt | 4 +- .../TestSelectCertificateInteractor.kt | 7 +- .../interactor/TestSuccessInteractor.kt | 367 ++++++++++++++++++ .../extension/TestUriExtensionsKt.kt | 86 ++++ 4 files changed, 459 insertions(+), 5 deletions(-) create mode 100644 rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSuccessInteractor.kt create mode 100644 rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/presentation/extension/TestUriExtensionsKt.kt diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt index 78ac5ab..5404955 100644 --- a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt @@ -16,7 +16,9 @@ package eu.europa.ec.eudi.rqesui -const val mockedPlainFailureMessage = "failure message" +const val mockedPlainFailureMessage = "Failure message" const val mockedAuthorizationUrl = "https://endpoint.com/mockedAuthorizationUrl" +const val mockedDocumentName = "Document.pdf" +const val mockedGenericErrorMessage = "An error occurred" val mockedExceptionWithMessage = RuntimeException("Exception to test interactor.") \ 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 index 647cdc4..7947774 100644 --- 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 @@ -163,7 +163,6 @@ class TestSelectCertificateInteractor { .authorizeService() } - //region authorizeServiceAndFetchCertificates // 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. @@ -209,7 +208,7 @@ class TestSelectCertificateInteractor { mockGetCredentialAuthorizationUrlCall(response = successResponse) // Act - val result = interactor.getCredentialAuthorizationUrl(certificateData) + val result = interactor.getCredentialAuthorizationUrl(certificate = certificateData) // Assert assertEquals(successResponse, result) @@ -237,7 +236,7 @@ class TestSelectCertificateInteractor { ) // Act - val result = interactor.getCredentialAuthorizationUrl(certificateData) + val result = interactor.getCredentialAuthorizationUrl(certificate = certificateData) // Assert assertTrue(result is EudiRqesGetCredentialAuthorizationUrlPartialState.Failure) @@ -266,7 +265,7 @@ class TestSelectCertificateInteractor { ).thenThrow(mockedExceptionWithMessage) // Act - val result = interactor.getCredentialAuthorizationUrl(certificateData) + val result = interactor.getCredentialAuthorizationUrl(certificate = certificateData) // Assert assertTrue(result is EudiRqesGetCredentialAuthorizationUrlPartialState.Failure) 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..e5911bb --- /dev/null +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSuccessInteractor.kt @@ -0,0 +1,367 @@ +/* + * 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.base.TestApplication +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.mockedDocumentName +import eu.europa.ec.eudi.rqesui.mockedGenericErrorMessage +import eu.europa.ec.eudi.rqesui.mockedPlainFailureMessage +import eu.europa.ec.eudi.rqesui.presentation.extension.getFileName +import eu.europa.ec.eudi.rqesui.test_rule.CoroutineTestRule +import eu.europa.ec.eudi.rqesui.test_rule.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 org.robolectric.annotation.Config +import kotlin.test.Test + +@RunWith(RobolectricTestRunner::class) +@Config(application = TestApplication::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) + } + + @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 + ) + } + //endregion + + //region signAndSaveDocument + // Case 1: Testing when `signAndSaveDocument` fails during the credential authorization step. + // Case 1 Expected Result: + // 1. The interactor should attempt to authorize the credential via `authorizeCredential`. + // 2. The authorization should fail and return an error (`EudiRQESUiError`). + // 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 (`mockedPlainFailureMessage`). + @Test + fun `Given Case 1, When signAndSaveDocument is called, Then Case 1 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 2: Testing when `signAndSaveDocument` succeeds in credential authorization but fails during document signing. + // Case 2 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 (`EudiRQESUiError`). + // 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 2, When signAndSaveDocument is called, Then Case 2 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 3: Testing when `signAndSaveDocument` successfully goes ahead with an authorized credential, signs the document, + // but fails to save the signed document. + // Case 3 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 3, When signAndSaveDocument is called, Then Case 3 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 4: Testing when `signAndSaveDocument` successfully goes ahead with an authorized credential, + // signs the document, and successfully saves the signed document. + // Case 4 Expected Result: + // 1. The interactor should successfully authorize the credential. + // 2. After successful authorization, the document should be signed successfully. + // 3. After signing, the interactor should attempt to save the signed document. + // 4. The save operation should succeed, returning the Uri of the saved document. + // 5. The `signAndSaveDocument` function should return a successful result. + // 6. The file name from the saved document's URI should be retrieved and should match the original document name. + @Test + fun `Given Case 4, When signAndSaveDocument is called, Then Case 4 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) + ) + + // Act + val result = interactor.signAndSaveDocument(mockedDocumentName) + + // Assert + val fileNameResult = mockGetFileNameFromUri() + assertEquals(mockedDocumentName, fileNameResult) + assertTrue(result is SuccessInteractorSignAndSaveDocumentPartialState.Failure) + } + //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(): String { + 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) + return documentFileUri.getFileName(context).getOrThrow() + } + //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..04ee707 --- /dev/null +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/presentation/extension/TestUriExtensionsKt.kt @@ -0,0 +1,86 @@ +/* + * 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.base.TestApplication +import eu.europa.ec.eudi.rqesui.mockedDocumentName +import junit.framework.TestCase.assertEquals +import org.junit.After +import org.junit.Before +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.test.Test + +@RunWith(RobolectricTestRunner::class) +@Config(application = TestApplication::class) +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 From 3bb66dbc201b03883e47502f25b67a192fa1aa70 Mon Sep 17 00:00:00 2001 From: Christos Kaitatzis Date: Thu, 21 Nov 2024 15:26:38 +0200 Subject: [PATCH 4/9] Removed robolectric configuration from TestUriExtensionsKt file Signed-off-by: Christos Kaitatzis --- .../rqesui/presentation/extension/TestUriExtensionsKt.kt | 6 ------ 1 file changed, 6 deletions(-) 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 index 04ee707..837013c 100644 --- 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 @@ -21,22 +21,16 @@ import android.content.Context import android.database.Cursor import android.net.Uri import android.provider.OpenableColumns -import eu.europa.ec.eudi.rqesui.base.TestApplication import eu.europa.ec.eudi.rqesui.mockedDocumentName import junit.framework.TestCase.assertEquals import org.junit.After import org.junit.Before -import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config import kotlin.test.Test -@RunWith(RobolectricTestRunner::class) -@Config(application = TestApplication::class) class TestUriExtensionsKt { @Mock From 26e44bdacbbf719aaf0a623a72fc79f0ac90548e Mon Sep 17 00:00:00 2001 From: Christos Kaitatzis Date: Fri, 22 Nov 2024 10:53:02 +0200 Subject: [PATCH 5/9] Deleted TestApplication and unused mocked string, corrections in comments Signed-off-by: Christos Kaitatzis --- .../eu/europa/ec/eudi/rqesui/Constants.kt | 1 - .../ec/eudi/rqesui/base/TestApplication.kt | 21 ------------------- .../TestSelectCertificateInteractor.kt | 3 --- .../interactor/TestSuccessInteractor.kt | 11 +++------- 4 files changed, 3 insertions(+), 33 deletions(-) delete mode 100644 rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/base/TestApplication.kt diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt index 5404955..da60140 100644 --- a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt @@ -19,6 +19,5 @@ package eu.europa.ec.eudi.rqesui const val mockedPlainFailureMessage = "Failure message" const val mockedAuthorizationUrl = "https://endpoint.com/mockedAuthorizationUrl" const val mockedDocumentName = "Document.pdf" -const val mockedGenericErrorMessage = "An error occurred" val mockedExceptionWithMessage = RuntimeException("Exception to test interactor.") \ No newline at end of file diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/base/TestApplication.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/base/TestApplication.kt deleted file mode 100644 index ad184d2..0000000 --- a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/base/TestApplication.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2023 European Commission - * - * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European - * Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work - * except in compliance with the Licence. - * - * You may obtain a copy of the Licence at: - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF - * ANY KIND, either express or implied. See the Licence for the specific language - * governing permissions and limitations under the Licence. - */ - -package eu.europa.ec.eudi.rqesui.base - -import android.app.Application - -class TestApplication : Application() \ 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 index 7947774..ec1254f 100644 --- 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 @@ -17,7 +17,6 @@ package eu.europa.ec.eudi.rqesui.domain.interactor import eu.europa.ec.eudi.rqes.core.RQESService -import eu.europa.ec.eudi.rqesui.base.TestApplication 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 @@ -44,10 +43,8 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) -@Config(application = TestApplication::class) class TestSelectCertificateInteractor { @get:Rule 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 index e5911bb..330c5c8 100644 --- 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 @@ -23,7 +23,6 @@ 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.base.TestApplication 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 @@ -35,7 +34,6 @@ 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.mockedDocumentName -import eu.europa.ec.eudi.rqesui.mockedGenericErrorMessage import eu.europa.ec.eudi.rqesui.mockedPlainFailureMessage import eu.europa.ec.eudi.rqesui.presentation.extension.getFileName import eu.europa.ec.eudi.rqesui.test_rule.CoroutineTestRule @@ -50,11 +48,9 @@ import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config import kotlin.test.Test @RunWith(RobolectricTestRunner::class) -@Config(application = TestApplication::class) class TestSuccessInteractor { @get:Rule @@ -97,7 +93,6 @@ class TestSuccessInteractor { fun setUp() { closeable = MockitoAnnotations.openMocks(this) interactor = SuccessInteractorImpl(resourceProvider, eudiRqesController) - whenever(resourceProvider.genericErrorMessage()).thenReturn(mockedGenericErrorMessage) } @After @@ -235,8 +230,8 @@ class TestSuccessInteractor { ) } - // Case 3: Testing when `signAndSaveDocument` successfully goes ahead with an authorized credential, signs the document, - // but fails to save the signed document. + // Case 3: Testing when `signAndSaveDocument` successfully goes ahead with an authorized credential, + // signs the document, but fails to save the signed document. // Case 3 Expected Result: // 1. The interactor should first successfully continue with an authorized credential. // 2. After successful authorization, the document should be signed successfully. @@ -277,7 +272,7 @@ class TestSuccessInteractor { } // Case 4: Testing when `signAndSaveDocument` successfully goes ahead with an authorized credential, - // signs the document, and successfully saves the signed document. + // signs the document and successfully saves the signed document. // Case 4 Expected Result: // 1. The interactor should successfully authorize the credential. // 2. After successful authorization, the document should be signed successfully. From e8f75eea80277e6c8ee76363c97d9d0a207b025c Mon Sep 17 00:00:00 2001 From: Christos Kaitatzis Date: Fri, 22 Nov 2024 12:30:39 +0200 Subject: [PATCH 6/9] Additional test cases given for increased coverage Signed-off-by: Christos Kaitatzis --- .../eu/europa/ec/eudi/rqesui/Constants.kt | 4 +- .../interactor/TestSuccessInteractor.kt | 48 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt index da60140..152ce79 100644 --- a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt @@ -17,7 +17,9 @@ package eu.europa.ec.eudi.rqesui const val mockedPlainFailureMessage = "Failure message" +const val mockedGenericErrorMessage = "resourceProvider's genericErrorMessage" const val mockedAuthorizationUrl = "https://endpoint.com/mockedAuthorizationUrl" const val mockedDocumentName = "Document.pdf" -val mockedExceptionWithMessage = RuntimeException("Exception to test interactor.") \ 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/domain/interactor/TestSuccessInteractor.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/domain/interactor/TestSuccessInteractor.kt index 330c5c8..0edbd68 100644 --- 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 @@ -34,6 +34,9 @@ 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.mockedDocumentName +import eu.europa.ec.eudi.rqesui.mockedExceptionWithMessage +import eu.europa.ec.eudi.rqesui.mockedExceptionWithNoMessage +import eu.europa.ec.eudi.rqesui.mockedGenericErrorMessage import eu.europa.ec.eudi.rqesui.mockedPlainFailureMessage import eu.europa.ec.eudi.rqesui.presentation.extension.getFileName import eu.europa.ec.eudi.rqesui.test_rule.CoroutineTestRule @@ -93,6 +96,8 @@ class TestSuccessInteractor { fun setUp() { closeable = MockitoAnnotations.openMocks(this) interactor = SuccessInteractorImpl(resourceProvider, eudiRqesController) + whenever(resourceProvider.genericErrorMessage()) + .thenReturn(mockedGenericErrorMessage) } @After @@ -169,6 +174,49 @@ class TestSuccessInteractor { (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 From b3ac3269b3e0ad36343c1cb14c7f37405c87eb60 Mon Sep 17 00:00:00 2001 From: Stilianos Tzouvaras Date: Fri, 22 Nov 2024 12:32:13 +0200 Subject: [PATCH 7/9] Added Kover exception rules, added run configuration for unit tests, clean up --- .run/Run All Unit Tests.run.xml | 44 +++++++++++++++++++ rqes-ui-sdk/build.gradle.kts | 24 ++++++++++ .../TestSelectCertificateInteractor.kt | 10 ++--- .../interactor/TestSuccessInteractor.kt | 8 ++-- .../extension/TestUriExtensionsKt.kt | 2 +- .../ec/eudi/rqesui/{ => util}/Constants.kt | 2 +- .../{test_rule => util}/CoroutineTestRule.kt | 2 +- 7 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 .run/Run All Unit Tests.run.xml rename rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/{ => util}/Constants.kt (96%) rename rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/{test_rule => util}/CoroutineTestRule.kt (96%) 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/rqes-ui-sdk/build.gradle.kts b/rqes-ui-sdk/build.gradle.kts index 0b9e826..adf92f2 100644 --- a/rqes-ui-sdk/build.gradle.kts +++ b/rqes-ui-sdk/build.gradle.kts @@ -130,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 index ec1254f..d478974 100644 --- 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 @@ -25,11 +25,11 @@ 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.mockedAuthorizationUrl -import eu.europa.ec.eudi.rqesui.mockedExceptionWithMessage -import eu.europa.ec.eudi.rqesui.mockedPlainFailureMessage -import eu.europa.ec.eudi.rqesui.test_rule.CoroutineTestRule -import eu.europa.ec.eudi.rqesui.test_rule.runTest +import eu.europa.ec.eudi.rqesui.util.mockedAuthorizationUrl +import eu.europa.ec.eudi.rqesui.util.mockedExceptionWithMessage +import eu.europa.ec.eudi.rqesui.util.mockedPlainFailureMessage +import eu.europa.ec.eudi.rqesui.util.CoroutineTestRule +import eu.europa.ec.eudi.rqesui.util.runTest import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertTrue import org.junit.After 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 index 330c5c8..c6837ad 100644 --- 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 @@ -33,11 +33,11 @@ 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.mockedDocumentName -import eu.europa.ec.eudi.rqesui.mockedPlainFailureMessage +import eu.europa.ec.eudi.rqesui.util.mockedDocumentName +import eu.europa.ec.eudi.rqesui.util.mockedPlainFailureMessage import eu.europa.ec.eudi.rqesui.presentation.extension.getFileName -import eu.europa.ec.eudi.rqesui.test_rule.CoroutineTestRule -import eu.europa.ec.eudi.rqesui.test_rule.runTest +import eu.europa.ec.eudi.rqesui.util.CoroutineTestRule +import eu.europa.ec.eudi.rqesui.util.runTest import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertTrue import org.junit.After 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 index 837013c..b0e6169 100644 --- 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 @@ -21,7 +21,7 @@ import android.content.Context import android.database.Cursor import android.net.Uri import android.provider.OpenableColumns -import eu.europa.ec.eudi.rqesui.mockedDocumentName +import eu.europa.ec.eudi.rqesui.util.mockedDocumentName import junit.framework.TestCase.assertEquals import org.junit.After import org.junit.Before diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/util/Constants.kt similarity index 96% rename from rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt rename to rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/util/Constants.kt index da60140..d6ff6e6 100644 --- a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/Constants.kt +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/util/Constants.kt @@ -14,7 +14,7 @@ * governing permissions and limitations under the Licence. */ -package eu.europa.ec.eudi.rqesui +package eu.europa.ec.eudi.rqesui.util const val mockedPlainFailureMessage = "Failure message" const val mockedAuthorizationUrl = "https://endpoint.com/mockedAuthorizationUrl" diff --git a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/test_rule/CoroutineTestRule.kt b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/util/CoroutineTestRule.kt similarity index 96% rename from rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/test_rule/CoroutineTestRule.kt rename to rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/util/CoroutineTestRule.kt index c1e7ecc..3cddbf0 100644 --- a/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/test_rule/CoroutineTestRule.kt +++ b/rqes-ui-sdk/src/test/java/eu/europa/ec/eudi/rqesui/util/CoroutineTestRule.kt @@ -14,7 +14,7 @@ * governing permissions and limitations under the Licence. */ -package eu.europa.ec.eudi.rqesui.test_rule +package eu.europa.ec.eudi.rqesui.util import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.StandardTestDispatcher From f954c891600e072bf456fba27e8766beb76d365e Mon Sep 17 00:00:00 2001 From: Stilianos Tzouvaras Date: Fri, 22 Nov 2024 12:33:50 +0200 Subject: [PATCH 8/9] merge correction --- .../domain/interactor/TestSuccessInteractor.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 index 0edbd68..1057448 100644 --- 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 @@ -33,14 +33,14 @@ 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.mockedDocumentName -import eu.europa.ec.eudi.rqesui.mockedExceptionWithMessage -import eu.europa.ec.eudi.rqesui.mockedExceptionWithNoMessage -import eu.europa.ec.eudi.rqesui.mockedGenericErrorMessage -import eu.europa.ec.eudi.rqesui.mockedPlainFailureMessage import eu.europa.ec.eudi.rqesui.presentation.extension.getFileName -import eu.europa.ec.eudi.rqesui.test_rule.CoroutineTestRule -import eu.europa.ec.eudi.rqesui.test_rule.runTest +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 From 9897a3510006e6810ef7c60730e66059451ba6aa Mon Sep 17 00:00:00 2001 From: Christos Kaitatzis Date: Fri, 22 Nov 2024 15:20:05 +0200 Subject: [PATCH 9/9] Tests added for signAndSaveDocument success result and for other edge cases Signed-off-by: Christos Kaitatzis --- .../TestSelectCertificateInteractor.kt | 96 ++++++++++++++- .../interactor/TestSuccessInteractor.kt | 112 +++++++++++------- 2 files changed, 161 insertions(+), 47 deletions(-) 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 index d478974..adc0464 100644 --- 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 @@ -25,10 +25,11 @@ 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.CoroutineTestRule import eu.europa.ec.eudi.rqesui.util.runTest import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertTrue @@ -70,6 +71,8 @@ class TestSelectCertificateInteractor { fun setUp() { closeable = MockitoAnnotations.openMocks(this) interactor = SelectCertificateInteractorImpl(resourceProvider, eudiController) + whenever(resourceProvider.genericErrorMessage()) + .thenReturn(mockedGenericErrorMessage) } @After @@ -183,6 +186,39 @@ class TestSelectCertificateInteractor { (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 @@ -247,7 +283,7 @@ class TestSelectCertificateInteractor { // 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 (`mockedExceptionWithMessage`). + // 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 { @@ -271,6 +307,62 @@ class TestSelectCertificateInteractor { (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 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 index 1057448..77b33c6 100644 --- 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 @@ -33,7 +33,6 @@ 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.presentation.extension.getFileName import eu.europa.ec.eudi.rqesui.util.CoroutineTestRule import eu.europa.ec.eudi.rqesui.util.mockedDocumentName import eu.europa.ec.eudi.rqesui.util.mockedExceptionWithMessage @@ -98,6 +97,7 @@ class TestSuccessInteractor { interactor = SuccessInteractorImpl(resourceProvider, eudiRqesController) whenever(resourceProvider.genericErrorMessage()) .thenReturn(mockedGenericErrorMessage) + whenever(resourceProvider.provideContext()).thenReturn(context) } @After @@ -220,14 +220,52 @@ class TestSuccessInteractor { //endregion //region signAndSaveDocument - // Case 1: Testing when `signAndSaveDocument` fails during the credential authorization step. + // 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 (`EudiRQESUiError`). + // 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 (`mockedPlainFailureMessage`). + // 4. The `error` in the returned result should match the mocked error. @Test - fun `Given Case 1, When signAndSaveDocument is called, Then Case 1 expected result is returned`() = + fun `Given Case 2, When signAndSaveDocument is called, Then Case 2 expected result is returned`() = coroutineRule.runTest { // Arrange val error = EudiRQESUiError(message = mockedPlainFailureMessage) @@ -248,15 +286,15 @@ class TestSuccessInteractor { ) } - // Case 2: Testing when `signAndSaveDocument` succeeds in credential authorization but fails during document signing. - // Case 2 Expected Result: + // 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 (`EudiRQESUiError`). + // 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. + // 5. 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`() = + fun `Given Case 3, When signAndSaveDocument is called, Then Case 3 expected result is returned`() = coroutineRule.runTest { // Arrange val error = EudiRQESUiError(message = mockedPlainFailureMessage) @@ -278,9 +316,9 @@ class TestSuccessInteractor { ) } - // Case 3: Testing when `signAndSaveDocument` successfully goes ahead with an authorized credential, - // signs the document, but fails to save the signed document. - // Case 3 Expected Result: + // 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. @@ -288,7 +326,7 @@ class TestSuccessInteractor { // 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 3, When signAndSaveDocument is called, Then Case 3 expected result is returned`() = + fun `Given Case 4, When signAndSaveDocument is called, Then Case 4 expected result is returned`() = coroutineRule.runTest { // Arrange val error = EudiRQESUiError(message = mockedPlainFailureMessage) @@ -319,43 +357,27 @@ class TestSuccessInteractor { ) } - // Case 4: Testing when `signAndSaveDocument` successfully goes ahead with an authorized credential, - // signs the document and successfully saves the signed document. - // Case 4 Expected Result: - // 1. The interactor should successfully authorize the credential. - // 2. After successful authorization, the document should be signed successfully. - // 3. After signing, the interactor should attempt to save the signed document. - // 4. The save operation should succeed, returning the Uri of the saved document. - // 5. The `signAndSaveDocument` function should return a successful result. - // 6. The file name from the saved document's URI should be retrieved and should match the original document name. + // 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 4, When signAndSaveDocument is called, Then Case 4 expected result is returned`() = + fun `Given Case 5, When signAndSaveDocument is called, Then Case 5 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) + whenever(eudiRqesController.authorizeCredential()).thenThrow( + mockedExceptionWithMessage ) // Act val result = interactor.signAndSaveDocument(mockedDocumentName) // Assert - val fileNameResult = mockGetFileNameFromUri() - assertEquals(mockedDocumentName, fileNameResult) assertTrue(result is SuccessInteractorSignAndSaveDocumentPartialState.Failure) + val failureState = result as SuccessInteractorSignAndSaveDocumentPartialState.Failure + assertTrue(failureState.error.message == mockedExceptionWithMessage.message) } //endregion @@ -392,14 +414,14 @@ class TestSuccessInteractor { ).thenReturn(event) } - private fun mockGetFileNameFromUri(): String { + private fun mockGetFileNameFromUri() { whenever(context.contentResolver).thenReturn(contentResolver) - whenever(contentResolver.query(documentFileUri, null, null, null, null)) - .thenReturn(cursor) + 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) - return documentFileUri.getFileName(context).getOrThrow() } //endregion }