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() + } + } +}