From c5739df9694e8ece086f83369b4e6fd74be96c42 Mon Sep 17 00:00:00 2001 From: Mugurell Date: Wed, 16 Oct 2019 20:16:41 +0300 Subject: [PATCH] For #4126 - Add tests for the new classes resulting from refactoring Used runBlocking to ensure we wait for the code using coroutines to execute instead of runBlockingTest and join() since this last option led to failed tests in CI because of "java.lang.IllegalStateException: This job has not completed yet". --- .../quicksettings/QuickSettingsController.kt | 13 +- .../QuickSettingsFragmentStore.kt | 25 +- .../DefaultQuickSettingsControllerTest.kt | 328 ++++++++++++++++++ .../QuickSettingsFragmentStoreTest.kt | 323 +++++++++++++++++ .../QuickSettingsInteractorTest.kt | 100 ++++++ .../ext/PhoneFeatureExtKtTest.kt | 85 +++++ 6 files changed, 862 insertions(+), 12 deletions(-) create mode 100644 app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/settings/quicksettings/ext/PhoneFeatureExtKtTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt index 483ed95ff..bcf27132a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.settings.quicksettings import android.content.Context +import androidx.annotation.VisibleForTesting import androidx.navigation.NavController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -197,7 +198,8 @@ class DefaultQuickSettingsController( * * @param requestedPermissions [Array]<[String]> runtime permissions needed to be requested. */ - private fun handleAndroidPermissionRequest(requestedPermissions: Array) { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun handleAndroidPermissionRequest(requestedPermissions: Array) { requestRuntimePermissions(requestedPermissions) } @@ -207,7 +209,8 @@ class DefaultQuickSettingsController( * * @param updatedPermissions [SitePermissions] updated website permissions. */ - private fun handlePermissionsChange(updatedPermissions: SitePermissions) { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun handlePermissionsChange(updatedPermissions: SitePermissions) { coroutineScope.launch(Dispatchers.IO) { permissionStorage.updateSitePermissions(updatedPermissions) reload(session) @@ -219,7 +222,8 @@ class DefaultQuickSettingsController( * * Get this [WebsitePermission]'s [PhoneFeature]. */ - private fun WebsitePermission.getBackingFeature(): PhoneFeature = when (this) { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun WebsitePermission.getBackingFeature(): PhoneFeature = when (this) { is WebsitePermission.Camera -> PhoneFeature.CAMERA is WebsitePermission.Microphone -> PhoneFeature.MICROPHONE is WebsitePermission.Notification -> PhoneFeature.NOTIFICATION @@ -232,7 +236,8 @@ class DefaultQuickSettingsController( * **The result only informs about the type of [WebsitePermission]. * The resulting object's properties are just stubs and not dependable.** */ - private fun PhoneFeature.getCorrespondingPermission(): WebsitePermission { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun PhoneFeature.getCorrespondingPermission(): WebsitePermission { val defaultStatus = "" val defaultEnabled = false val defaultVisible = false diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt index 3a8cb65fd..740025ade 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt @@ -8,6 +8,7 @@ import android.content.Context import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.lib.state.Action import mozilla.components.lib.state.Reducer @@ -42,7 +43,8 @@ class QuickSettingsFragmentStore( /** * String, Drawable & Drawable Tint color used to display that the current website connection is secured. */ - private val getSecuredWebsiteUiValues = Triple( + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + val getSecuredWebsiteUiValues = Triple( R.string.quick_settings_sheet_secure_connection, R.drawable.mozac_ic_lock, R.color.photonGreen50 @@ -52,7 +54,8 @@ class QuickSettingsFragmentStore( * String, Drawable & Drawable Tint color used to display that the current website connection is * **not** secured. */ - private val getInsecureWebsiteUiValues = Triple( + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + val getInsecureWebsiteUiValues = Triple( R.string.quick_settings_sheet_insecure_connection, R.drawable.mozac_ic_globe, R.color.photonRed50 @@ -97,7 +100,8 @@ class QuickSettingsFragmentStore( * tracking protection is enabled for the current website or not. * @param settings [Settings] application settings. */ - private fun createTrackingProtectionState( + @VisibleForTesting + fun createTrackingProtectionState( websiteUrl: String, isTrackingProtectionOn: Boolean, settings: Settings @@ -117,7 +121,8 @@ class QuickSettingsFragmentStore( * @param websiteUrl [String] the URL of the current web page. * @param isSecured [Boolean] whether the connection is secured (TLS) or not. */ - private fun createWebsiteInfoState( + @VisibleForTesting + fun createWebsiteInfoState( websiteUrl: String, isSecured: Boolean ): WebsiteInfoState { @@ -138,7 +143,8 @@ class QuickSettingsFragmentStore( * @param permissions [SitePermissions]? list of website permissions and their status. * @param settings [Settings] application settings. */ - private fun createWebsitePermissionState( + @VisibleForTesting + fun createWebsitePermissionState( context: Context, permissions: SitePermissions?, settings: Settings @@ -158,7 +164,8 @@ class QuickSettingsFragmentStore( /** * [PhoneFeature] to a [WebsitePermission] mapper. */ - private fun PhoneFeature.toWebsitePermission( + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun PhoneFeature.toWebsitePermission( context: Context, permissions: SitePermissions?, settings: Settings @@ -185,7 +192,8 @@ class QuickSettingsFragmentStore( /** * Helper method for getting the [WebsitePermission] properties based on a specific [PhoneFeature]. */ - private fun PhoneFeature.getPermissionStatus( + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun PhoneFeature.getPermissionStatus( context: Context, permissions: SitePermissions?, settings: Settings @@ -199,7 +207,8 @@ class QuickSettingsFragmentStore( /** * Helper class acting as a temporary container of [WebsitePermission] properties. */ - private data class PermissionStatus( + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + data class PermissionStatus( val status: String, val isVisible: Boolean, val isEnabled: Boolean, diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt new file mode 100644 index 000000000..fa506d669 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt @@ -0,0 +1,328 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.quicksettings + +import androidx.navigation.NavController +import assertk.assertAll +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFailure +import assertk.assertions.isInstanceOf +import assertk.assertions.isSameAs +import assertk.assertions.isTrue +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.slot +import io.mockk.verify +import io.mockk.verifyOrder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.ObsoleteCoroutinesApi +import kotlinx.coroutines.runBlocking +import mozilla.components.browser.session.Session +import mozilla.components.feature.session.SessionUseCases +import mozilla.components.feature.sitepermissions.SitePermissions +import mozilla.components.feature.sitepermissions.SitePermissions.Status.NO_DECISION +import mozilla.components.feature.tabs.TabsUseCases +import mozilla.components.support.test.robolectric.testContext +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.TestApplication +import org.mozilla.fenix.browser.BrowserFragment +import org.mozilla.fenix.components.PermissionStorage +import org.mozilla.fenix.exceptions.ExceptionDomains +import org.mozilla.fenix.settings.PhoneFeature +import org.mozilla.fenix.settings.quicksettings.ext.shouldBeEnabled +import org.mozilla.fenix.settings.toggle +import org.mozilla.fenix.utils.Settings +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@UseExperimental(ObsoleteCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +@Config(application = TestApplication::class) +class DefaultQuickSettingsControllerTest { + private val context = testContext + private val store = mockk() + private val coroutinesScope = GlobalScope + private val navController = mockk(relaxed = true) + private val browserSession = mockk() + private val sitePermissions = SitePermissions(origin = "", savedAt = 123) + private val appSettings = mockk(relaxed = true) + private val permissionStorage = mockk(relaxed = true) + private val trackingExceptions = mockk(relaxed = true) + private val reload = mockk(relaxed = true) + private val addNewTab = mockk(relaxed = true) + private val requestPermissions = mockk<(Array) -> Unit>(relaxed = true) + private val reportIssue = mockk<() -> Unit>(relaxed = true) + private val displayTrackingProtection = mockk<() -> Unit>(relaxed = true) + private val displayPermissions = mockk<() -> Unit>(relaxed = true) + private val dismiss = mockk<() -> Unit>(relaxed = true) + private val controller = DefaultQuickSettingsController( + context = context, + quickSettingsStore = store, + coroutineScope = coroutinesScope, + navController = navController, + session = browserSession, + sitePermissions = sitePermissions, + settings = appSettings, + permissionStorage = permissionStorage, + trackingExceptions = trackingExceptions, + reload = reload, + addNewTab = addNewTab, + requestRuntimePermissions = requestPermissions, + reportSiteIssue = reportIssue, + displayTrackingProtection = displayTrackingProtection, + displayPermissions = displayPermissions, + dismiss = dismiss + ) + + @Test + fun `handleTrackingProtectionToggled should toggle tracking and reload website`() { + val testWebsiteHost = "host.com" + val websiteHost = slot() + val session = slot() + every { store.dispatch(any()) } returns mockk() + + controller.handleTrackingProtectionToggled("https://$testWebsiteHost/page1", false) + + verifyOrder { + trackingExceptions.toggle(capture(websiteHost)) + reload(capture(session)) + } + assertAll { + assertThat(websiteHost.isCaptured).isTrue() + assertThat(websiteHost.captured).isEqualTo(testWebsiteHost) + assertThat(session.isCaptured).isTrue() + assertThat(session.captured).isEqualTo(browserSession) + } + } + + @Test + fun `handleTrackingProtectionSettingsSelected should navigate to TrackingProtectionFragment`() { + controller.handleTrackingProtectionSettingsSelected() + + verify { + navController.navigate( + QuickSettingsSheetDialogFragmentDirections + .actionQuickSettingsSheetDialogFragmentToTrackingProtectionFragment() + ) + } + } + + @Test + @ObsoleteCoroutinesApi + @ExperimentalCoroutinesApi + fun `handleReportTrackingProblem should open a report issue webpage and dismiss when in normal mode`() { + val websiteWithIssuesUrl = "https://host.com/page1" + val testReportUrl = String.format(BrowserFragment.REPORT_SITE_ISSUE_URL, websiteWithIssuesUrl) + val reportUrl = slot() + // `handleReportTrackingProblem` will behave differently depending on `isCustomTabSession` + every { browserSession.isCustomTabSession() } returns false + + controller.handleReportTrackingProblem(websiteWithIssuesUrl) + + verify { + addNewTab(capture(reportUrl)) + dismiss() + } + assertAll { + assertThat(reportUrl.isCaptured).isTrue() + assertThat(reportUrl.captured).isEqualTo(testReportUrl) + } + } + + @Test + @ObsoleteCoroutinesApi + @ExperimentalCoroutinesApi + fun `handleReportTrackingProblem should open a report issue in browser from custom tab and dismiss`() { + val websiteWithIssuesUrl = "https://host.com/page1" + val testReportUrl = String.format(BrowserFragment.REPORT_SITE_ISSUE_URL, websiteWithIssuesUrl) + val reportUrl = slot() + // `handleReportTrackingProblem` will behave differently depending on `isCustomTabSession` + every { browserSession.isCustomTabSession() } returns true + + controller.handleReportTrackingProblem(websiteWithIssuesUrl) + + verify { + addNewTab(capture(reportUrl)) + reportIssue() + dismiss() + } + assertAll { + assertThat(reportUrl.isCaptured).isTrue() + assertThat(reportUrl.captured).isEqualTo(testReportUrl) + } + } + + @Test + fun `handleTrackingProtectionShown should delegate to an injected parameter`() { + controller.handleTrackingProtectionShown() + + verify { + displayTrackingProtection() + } + } + + @Test + fun `handlePermissionsShown should delegate to an injected parameter`() { + controller.handlePermissionsShown() + + verify { + displayPermissions() + } + } + + @Test + fun `handlePermissionToggled blocked by Android should handleAndroidPermissionRequest`() { + val cameraFeature = PhoneFeature.CAMERA + val websitePermission = mockk() + val androidPermissions = slot>() + every { websitePermission.isBlockedByAndroid } returns true + + controller.handlePermissionToggled(websitePermission) + + verify { + controller.handleAndroidPermissionRequest(capture(androidPermissions)) + } + assertAll { + assertThat(androidPermissions.isCaptured).isTrue() + assertThat(androidPermissions.captured).isEqualTo(cameraFeature.androidPermissionsList) + } + } + + @Test + fun `handlePermissionToggled allowed by Android should toggle the permissions and modify View's state`() { + val permissionName = "CAMERA" + val websitePermission = mockk() + val toggledFeature = slot() + val action = slot() + every { websitePermission.isBlockedByAndroid } returns false + every { websitePermission.name } returns permissionName + every { store.dispatch(any()) } returns mockk() + // For using the SitePermissions.toggle(..) extension method we need a static mock of SitePermissions. + mockkStatic("org.mozilla.fenix.settings.ExtensionsKt") + + controller.handlePermissionToggled(websitePermission) + + // We want to verify that the Status is toggled and this event is passed to Controller also. + assertThat(sitePermissions.camera).isSameAs(NO_DECISION) + verifyOrder { + sitePermissions.toggle(capture(toggledFeature)).also { + controller.handlePermissionsChange(it) + } + } + // We should also modify View's state. Not necessarily as the last operation. + verify { + store.dispatch(capture(action)) + } + assertAll { + assertThat(toggledFeature.isCaptured).isTrue() + assertThat(toggledFeature.captured).isSameAs(PhoneFeature.CAMERA) + + assertThat(action.isCaptured).isTrue() + assertThat(action.captured).isInstanceOf(WebsitePermissionAction.TogglePermission::class) + assertThat((action.captured as WebsitePermissionAction.TogglePermission).websitePermission) + .isInstanceOf(websitePermission::class) + } + } + + @Test + fun `handleAndroidPermissionGranted should update the View's state`() { + val featureGranted = PhoneFeature.CAMERA + val permission = with(controller) { + featureGranted.getCorrespondingPermission() + } + val permissionStatus = featureGranted.getActionLabel(context, sitePermissions, appSettings) + val permissionEnabled = featureGranted.shouldBeEnabled(context, sitePermissions, appSettings) + val action = slot() + every { store.dispatch(any()) } returns mockk() + + controller.handleAndroidPermissionGranted(featureGranted) + + verify { + store.dispatch(capture(action)) + } + assertAll { + assertThat(action.isCaptured).isTrue() + assertThat(action.captured).isInstanceOf(WebsitePermissionAction.TogglePermission::class) + assertThat((action.captured as WebsitePermissionAction.TogglePermission).websitePermission).isEqualTo(permission) + assertThat((action.captured as WebsitePermissionAction.TogglePermission).updatedStatus).isEqualTo(permissionStatus) + assertThat((action.captured as WebsitePermissionAction.TogglePermission).updatedEnabledStatus).isEqualTo(permissionEnabled) + } + } + + @Test + fun `handleAndroidPermissionRequest should request from the injected callback`() { + val testPermissions = arrayOf("TestPermission") + val requiredPermissions = slot>() +// every { requestPermissions(capture(requiredPermissions)) } just Runs + + controller.handleAndroidPermissionRequest(testPermissions) + + verify { requestPermissions(capture(requiredPermissions)) } + assertAll { + assertThat(requiredPermissions.isCaptured).isTrue() + assertThat(requiredPermissions.captured).isEqualTo(testPermissions) + } + } + + @Test + @ExperimentalCoroutinesApi + fun `handlePermissionsChange should store the updated permission and reload webpage`() = runBlocking { + val testPermissions = mockk() + val permissions = slot() + val session = slot() + + controller.handlePermissionsChange(testPermissions) + + verifyOrder { + permissionStorage.updateSitePermissions(capture(permissions)) + reload(capture(session)) + } + assertAll { + assertThat(permissions.isCaptured).isTrue() + assertThat(permissions.captured).isEqualTo(testPermissions) + assertThat(session.isCaptured).isTrue() + assertThat(session.captured).isEqualTo(browserSession) + } + } + + @Test + fun `WebsitePermission#getBackingFeature should return the PhoneFeature this permission is mapped from`() { + val cameraPermission = mockk() + val microphonePermission = mockk() + val notificationPermission = mockk() + val locationPermission = mockk() + + with(controller) { + assertAll { + assertThat(cameraPermission.getBackingFeature()).isSameAs(PhoneFeature.CAMERA) + assertThat(microphonePermission.getBackingFeature()).isSameAs(PhoneFeature.MICROPHONE) + assertThat(notificationPermission.getBackingFeature()).isSameAs(PhoneFeature.NOTIFICATION) + assertThat(locationPermission.getBackingFeature()).isSameAs(PhoneFeature.LOCATION) + } + } + } + + @Test + fun `PhoneFeature#getCorrespondingPermission should return the WebsitePermission which it maps to`() { + with(controller) { + assertAll { + assertThat(PhoneFeature.CAMERA.getCorrespondingPermission()) + .isInstanceOf(WebsitePermission.Camera::class) + assertThat(PhoneFeature.MICROPHONE.getCorrespondingPermission()) + .isInstanceOf(WebsitePermission.Microphone::class) + assertThat(PhoneFeature.NOTIFICATION.getCorrespondingPermission()) + .isInstanceOf(WebsitePermission.Notification::class) + assertThat(PhoneFeature.LOCATION.getCorrespondingPermission()) + .isInstanceOf(WebsitePermission.Location::class) + assertThat { PhoneFeature.AUTOPLAY.getCorrespondingPermission() } + .isFailure().isInstanceOf(KotlinNullPointerException::class) + } + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt new file mode 100644 index 000000000..21c34e61c --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt @@ -0,0 +1,323 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.quicksettings + +import android.content.pm.PackageManager +import assertk.assertAll +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isInstanceOf +import assertk.assertions.isNotEqualTo +import assertk.assertions.isNotNull +import assertk.assertions.isNotSameAs +import assertk.assertions.isSameAs +import assertk.assertions.isTrue +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import mozilla.components.feature.sitepermissions.SitePermissions +import mozilla.components.support.test.robolectric.testContext +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.R +import org.mozilla.fenix.TestApplication +import org.mozilla.fenix.settings.PhoneFeature +import org.mozilla.fenix.settings.quicksettings.QuickSettingsFragmentStore.Companion.getInsecureWebsiteUiValues +import org.mozilla.fenix.settings.quicksettings.QuickSettingsFragmentStore.Companion.getPermissionStatus +import org.mozilla.fenix.settings.quicksettings.QuickSettingsFragmentStore.Companion.getSecuredWebsiteUiValues +import org.mozilla.fenix.settings.quicksettings.QuickSettingsFragmentStore.Companion.toWebsitePermission +import org.mozilla.fenix.settings.quicksettings.ext.shouldBeEnabled +import org.mozilla.fenix.settings.quicksettings.ext.shouldBeVisible +import org.mozilla.fenix.utils.Settings +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@UseExperimental(kotlinx.coroutines.ObsoleteCoroutinesApi::class) +@Config(application = TestApplication::class) +class QuickSettingsFragmentStoreTest { + private val context = spyk(testContext) + private val permissions = mockk() + private val appSettings = mockk() + private val secureStringRes = R.string.quick_settings_sheet_secure_connection + private val secureDrawableRes = R.drawable.mozac_ic_lock + private val secureColorRes = R.color.photonGreen50 + private val insecureStringRes = R.string.quick_settings_sheet_insecure_connection + private val insecureDrawableRes = R.drawable.mozac_ic_globe + private val insecureColorRes = R.color.photonRed50 + + @Test + fun `createStore constructs a QuickSettingsFragmentState`() { + val settings = mockk(relaxed = true) + val permissions = mockk(relaxed = true) + every { settings.shouldUseTrackingProtection } returns true + + val store = QuickSettingsFragmentStore.createStore( + context, "url", true, true, permissions, settings + ) + + assertAll { + assertThat(store).isNotNull() + assertThat(store.state).isNotNull() + assertThat(store.state.webInfoState).isNotNull() + assertThat(store.state.trackingProtectionState).isNotNull() + assertThat(store.state.websitePermissionsState).isNotNull() + } + } + + @Test + fun `createWebsiteInfoState constructs a WebsiteInfoState with the right values for a secure connection`() { + val websiteUrl = "https://host.com/page1" + val securedStatus = true + + val state = QuickSettingsFragmentStore.createWebsiteInfoState(websiteUrl, securedStatus) + + assertAll { + assertThat(state).isNotNull() + assertThat(state.websiteUrl).isSameAs(websiteUrl) + assertThat(state.securityInfoRes).isEqualTo(secureStringRes) + assertThat(state.iconRes).isEqualTo(secureDrawableRes) + assertThat(state.iconTintRes).isEqualTo(secureColorRes) + } + } + + @Test + fun `createWebsiteInfoState constructs a WebsiteInfoState with the right values for an insecure connection`() { + val websiteUrl = "https://host.com/page1" + val securedStatus = false + + val state = QuickSettingsFragmentStore.createWebsiteInfoState(websiteUrl, securedStatus) + + assertAll { + assertThat(state).isNotNull() + assertThat(state.websiteUrl).isSameAs(websiteUrl) + assertThat(state.securityInfoRes).isEqualTo(insecureStringRes) + assertThat(state.iconRes).isEqualTo(insecureDrawableRes) + assertThat(state.iconTintRes).isEqualTo(insecureColorRes) + } + } + + @Test + fun `createTrackingProtectionState helps in constructing an initial TrackingProtectionState for it's Store`() { + val websiteUrl = "https://host.com/pageThatShouldUseTrackingProtection" + val trackingPerWebsiteStatus = true + val trackingPerAppStatus = true + every { appSettings.shouldUseTrackingProtection } returns trackingPerAppStatus + + val state = QuickSettingsFragmentStore.createTrackingProtectionState( + websiteUrl, trackingPerWebsiteStatus, appSettings + ) + + assertAll { + assertThat(state).isNotNull() + assertThat(state).isNotNull() + assertThat(state.websiteUrl).isSameAs(websiteUrl) + assertThat(state.isTrackingProtectionEnabledPerWebsite).isSameAs(trackingPerWebsiteStatus) + assertThat(state.isTrackingProtectionEnabledPerApp).isEqualTo(trackingPerAppStatus) + } + } + + @Test + fun `createWebsitePermissionState helps in constructing an initial WebsitePermissionState for it's Store`() { + every { context.checkPermission(any(), any(), any()) }.returns(PackageManager.PERMISSION_GRANTED) + every { permissions.camera } returns SitePermissions.Status.ALLOWED + every { permissions.microphone } returns SitePermissions.Status.NO_DECISION + every { permissions.notification } returns SitePermissions.Status.BLOCKED + every { permissions.location } returns SitePermissions.Status.ALLOWED + + val state = QuickSettingsFragmentStore.createWebsitePermissionState( + context, permissions, appSettings + ) + + // Just need to know that the WebsitePermissionsState properties are initialized. + // Making sure they are correctly initialized is tested in the `initWebsitePermission` test. + assertAll { + assertThat(state).isNotNull() + assertThat(state.camera).isNotNull() + assertThat(state.microphone).isNotNull() + assertThat(state.notification).isNotNull() + assertThat(state.location).isNotNull() + } + } + + @Test + fun `PhoneFeature#toWebsitePermission helps in constructing the right WebsitePermission`() { + val cameraFeature = PhoneFeature.CAMERA + val allowedStatus = testContext.getString(R.string.preference_option_phone_feature_allowed) + every { context.checkPermission(any(), any(), any()) }.returns(PackageManager.PERMISSION_GRANTED) + every { permissions.camera } returns SitePermissions.Status.ALLOWED + + val websitePermission = cameraFeature.toWebsitePermission(context, permissions, appSettings) + + assertAll { + assertThat(websitePermission).isNotNull() + assertThat(websitePermission).isInstanceOf(WebsitePermission.Camera::class) + assertThat(websitePermission.status).isEqualTo(allowedStatus) + assertThat(websitePermission.isVisible).isTrue() + assertThat(websitePermission.isEnabled).isTrue() + assertThat(websitePermission.isBlockedByAndroid).isFalse() + } + } + + @Test + fun `PhoneFeature#getPermissionStatus gets the permission properties from delegates`() { + val phoneFeature = PhoneFeature.CAMERA + every { permissions.camera } returns SitePermissions.Status.NO_DECISION + + val permissionsStatus = phoneFeature.getPermissionStatus(context, permissions, appSettings) + + verify { + // Verifying phoneFeature.getActionLabel gets "Status(child of #2#4).ordinal()) was not called" +// phoneFeature.getActionLabel(context, permissions, appSettings) + phoneFeature.shouldBeVisible(permissions, appSettings) + phoneFeature.shouldBeEnabled(context, permissions, appSettings) + phoneFeature.isAndroidPermissionGranted(context) + } + assertAll { + // Check that we only have a non-null permission status. + // Having each property calculated in a separate delegate means their correctness is + // to be tested in that delegated method. + assertThat(permissionsStatus).isNotNull() + } + } + + @ExperimentalCoroutinesApi + @Test + fun `TrackingProtectionToggled should update only the tracking enabled status`() = runBlocking { + val initialUrl = "https://host.com/page1" + val initialTrackingPerApp = true + val initialTrackingPerWebsite = true + val updatedTrackingPerWebsite = false + val appSettings = mockk() + every { appSettings.shouldUseTrackingProtection } returns initialTrackingPerApp + val initialTrackingProtectionState = QuickSettingsFragmentStore.createTrackingProtectionState( + initialUrl, initialTrackingPerWebsite, appSettings + ) + val initialWebsiteInfoState = mockk() + val initialWebsitePermissionsState = mockk() + val store = QuickSettingsFragmentStore(QuickSettingsFragmentState( + initialTrackingProtectionState, initialWebsiteInfoState, initialWebsitePermissionsState + )) + + store.dispatch(TrackingProtectionAction.TrackingProtectionToggled(updatedTrackingPerWebsite)).join() + + assertAll { + assertThat(store.state.webInfoState).isSameAs(initialWebsiteInfoState) + assertThat(store.state.websitePermissionsState).isSameAs(initialWebsitePermissionsState) + assertThat(store.state.trackingProtectionState).isNotSameAs(initialTrackingProtectionState) + + assertThat(store.state.trackingProtectionState.isTrackingProtectionEnabledPerWebsite) + .isNotEqualTo(initialTrackingPerWebsite) + assertThat(store.state.trackingProtectionState.isTrackingProtectionEnabledPerWebsite) + .isEqualTo(updatedTrackingPerWebsite) + assertThat(store.state.trackingProtectionState.isTrackingProtectionEnabledPerApp) + .isSameAs(initialTrackingPerApp) + assertThat(store.state.trackingProtectionState.websiteUrl).isSameAs(initialUrl) + } + } + + @Test + @ExperimentalCoroutinesApi + fun `TogglePermission should only modify status and visibility of a specific WebsitePermissionsState`() = runBlocking { + val cameraPermissionName = "Camera" + val microphonePermissionName = "Microphone" + val notificationPermissionName = "Notification" + val locationPermissionName = "Location" + val initialCameraStatus = "initialCameraStatus" + val initialMicStatus = "initialMicStatus" + val initialNotificationStatus = "initialNotificationStatus" + val initialLocationStatus = "initialLocationStatus" + val updatedMicrophoneStatus = "updatedNotificationStatus" + val updatedMicrophoneEnabledStatus = false + val defaultVisibilityStatus = true + val defaultEnabledStatus = true + val defaultBlockedByAndroidStatus = true + val websiteInfoState = mockk() + val trackingProtectionState = mockk() + val initialWebsitePermissionsState = WebsitePermissionsState( + isVisible = true, + camera = WebsitePermission.Camera(initialCameraStatus, defaultVisibilityStatus, + defaultEnabledStatus, defaultBlockedByAndroidStatus, cameraPermissionName), + microphone = WebsitePermission.Microphone(initialMicStatus, defaultVisibilityStatus, + defaultEnabledStatus, defaultBlockedByAndroidStatus, microphonePermissionName), + notification = WebsitePermission.Notification(initialNotificationStatus, defaultVisibilityStatus, + defaultEnabledStatus, defaultBlockedByAndroidStatus, notificationPermissionName), + location = WebsitePermission.Location(initialLocationStatus, defaultVisibilityStatus, + defaultEnabledStatus, defaultBlockedByAndroidStatus, locationPermissionName) + ) + val initialState = QuickSettingsFragmentState( + trackingProtectionState, websiteInfoState, initialWebsitePermissionsState + ) + val store = QuickSettingsFragmentStore(initialState) + + store.dispatch(WebsitePermissionAction.TogglePermission( + mockk(), updatedMicrophoneStatus, updatedMicrophoneEnabledStatus) + ).join() + + assertAll { + assertThat(store.state).isNotNull() + assertThat(store.state).isNotSameAs(initialState) + assertThat(store.state.websitePermissionsState).isNotSameAs(initialWebsitePermissionsState) + assertThat(store.state.webInfoState).isSameAs(websiteInfoState) + assertThat(store.state.trackingProtectionState).isSameAs(trackingProtectionState) + + assertThat(store.state.websitePermissionsState.camera).isNotNull() + assertThat((store.state.websitePermissionsState.camera as WebsitePermission.Camera).name).isEqualTo(cameraPermissionName) + assertThat(store.state.websitePermissionsState.camera.status).isEqualTo(initialCameraStatus) + assertThat(store.state.websitePermissionsState.camera.isVisible).isEqualTo(defaultVisibilityStatus) + assertThat(store.state.websitePermissionsState.camera.isEnabled).isEqualTo(defaultEnabledStatus) + assertThat(store.state.websitePermissionsState.camera.isBlockedByAndroid).isEqualTo(defaultBlockedByAndroidStatus) + + assertThat(store.state.websitePermissionsState.microphone).isNotNull() + assertThat((store.state.websitePermissionsState.microphone as WebsitePermission.Microphone).name).isEqualTo(microphonePermissionName) + // Only the following two properties must have been changed! + assertThat(store.state.websitePermissionsState.microphone.status).isEqualTo(updatedMicrophoneStatus) + assertThat(store.state.websitePermissionsState.microphone.isEnabled).isEqualTo(updatedMicrophoneEnabledStatus) + + assertThat(store.state.websitePermissionsState.microphone.isVisible).isEqualTo(defaultVisibilityStatus) + assertThat(store.state.websitePermissionsState.microphone.isBlockedByAndroid).isEqualTo(defaultBlockedByAndroidStatus) + + assertThat(store.state.websitePermissionsState.notification).isNotNull() + assertThat((store.state.websitePermissionsState.notification as WebsitePermission.Notification).name).isEqualTo(notificationPermissionName) + assertThat(store.state.websitePermissionsState.notification.status).isEqualTo(initialNotificationStatus) + assertThat(store.state.websitePermissionsState.notification.isVisible).isEqualTo(defaultVisibilityStatus) + assertThat(store.state.websitePermissionsState.notification.isEnabled).isEqualTo(defaultEnabledStatus) + assertThat(store.state.websitePermissionsState.notification.isBlockedByAndroid).isEqualTo(defaultBlockedByAndroidStatus) + + assertThat(store.state.websitePermissionsState.location).isNotNull() + assertThat((store.state.websitePermissionsState.location as WebsitePermission.Location).name).isEqualTo(locationPermissionName) + assertThat(store.state.websitePermissionsState.location.status).isEqualTo(initialLocationStatus) + assertThat(store.state.websitePermissionsState.location.isVisible).isEqualTo(defaultVisibilityStatus) + assertThat(store.state.websitePermissionsState.location.isEnabled).isEqualTo(defaultEnabledStatus) + assertThat(store.state.websitePermissionsState.location.isBlockedByAndroid).isEqualTo(defaultBlockedByAndroidStatus) + } + } + + @Test + fun `getSecuredWebsiteUiValues() should return the right values`() { + val uiValues = getSecuredWebsiteUiValues + + assertAll { + assertThat(uiValues.first).isEqualTo(secureStringRes) + assertThat(uiValues.second).isEqualTo(secureDrawableRes) + assertThat(uiValues.third).isEqualTo(secureColorRes) + } + } + + @Test + fun `getInsecureWebsiteUiValues() should return the right values`() { + val uiValues = getInsecureWebsiteUiValues + + assertAll { + assertThat(uiValues.first).isEqualTo(insecureStringRes) + assertThat(uiValues.second).isEqualTo(insecureDrawableRes) + assertThat(uiValues.third).isEqualTo(insecureColorRes) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt new file mode 100644 index 000000000..9da7ea11b --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.quicksettings + +import assertk.assertAll +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isTrue +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import io.mockk.verifyAll +import org.junit.Test + +class QuickSettingsInteractorTest { + private val controller = mockk(relaxed = true) + private val interactor = QuickSettingsInteractor(controller) + + @Test + fun `onReportProblemSelected should delegate the controller`() { + val websiteUrl = "https://host.com/page1" + val url = slot() + + interactor.onReportProblemSelected(websiteUrl) + + verify { + controller.handleReportTrackingProblem(capture(url)) + } + assertAll { + assertThat(url.isCaptured).isTrue() + assertThat(url.captured).isEqualTo(websiteUrl) + } + } + + @Test + fun `onProtectionToggled should delegate the controller`() { + val websiteUrl = "https://host.com/page1" + val trackingEnabled = true + val url = slot() + val trackingStatus = slot() + + interactor.onProtectionToggled(websiteUrl, trackingEnabled) + + verifyAll { + controller.handleTrackingProtectionToggled(capture(url), capture(trackingStatus)) + } + assertAll { + assertThat(url.isCaptured).isTrue() + assertThat(url.captured).isEqualTo(websiteUrl) + + assertThat(trackingStatus.isCaptured).isTrue() + assertThat(trackingStatus.captured).isEqualTo(trackingEnabled) + } + } + + @Test + fun `onProtectionSettingsSelected should delegate the controller`() { + interactor.onProtectionSettingsSelected() + + verify { + controller.handleTrackingProtectionSettingsSelected() + } + } + + @Test + fun `onTrackingProtectionShown should delegate the controller`() { + interactor.onTrackingProtectionShown() + + verify { + controller.handleTrackingProtectionShown() + } + } + + @Test + fun `onPermissionsShown should delegate the controller`() { + interactor.onPermissionsShown() + + verify { + controller.handlePermissionsShown() + } + } + + @Test + fun `onPermissionToggled should delegate the controller`() { + val websitePermission = mockk() + val permission = slot() + + interactor.onPermissionToggled(websitePermission) + + verify { + controller.handlePermissionToggled(capture(permission)) + } + assertAll { + assertThat(permission.isCaptured).isTrue() + assertThat(permission.captured).isEqualTo(websitePermission) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/ext/PhoneFeatureExtKtTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/ext/PhoneFeatureExtKtTest.kt new file mode 100644 index 000000000..6c3ab5305 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/ext/PhoneFeatureExtKtTest.kt @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.quicksettings.ext + +import android.content.Context +import android.content.pm.PackageManager +import assertk.assertAll +import assertk.assertThat +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import io.mockk.every +import io.mockk.mockk +import mozilla.components.feature.sitepermissions.SitePermissions +import org.junit.Test +import org.mozilla.fenix.settings.PhoneFeature + +class PhoneFeatureExtKtTest { + @Test + fun `shouldBeVisible returns if the user made a decision about the permission`() { + val noDecisionForPermission = mockk() + val userAllowedPermission = mockk() + val userBlockedPermission = mockk() + every { noDecisionForPermission.camera } returns SitePermissions.Status.NO_DECISION + every { userAllowedPermission.camera } returns SitePermissions.Status.ALLOWED + every { userBlockedPermission.camera } returns SitePermissions.Status.BLOCKED + + assertAll { + assertThat(PhoneFeature.CAMERA.shouldBeVisible(noDecisionForPermission, mockk())).isFalse() + assertThat(PhoneFeature.CAMERA.shouldBeVisible(userAllowedPermission, mockk())).isTrue() + assertThat(PhoneFeature.CAMERA.shouldBeVisible(userBlockedPermission, mockk())).isTrue() + } + } + + @Test + fun `isUserPermissionGranted returns if user allowed or denied a permission`() { + val noDecisionForPermission = mockk() + val userAllowedPermission = mockk() + val userBlockedPermission = mockk() + every { noDecisionForPermission.camera } returns SitePermissions.Status.NO_DECISION + every { userAllowedPermission.camera } returns SitePermissions.Status.ALLOWED + every { userBlockedPermission.camera } returns SitePermissions.Status.BLOCKED + + assertAll { + assertThat(PhoneFeature.CAMERA.isUserPermissionGranted(userAllowedPermission, mockk())).isTrue() + assertThat(PhoneFeature.CAMERA.isUserPermissionGranted(noDecisionForPermission, mockk())).isFalse() + assertThat(PhoneFeature.CAMERA.isUserPermissionGranted(userBlockedPermission, mockk())).isFalse() + } + } + + @Test + fun `shouldBeEnabled returns if permission is granted by user and Android`() { + val androidPermissionGrantedContext = mockk() + val androidPermissionDeniedContext = mockk() + val userAllowedPermission = mockk() + val noDecisionForPermission = mockk() + val userBlockedPermission = mockk() + every { androidPermissionGrantedContext.checkPermission(any(), any(), any()) } + .returns(PackageManager.PERMISSION_GRANTED) + every { androidPermissionDeniedContext.checkPermission(any(), any(), any()) } + .returns(PackageManager.PERMISSION_DENIED) + every { userAllowedPermission.camera } returns SitePermissions.Status.ALLOWED + every { noDecisionForPermission.camera } returns SitePermissions.Status.NO_DECISION + every { userBlockedPermission.camera } returns SitePermissions.Status.BLOCKED + + assertAll { + // Check result for when the Android permission is granted to the app + assertThat(PhoneFeature.CAMERA.shouldBeEnabled( + androidPermissionGrantedContext, userAllowedPermission, mockk())).isTrue() + assertThat(PhoneFeature.CAMERA.shouldBeEnabled( + androidPermissionGrantedContext, noDecisionForPermission, mockk())).isFalse() + assertThat(PhoneFeature.CAMERA.shouldBeEnabled( + androidPermissionGrantedContext, userBlockedPermission, mockk())).isFalse() + + // Check result for when the Android permission is denied to the app + assertThat(PhoneFeature.CAMERA.shouldBeEnabled( + androidPermissionDeniedContext, userAllowedPermission, mockk())).isFalse() + assertThat(PhoneFeature.CAMERA.shouldBeEnabled( + androidPermissionDeniedContext, noDecisionForPermission, mockk())).isFalse() + assertThat(PhoneFeature.CAMERA.shouldBeEnabled( + androidPermissionDeniedContext, userBlockedPermission, mockk())).isFalse() + } + } +}