From e058e44332fd5d09aaeec557969efda7bc53712d Mon Sep 17 00:00:00 2001 From: Arturo Mejia Date: Thu, 10 Jun 2021 10:22:49 -0400 Subject: [PATCH] Closes #10245: Migrate tracking protection exceptions to new GV storage --- .../TrackingProtectionExceptionFileStorage.kt | 151 ++++---- .../GeckoTrackingProtectionException.kt | 11 +- ...ckingProtectionExceptionFileStorageTest.kt | 322 +++++++++--------- docs/changelog.md | 1 + 4 files changed, 257 insertions(+), 228 deletions(-) diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/TrackingProtectionExceptionFileStorage.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/TrackingProtectionExceptionFileStorage.kt index bb0114f0904..75a0aea6c27 100644 --- a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/TrackingProtectionExceptionFileStorage.kt +++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/TrackingProtectionExceptionFileStorage.kt @@ -14,15 +14,18 @@ import mozilla.components.browser.engine.gecko.content.blocking.GeckoTrackingPro import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.content.blocking.TrackingProtectionException import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage +import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.ktx.util.readAndDeserialize -import mozilla.components.support.ktx.util.writeString import org.json.JSONArray -import org.json.JSONObject import org.mozilla.geckoview.ContentBlockingController.ContentBlockingException import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission +import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW +import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY +import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_TRACKING import java.io.File -private const val STORE_FILE_NAME_FORMAT = +internal const val STORE_FILE_NAME = "mozilla_components_tracking_protection_storage_gecko.json" /** @@ -34,83 +37,97 @@ internal class TrackingProtectionExceptionFileStorage( ) : TrackingProtectionExceptionStorage { private val fileLock = Any() internal var scope = CoroutineScope(Dispatchers.IO) + private val logger = Logger("TrackingProtectionExceptionFileStorage") /** - * Restore all exceptions from the [STORE_FILE_NAME_FORMAT] file, + * Restore all exceptions from the [STORE_FILE_NAME] file, * and provides them to the gecko [runtime]. */ override fun restore() { - scope.launch { - synchronized(fileLock) { - getFile(context).readAndDeserialize { json -> - if (json.isNotEmpty()) { - val jsonArray = JSONArray(json) - val exceptionList = (0 until jsonArray.length()).map { index -> - val jsonObject = jsonArray.getJSONObject(index) - ContentBlockingException.fromJson(jsonObject) - } - runtime.contentBlockingController.restoreExceptionList(exceptionList) - } - } - } + if (!isMigrationOver(context)) { + logger.info("Starting tracking protection exceptions migration") + migrateExceptions() } } override fun contains(session: EngineSession, onResult: (Boolean) -> Unit) { - val geckoSession = (session as GeckoEngineSession).geckoSession - runtime.contentBlockingController.checkException(geckoSession).accept { - if (it != null) { - onResult(it) - } else { - onResult(false) + val url = (session as GeckoEngineSession).currentUrl + if (!url.isNullOrEmpty()) { + runtime.storageController.getPermissions(url).accept { permissions -> + val contains = permissions.filterTrackingProtectionExceptions().isNotEmpty() + onResult(contains) } + } else { + onResult(false) } } override fun fetchAll(onResult: (List) -> Unit) { - runtime.contentBlockingController.saveExceptionList().accept { exceptionList -> - val exceptions = if (exceptionList != null) { - val exceptions = exceptionList.map { it.toTrackingProtectionException() } - exceptions - } else { - emptyList() - } - onResult(exceptions) + runtime.storageController.allPermissions.accept { permissions -> + val trackingExceptions = permissions.filterTrackingProtectionExceptions() + onResult(trackingExceptions.map { exceptions -> exceptions.toTrackingProtectionException() }) } } + private fun List?.filterTrackingProtectionExceptions() = + this.orEmpty().filter { it.isExcluded } + + private val ContentPermission.isExcluded: Boolean + get() = this.permission == PERMISSION_TRACKING && value == VALUE_ALLOW + override fun add(session: EngineSession) { val geckoEngineSession = (session as GeckoEngineSession) runtime.contentBlockingController.addException(geckoEngineSession.geckoSession) geckoEngineSession.notifyObservers { onExcludedOnTrackingProtectionChange(true) } - persist() } override fun remove(session: EngineSession) { val geckoEngineSession = (session as GeckoEngineSession) - runtime.contentBlockingController.removeException(geckoEngineSession.geckoSession) + val url = geckoEngineSession.currentUrl ?: return geckoEngineSession.notifyObservers { onExcludedOnTrackingProtectionChange(false) } - persist() + remove(url) } override fun remove(exception: TrackingProtectionException) { - val geckoException = (exception as GeckoTrackingProtectionException) - runtime.contentBlockingController.removeException(geckoException.toContentBlockingException()) - persist() + if (exception is GeckoTrackingProtectionException) { + remove(exception.contentPermission) + } else { + remove(exception.url) + } + } + + @VisibleForTesting + internal fun remove(url: String) { + val storage = runtime.storageController + storage.getPermissions(url).accept { permissions -> + permissions.filterTrackingProtectionExceptions().forEach { geckoPermissions -> + storage.setPermission(geckoPermissions, VALUE_DENY) + } + } + } + + @VisibleForTesting + internal fun remove(contentPermission: ContentPermission) { + runtime.storageController.setPermission(contentPermission, VALUE_DENY) } override fun removeAll(activeSessions: List?) { - runtime.contentBlockingController.clearExceptionList() + val storage = runtime.storageController activeSessions?.forEach { engineSession -> engineSession.notifyObservers { onExcludedOnTrackingProtectionChange(false) } } - removeFileFromDisk(context) + storage.allPermissions.accept { permissions -> + val trackingExceptions = permissions.filterTrackingProtectionExceptions() + trackingExceptions.forEach { + storage.setPermission(it, VALUE_DENY) + } + } } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @@ -118,35 +135,47 @@ internal class TrackingProtectionExceptionFileStorage( return AtomicFile( File( context.filesDir, - STORE_FILE_NAME_FORMAT + STORE_FILE_NAME ) ) } + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun isMigrationOver(context: Context): Boolean { + /* + * We only keep around the exceptions file until + * the migration is over [STORE_FILE_NAME], + * after we migrate exceptions we delete the file. + * */ + return File(context.filesDir, STORE_FILE_NAME).exists() + } + /** - * Take all the exception from the gecko [runtime] and saves them into the - * [STORE_FILE_NAME_FORMAT] file. + * As part of the migration process, we have to load all exceptions from our + * file [STORE_FILE_NAME] into geckoView once, after that we can remove our, + * file [STORE_FILE_NAME]. */ - private fun persist() { - runtime.contentBlockingController.saveExceptionList().accept { exceptionList -> - if (exceptionList != null) { - scope.launch { - synchronized(fileLock) { - getFile(context).writeString { - val jsonList = exceptionList.map { item -> - item.toJson() - } - JSONArray(jsonList).toString() + internal fun migrateExceptions() { + scope.launch { + synchronized(fileLock) { + getFile(context).readAndDeserialize { json -> + if (json.isNotEmpty()) { + val jsonArray = JSONArray(json) + val exceptionList = (0 until jsonArray.length()).map { index -> + val jsonObject = jsonArray.getJSONObject(index) + ContentBlockingException.fromJson(jsonObject) } + runtime.contentBlockingController.restoreExceptionList(exceptionList) } } - } else { removeFileFromDisk(context) + logger.debug("Finished tracking protection exceptions migration") } } } - private fun removeFileFromDisk(context: Context) { + @VisibleForTesting + internal fun removeFileFromDisk(context: Context) { scope.launch { synchronized(fileLock) { getFile(context) @@ -156,16 +185,6 @@ internal class TrackingProtectionExceptionFileStorage( } } -private fun ContentBlockingException.toTrackingProtectionException(): GeckoTrackingProtectionException { - val json = toJson() - val principal = json.getString("principal") - val uri = json.getString("uri") - return GeckoTrackingProtectionException(uri, principal) -} - -private fun GeckoTrackingProtectionException.toContentBlockingException(): ContentBlockingException { - val json = JSONObject() - json.put("principal", principal) - json.put("uri", url) - return ContentBlockingException.fromJson(json) +private fun ContentPermission.toTrackingProtectionException(): GeckoTrackingProtectionException { + return GeckoTrackingProtectionException(uri, privateMode, this) } diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/content/blocking/GeckoTrackingProtectionException.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/content/blocking/GeckoTrackingProtectionException.kt index eb5ed61a9a5..082f1ab9e6d 100644 --- a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/content/blocking/GeckoTrackingProtectionException.kt +++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/content/blocking/GeckoTrackingProtectionException.kt @@ -5,11 +5,16 @@ package mozilla.components.browser.engine.gecko.content.blocking import mozilla.components.concept.engine.content.blocking.TrackingProtectionException +import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission /** * Represents a site that will be ignored by the tracking protection policies. * @property url The url of the site to be ignored. - * @property principal Internal gecko identifier of an URI. + * @property privateMode Indicates if this exception should persisted in private mode. + * @property contentPermission The associated gecko content permission of this exception. */ -data class GeckoTrackingProtectionException(override val url: String, val principal: String = "") : - TrackingProtectionException +data class GeckoTrackingProtectionException( + override val url: String, + val privateMode: Boolean = false, + val contentPermission: ContentPermission +) : TrackingProtectionException diff --git a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/TrackingProtectionExceptionFileStorageTest.kt b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/TrackingProtectionExceptionFileStorageTest.kt index 30b3ea65134..d8d327d9dc9 100644 --- a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/TrackingProtectionExceptionFileStorageTest.kt +++ b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/TrackingProtectionExceptionFileStorageTest.kt @@ -6,18 +6,20 @@ package mozilla.components.browser.engine.gecko import android.app.Activity import android.content.Context +import android.util.AtomicFile import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import mozilla.components.browser.engine.gecko.content.blocking.GeckoTrackingProtectionException +import mozilla.components.browser.engine.gecko.permission.geckoContentPermission import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.content.blocking.TrackingProtectionException import mozilla.components.support.ktx.util.readAndDeserialize +import mozilla.components.support.ktx.util.writeString import mozilla.components.support.test.any import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.whenever -import org.json.JSONObject import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -26,20 +28,31 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.anyString +import org.mockito.Mockito.doNothing import org.mockito.Mockito.spy +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mozilla.geckoview.ContentBlockingController import org.mozilla.geckoview.ContentBlockingController.ContentBlockingException import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission +import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW +import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission.VALUE_DENY +import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_TRACKING +import org.mozilla.geckoview.StorageController import org.robolectric.Robolectric.buildActivity +import java.io.File @RunWith(AndroidJUnit4::class) class TrackingProtectionExceptionFileStorageTest { private lateinit var runtime: GeckoRuntime + private lateinit var storage: TrackingProtectionExceptionFileStorage + private val context: Context get() = buildActivity(Activity::class.java).get() @@ -47,53 +60,68 @@ class TrackingProtectionExceptionFileStorageTest { fun setup() { runtime = mock() whenever(runtime.settings).thenReturn(mock()) + storage = spy(TrackingProtectionExceptionFileStorage(testContext, runtime)) + storage.scope = CoroutineScope(Dispatchers.Main) + } + + @Test + fun `GIVEN the migration has not been completed WHEN restoring THEN migrate exceptions`() { + + whenever(storage.isMigrationOver(testContext)).thenReturn(false) + doNothing().`when`(storage).migrateExceptions() + + storage.restore() + + verify(storage).migrateExceptions() } @Test - fun `restoreAsync exception`() { + fun `GIVEN the migration has been completed WHEN restoring THEN not migrate exceptions`() { + + whenever(storage.isMigrationOver(testContext)).thenReturn(true) + + storage.restore() + + verify(storage, times(0)).migrateExceptions() + } + + @Test + fun `WHEN migrating exceptions THEN exceptions on disk will be restored on gecko and removed from disk`() { + val exceptionsFile = AtomicFile( + File(context.filesDir, STORE_FILE_NAME) + ) + val exceptionStringJSON = + "[{\"principal\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5jbm4uY29tLyJ9fQ==\",\"uri\":\"https:\\/\\/www.cnn.com\\/\"}]" + + exceptionsFile.writeString { exceptionStringJSON } + val mockContentBlocking = mock() - val session = mock() - val geckoResult = GeckoResult>() - val mockGeckoSession = mock() - val mockExceptionList = - listOf(ContentBlockingException.fromJson(JSONObject("{\"principal\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5jbm4uY29tLyJ9fQ==\",\"uri\":\"https:\\/\\/www.cnn.com\\/\"}"))) - whenever(session.geckoSession).thenReturn(mockGeckoSession) whenever(runtime.contentBlockingController).thenReturn(mockContentBlocking) - whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult) - val storage = TrackingProtectionExceptionFileStorage(testContext, runtime) storage.scope = CoroutineScope(Dispatchers.Main) - assertNull(storage.getFile(context).readAndDeserialize { }) + assertNotNull(storage.getFile(context).readAndDeserialize { }) - storage.add(session) - geckoResult.complete(mockExceptionList) - - storage.restore() + storage.migrateExceptions() verify(mockContentBlocking).restoreExceptionList(any>()) - assertNotNull(storage.getFile(context).readAndDeserialize { }) + verify(storage).removeFileFromDisk(any()) + + assertNull(storage.getFile(context).readAndDeserialize { }) } @Test - fun `add exception`() { + fun `GIVEN a new exception WHEN adding THEN the exception is stored on the gecko storage`() { val mockContentBlocking = mock() - val session = spy(GeckoEngineSession(runtime)) - val geckoResult = GeckoResult>() val mockGeckoSession = mock() - val mockExceptionList = - listOf(ContentBlockingException.fromJson(JSONObject("{\"principal\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5jbm4uY29tLyJ9fQ==\",\"uri\":\"https:\\/\\/www.cnn.com\\/\"}"))) - var excludedOnTrackingProtection = false + val session = spy(GeckoEngineSession(runtime, geckoSessionProvider = { mockGeckoSession })) - whenever(session.geckoSession).thenReturn(mockGeckoSession) whenever(runtime.contentBlockingController).thenReturn(mockContentBlocking) - whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult) + whenever(session.geckoSession).thenReturn(mockGeckoSession) - val storage = TrackingProtectionExceptionFileStorage(testContext, runtime) - storage.scope = CoroutineScope(Dispatchers.Main) + var excludedOnTrackingProtection = false - assertNull(storage.getFile(context).readAndDeserialize { }) session.register(object : EngineSession.Observer { override fun onExcludedOnTrackingProtectionChange(excluded: Boolean) { excludedOnTrackingProtection = excluded @@ -101,192 +129,168 @@ class TrackingProtectionExceptionFileStorageTest { }) storage.add(session) - geckoResult.complete(mockExceptionList) verify(mockContentBlocking).addException(mockGeckoSession) - verify(mockContentBlocking).saveExceptionList() assertTrue(excludedOnTrackingProtection) - assertNotNull(storage.getFile(context).readAndDeserialize { }) } @Test - fun `remove all exceptions`() { - val mockContentBlocking = mock() - val session = mock() - val geckoResult = GeckoResult>() + fun `WHEN removing an exception by session THEN the session is removed of the exception list`() { val mockGeckoSession = mock() - val mockExceptionList = - listOf(ContentBlockingException.fromJson(JSONObject("{\"principal\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5jbm4uY29tLyJ9fQ==\",\"uri\":\"https:\\/\\/www.cnn.com\\/\"}"))) - val engineSession: EngineSession = mock() - val activeSessions: List = listOf(engineSession) + val session = spy(GeckoEngineSession(runtime, geckoSessionProvider = { mockGeckoSession })) whenever(session.geckoSession).thenReturn(mockGeckoSession) - whenever(runtime.contentBlockingController).thenReturn(mockContentBlocking) - whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult) + whenever(session.currentUrl).thenReturn("https://example.com/") + doNothing().`when`(storage).remove(anyString()) - // Adding exception - val storage = TrackingProtectionExceptionFileStorage(testContext, runtime) - storage.scope = CoroutineScope(Dispatchers.Main) + var excludedOnTrackingProtection = true - assertNull(storage.getFile(context).readAndDeserialize { }) + session.register(object : EngineSession.Observer { + override fun onExcludedOnTrackingProtectionChange(excluded: Boolean) { + excludedOnTrackingProtection = excluded + } + }) - storage.add(session) - geckoResult.complete(mockExceptionList) + storage.remove(session) - verify(mockContentBlocking).addException(mockGeckoSession) - verify(mockContentBlocking).saveExceptionList() - assertNotNull(storage.getFile(context).readAndDeserialize { }) + verify(storage).remove(anyString()) + assertFalse(excludedOnTrackingProtection) + } - // Removing exceptions - storage.removeAll(activeSessions) - verify(mockContentBlocking).clearExceptionList() - assertNull(storage.getFile(context).readAndDeserialize { }) - verify(engineSession).notifyObservers(any()) + @Test + fun `GIVEN TrackingProtectionException WHEN removing THEN remove the exception using with its contentPermission`() { + val geckoException = mock() + val contentPermission = mock() + + whenever(geckoException.contentPermission).thenReturn(contentPermission) + doNothing().`when`(storage).remove(contentPermission) + + storage.remove(geckoException) + verify(storage).remove(geckoException.contentPermission) } @Test - fun `remove exception`() { - val mockContentBlocking = mock() - val session = spy(GeckoEngineSession(runtime)) - var geckoResult = GeckoResult>() - val mockGeckoSession = mock() - val mockExceptionList = - listOf(ContentBlockingException.fromJson(JSONObject("{\"principal\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5jbm4uY29tLyJ9fQ==\",\"uri\":\"https:\\/\\/www.cnn.com\\/\"}"))) - var excludedOnTrackingProtection = false + fun `GIVEN URL WHEN removing THEN remove the exception using with its URL`() { + val exception = mock() - whenever(session.geckoSession).thenReturn(mockGeckoSession) - whenever(runtime.contentBlockingController).thenReturn(mockContentBlocking) - whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult) + whenever(exception.url).thenReturn("https://example.com/") + doNothing().`when`(storage).remove(anyString()) - // Adding exception - val storage = TrackingProtectionExceptionFileStorage(testContext, runtime) - storage.scope = CoroutineScope(Dispatchers.Main) + storage.remove(exception) + verify(storage).remove(anyString()) + } + + @Test + fun `WHEN removing an exception by contentPermission THEN remove it from the gecko storage`() { + val contentPermission = mock() + val storageController = mock() + + whenever(runtime.storageController).thenReturn(storageController) + + storage.remove(contentPermission) + + verify(storageController).setPermission(contentPermission, VALUE_DENY) + } + + @Test + fun `WHEN removing an exception by URL THEN try to find it in the gecko store and remove it`() { + val contentPermission = + geckoContentPermission("https://example.com/", PERMISSION_TRACKING, VALUE_ALLOW) + val storageController = mock() + val geckoResult = GeckoResult>() + + whenever(runtime.storageController).thenReturn(storageController) + whenever(runtime.storageController.getPermissions(anyString())).thenReturn(geckoResult) + + storage.remove("https://example.com/") + + geckoResult.complete(listOf(contentPermission)) + + verify(storageController).setPermission(contentPermission, VALUE_DENY) + } + + @Test + fun `WHEN removing all exceptions THEN remove all the exceptions in the gecko store`() { + val mockGeckoSession = mock() + val session = GeckoEngineSession(runtime, geckoSessionProvider = { mockGeckoSession }) + + val contentPermission = + geckoContentPermission("https://example.com/", PERMISSION_TRACKING, VALUE_ALLOW) + val storageController = mock() + val geckoResult = GeckoResult>() + var excludedOnTrackingProtection = true - assertNull(storage.getFile(context).readAndDeserialize { }) session.register(object : EngineSession.Observer { override fun onExcludedOnTrackingProtectionChange(excluded: Boolean) { excludedOnTrackingProtection = excluded } }) - storage.add(session) - geckoResult.complete(mockExceptionList) + whenever(runtime.storageController).thenReturn(storageController) + whenever(runtime.storageController.allPermissions).thenReturn(geckoResult) - verify(mockContentBlocking).addException(mockGeckoSession) - verify(mockContentBlocking).saveExceptionList() - assertNotNull(storage.getFile(context).readAndDeserialize { }) - assertTrue(excludedOnTrackingProtection) + storage.removeAll(listOf(session)) - // Removing exception - geckoResult = GeckoResult>() - whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult) - storage.remove(session) - verify(mockContentBlocking).removeException(mockGeckoSession) - geckoResult.complete(null) - assertNull(storage.getFile(context).readAndDeserialize { }) + geckoResult.complete(listOf(contentPermission)) + + verify(storageController).setPermission(contentPermission, VALUE_DENY) assertFalse(excludedOnTrackingProtection) } @Test - fun `remove a TrackingProtectionException`() { - val mockContentBlocking = mock() - val session = spy(GeckoEngineSession(runtime)) - var geckoResult = GeckoResult>() - val mockGeckoSession = mock() - val mockExceptionList = - listOf(ContentBlockingException.fromJson(JSONObject("{\"principal\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5jbm4uY29tLyJ9fQ==\",\"uri\":\"https:\\/\\/www.cnn.com\\/\"}"))) + fun `WHEN querying all exceptions THEN all the exceptions in the gecko store should be fetched`() { + val contentPermission = + geckoContentPermission("https://example.com/", PERMISSION_TRACKING, VALUE_ALLOW) + val storageController = mock() + val geckoResult = GeckoResult>() + var exceptionList: List? = null - whenever(session.geckoSession).thenReturn(mockGeckoSession) - whenever(runtime.contentBlockingController).thenReturn(mockContentBlocking) - whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult) + whenever(runtime.storageController).thenReturn(storageController) + whenever(runtime.storageController.allPermissions).thenReturn(geckoResult) - // Adding exception - val storage = TrackingProtectionExceptionFileStorage(testContext, runtime) - storage.scope = CoroutineScope(Dispatchers.Main) + storage.fetchAll { exceptions -> + exceptionList = exceptions + } - storage.add(session) - geckoResult.complete(mockExceptionList) - assertNotNull(storage.getFile(context).readAndDeserialize { }) + geckoResult.complete(listOf(contentPermission)) - // Removing exception - geckoResult = GeckoResult() - whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult) - storage.remove( - GeckoTrackingProtectionException( - "https://www.cnn.com/", - "eyIxIjp7IjAiOiJodHRwczovL3d3dy5jbm4uY29tLyJ9fQ" - ) - ) - verify(mockContentBlocking).removeException(any()) - geckoResult.complete(null) - assertNull(storage.getFile(context).readAndDeserialize { }) + assertTrue(exceptionList!!.isNotEmpty()) + val exception = exceptionList!!.first() as GeckoTrackingProtectionException + + assertEquals("https://example.com/", exception.url) + assertEquals(contentPermission, exception.contentPermission) } @Test - fun `contains exception`() { - val mockContentBlocking = mock() + fun `WHEN checking if exception is on the exception list THEN the exception is found in the storage`() { val session = mock() - var geckoResult = GeckoResult() val mockGeckoSession = mock() var containsException = false - whenever(session.geckoSession).thenReturn(mockGeckoSession) - whenever(runtime.contentBlockingController).thenReturn(mockContentBlocking) - whenever(runtime.contentBlockingController.checkException(mockGeckoSession)).thenReturn(geckoResult) + val contentPermission = + geckoContentPermission("https://example.com/", PERMISSION_TRACKING, VALUE_ALLOW) + val storageController = mock() + val geckoResult = GeckoResult>() + + whenever(runtime.storageController).thenReturn(storageController) + whenever(runtime.storageController.getPermissions(anyString())).thenReturn(geckoResult) - val storage = TrackingProtectionExceptionFileStorage(testContext, runtime) + whenever(session.currentUrl).thenReturn("https://example.com/") + whenever(session.geckoSession).thenReturn(mockGeckoSession) storage.contains(session) { contains -> containsException = contains } - geckoResult.complete(true) - verify(mockContentBlocking).checkException(mockGeckoSession) + geckoResult.complete(listOf(contentPermission)) + assertTrue(containsException) - geckoResult = GeckoResult() - whenever(runtime.contentBlockingController.checkException(mockGeckoSession)).thenReturn(geckoResult) + whenever(session.currentUrl).thenReturn("") storage.contains(session) { contains -> containsException = contains } - geckoResult.complete(null) - assertFalse(containsException) - } - - @Test - fun `getAll exceptions`() { - val mockContentBlocking = mock() - val session = mock() - var geckoResult = GeckoResult>() - val mockGeckoSession = mock() - val mockExceptionList = - listOf(ContentBlockingException.fromJson(JSONObject("{\"principal\":\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5jbm4uY29tLyJ9fQ==\",\"uri\":\"https:\\/\\/www.mozilla.com\\/\"}"))) - var exceptionList: List? = null - whenever(session.geckoSession).thenReturn(mockGeckoSession) - whenever(runtime.contentBlockingController).thenReturn(mockContentBlocking) - whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult) - - val storage = TrackingProtectionExceptionFileStorage(testContext, runtime) - - storage.fetchAll { exceptions -> - exceptionList = exceptions - } - geckoResult.complete(mockExceptionList) - - verify(mockContentBlocking).saveExceptionList() - assertTrue(exceptionList!!.isNotEmpty()) - assertEquals("https://www.mozilla.com/", exceptionList!!.first().url) - assertEquals("eyIxIjp7IjAiOiJodHRwczovL3d3dy5jbm4uY29tLyJ9fQ==", (exceptionList!!.first() as GeckoTrackingProtectionException).principal) - - geckoResult = GeckoResult() - whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult) - - storage.fetchAll { exceptions -> - exceptionList = exceptions - } - - geckoResult.complete(null) - assertTrue(exceptionList!!.isEmpty()) + assertFalse(containsException) } } diff --git a/docs/changelog.md b/docs/changelog.md index 2db16782717..26456bb6d3a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -18,6 +18,7 @@ permalink: /changelog/ * ⚠️ **This is a breaking change**: `GeckoPermissionRequest.Content` changed its signature from `GeckoPermissionRequest.Content(uri: String, type: Int, callback: PermissionDelegate.Callback)` to `GeckoPermissionRequest.Content(uri: String, type: Int, geckoPermission: PermissionDelegate.ContentPermission, geckoResult: GeckoResult)`. * 🌟️ Adds a new `GeckoSitePermissionsStorage` this allows to store permissions using the GV APIs for more information see [the geckoView ticket](https://bugzilla.mozilla.org/show_bug.cgi?id=1654832). * 🌟️ Integrated the new GeckoView permissions APIs that will bring many improvements in how site permissions are handled, see the API abstract document for [more information](https://docs.google.com/document/d/1KUq0gejnFm5erkHNkswm8JsT7nLOmWvs1KEGFz9FWxk/edit#heading=h.ls1dr18v7zrx). + * 🌟️ Tracking protection exceptions have been migrated to the GeckoView storage see [#10245](https://github.com/mozilla-mobile/android-components/issues/10245), for more details. * **feature-sitepermissions** * ⚠️ **This is a breaking change**: The `SitePermissionsStorage` has been renamed to `OnDiskSitePermissionsStorage`.