Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Closes #8681: Do not retry to update an extension when an unrecoverab…
Browse files Browse the repository at this point in the history
…le exception happens
  • Loading branch information
Amejia481 authored and mergify[bot] committed Oct 19, 2020
1 parent b78b41f commit 6883c17
Show file tree
Hide file tree
Showing 15 changed files with 355 additions and 16 deletions.
Expand Up @@ -16,6 +16,7 @@ import mozilla.components.browser.engine.gecko.mediaquery.toGeckoValue
import mozilla.components.browser.engine.gecko.profiler.Profiler
import mozilla.components.browser.engine.gecko.util.SpeculativeSessionFactory
import mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
import mozilla.components.browser.engine.gecko.webextension.GeckoWebExtensionException
import mozilla.components.browser.engine.gecko.webnotifications.GeckoWebNotificationDelegate
import mozilla.components.browser.engine.gecko.webpush.GeckoWebPushDelegate
import mozilla.components.browser.engine.gecko.webpush.GeckoWebPushHandler
Expand Down Expand Up @@ -266,7 +267,7 @@ class GeckoEngine(
onSuccess(updatedExtension)
GeckoResult<Void>()
}, { throwable ->
onError(extension.id, throwable)
onError(extension.id, GeckoWebExtensionException(throwable))
GeckoResult<Void>()
})
}
Expand Down
@@ -0,0 +1,18 @@
/* 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 mozilla.components.browser.engine.gecko.webextension

import mozilla.components.concept.engine.webextension.WebExtensionException
import org.mozilla.geckoview.WebExtension.InstallException
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED

/**
* An unexpected gecko exception that occurs when trying to perform an action on the extension like
* (but not exclusively) installing/uninstalling, removing or updating.
*/
class GeckoWebExtensionException(throwable: Throwable) : WebExtensionException(throwable) {
override val isRecoverable: Boolean = throwable is InstallException &&
throwable.code == ERROR_USER_CANCELED
}
Expand Up @@ -24,6 +24,7 @@ import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.engine.webextension.Action
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
import mozilla.components.concept.engine.webextension.WebExtensionException
import mozilla.components.support.test.any
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.eq
Expand Down Expand Up @@ -63,12 +64,23 @@ import org.mozilla.geckoview.GeckoWebExecutor
import org.mozilla.geckoview.MockWebExtension
import org.mozilla.geckoview.StorageController
import org.mozilla.geckoview.WebExtensionController
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_CORRUPT_FILE
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_FILE_ACCESS
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_INCORRECT_HASH
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_INCORRECT_ID
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_NETWORK_FAILURE
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_POSTPONED
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_UNEXPECTED_ADDON_TYPE
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED
import org.mozilla.geckoview.WebPushController
import org.robolectric.Robolectric
import java.io.IOException
import java.lang.Exception
import org.mozilla.geckoview.WebExtension as GeckoWebExtension

typealias GeckoInstallException = org.mozilla.geckoview.WebExtension.InstallException

@RunWith(AndroidJUnit4::class)
class GeckoEngineTest {

Expand Down Expand Up @@ -1208,10 +1220,57 @@ class GeckoEngineTest {
)
updateExtensionResult.completeExceptionally(expected)

assertSame(expected, throwable)
assertSame(expected, throwable!!.cause)
assertNull(result)
}

@Test
fun `failures when updating MUST indicate if they are recoverable`() {
val runtime = mock<GeckoRuntime>()
val extensionController: WebExtensionController = mock()
val engine = GeckoEngine(context, runtime = runtime)

val extension = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
mockNativeExtension(),
runtime
)
val performUpdate: (GeckoInstallException) -> WebExtensionException = { exception ->
val updateExtensionResult = GeckoResult<GeckoWebExtension>()
whenever(extensionController.update(any())).thenReturn(updateExtensionResult)
whenever(runtime.webExtensionController).thenReturn(extensionController)
var throwable: WebExtensionException? = null

engine.updateWebExtension(
extension,
onError = { _, e -> throwable = e as WebExtensionException }
)

updateExtensionResult.completeExceptionally(exception)
throwable!!
}

val unrecoverableExceptions = listOf(
mockGeckoInstallException(ERROR_NETWORK_FAILURE),
mockGeckoInstallException(ERROR_INCORRECT_HASH),
mockGeckoInstallException(ERROR_CORRUPT_FILE),
mockGeckoInstallException(ERROR_FILE_ACCESS),
mockGeckoInstallException(ERROR_SIGNEDSTATE_REQUIRED),
mockGeckoInstallException(ERROR_UNEXPECTED_ADDON_TYPE),
mockGeckoInstallException(ERROR_INCORRECT_ID),
mockGeckoInstallException(ERROR_POSTPONED)
)

unrecoverableExceptions.forEach { exception ->
assertFalse(performUpdate(exception).isRecoverable)
}

val recoverableExceptions = listOf(mockGeckoInstallException(ERROR_USER_CANCELED))

recoverableExceptions.forEach { exception ->
assertTrue(performUpdate(exception).isRecoverable)
}
}

@Test
fun `list web extensions successfully`() {
val bundle = GeckoBundle()
Expand Down Expand Up @@ -1916,4 +1975,10 @@ class GeckoEngineTest {
}
return spy(MockWebExtension(bundle))
}

private fun mockGeckoInstallException(errorCode: Int): GeckoInstallException {
val exception = object : GeckoInstallException() {}
ReflectionUtils.setField(exception, "code", errorCode)
return exception
}
}
Expand Up @@ -17,6 +17,7 @@ import mozilla.components.browser.engine.gecko.mediaquery.toGeckoValue
import mozilla.components.browser.engine.gecko.profiler.Profiler
import mozilla.components.browser.engine.gecko.util.SpeculativeSessionFactory
import mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
import mozilla.components.browser.engine.gecko.webextension.GeckoWebExtensionException
import mozilla.components.browser.engine.gecko.webnotifications.GeckoWebNotificationDelegate
import mozilla.components.browser.engine.gecko.webpush.GeckoWebPushDelegate
import mozilla.components.browser.engine.gecko.webpush.GeckoWebPushHandler
Expand Down Expand Up @@ -267,7 +268,7 @@ class GeckoEngine(
onSuccess(updatedExtension)
GeckoResult<Void>()
}, { throwable ->
onError(extension.id, throwable)
onError(extension.id, GeckoWebExtensionException(throwable))
GeckoResult<Void>()
})
}
Expand Down
@@ -0,0 +1,18 @@
/* 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 mozilla.components.browser.engine.gecko.webextension

import mozilla.components.concept.engine.webextension.WebExtensionException
import org.mozilla.geckoview.WebExtension.InstallException
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED

/**
* An unexpected gecko exception that occurs when trying to perform an action on the extension like
* (but not exclusively) installing/uninstalling, removing or updating..
*/
class GeckoWebExtensionException(throwable: Throwable) : WebExtensionException(throwable) {
override val isRecoverable: Boolean = throwable is InstallException &&
throwable.code == ERROR_USER_CANCELED
}
Expand Up @@ -24,6 +24,7 @@ import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.engine.webextension.Action
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
import mozilla.components.concept.engine.webextension.WebExtensionException
import mozilla.components.support.test.any
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.eq
Expand Down Expand Up @@ -62,13 +63,23 @@ import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoWebExecutor
import org.mozilla.geckoview.MockWebExtension
import org.mozilla.geckoview.StorageController
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_CORRUPT_FILE
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_FILE_ACCESS
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_INCORRECT_HASH
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_INCORRECT_ID
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_NETWORK_FAILURE
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_POSTPONED
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_UNEXPECTED_ADDON_TYPE
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED
import org.mozilla.geckoview.WebExtensionController
import org.mozilla.geckoview.WebPushController
import org.robolectric.Robolectric
import java.io.IOException
import java.lang.Exception
import org.mozilla.geckoview.WebExtension as GeckoWebExtension

typealias GeckoInstallException = org.mozilla.geckoview.WebExtension.InstallException

@RunWith(AndroidJUnit4::class)
class GeckoEngineTest {

Expand Down Expand Up @@ -1208,10 +1219,58 @@ class GeckoEngineTest {
)
updateExtensionResult.completeExceptionally(expected)

assertSame(expected, throwable)
assertSame(expected, throwable!!.cause)
assertNull(result)
}

@Test
fun `failures when updating MUST indicate if they are recoverable`() {
val runtime = mock<GeckoRuntime>()
val extensionController: WebExtensionController = mock()
val engine = GeckoEngine(context, runtime = runtime)

val extension = mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension(
mockNativeExtension(),
runtime
)
val performUpdate: (GeckoInstallException) -> WebExtensionException = { exception ->
val updateExtensionResult = GeckoResult<GeckoWebExtension>()
whenever(extensionController.update(any())).thenReturn(updateExtensionResult)
whenever(runtime.webExtensionController).thenReturn(extensionController)
var throwable: WebExtensionException? = null

engine.updateWebExtension(
extension,
onError = { _, e -> throwable = e as WebExtensionException
}
)

updateExtensionResult.completeExceptionally(exception)
throwable!!
}

val unrecoverableExceptions = listOf(
mockGeckoInstallException(ERROR_NETWORK_FAILURE),
mockGeckoInstallException(ERROR_INCORRECT_HASH),
mockGeckoInstallException(ERROR_CORRUPT_FILE),
mockGeckoInstallException(ERROR_FILE_ACCESS),
mockGeckoInstallException(ERROR_SIGNEDSTATE_REQUIRED),
mockGeckoInstallException(ERROR_UNEXPECTED_ADDON_TYPE),
mockGeckoInstallException(ERROR_INCORRECT_ID),
mockGeckoInstallException(ERROR_POSTPONED)
)

unrecoverableExceptions.forEach { exception ->
assertFalse(performUpdate(exception).isRecoverable)
}

val recoverableExceptions = listOf(mockGeckoInstallException(ERROR_USER_CANCELED))

recoverableExceptions.forEach { exception ->
assertTrue(performUpdate(exception).isRecoverable)
}
}

@Test
fun `list web extensions successfully`() {
val bundle = GeckoBundle()
Expand Down Expand Up @@ -1919,4 +1978,10 @@ class GeckoEngineTest {
}
return spy(MockWebExtension(bundle))
}

private fun mockGeckoInstallException(errorCode: Int): GeckoInstallException {
val exception = object : GeckoInstallException() {}
ReflectionUtils.setField(exception, "code", errorCode)
return exception
}
}
Expand Up @@ -16,6 +16,7 @@ import mozilla.components.browser.engine.gecko.mediaquery.toGeckoValue
import mozilla.components.browser.engine.gecko.profiler.Profiler
import mozilla.components.browser.engine.gecko.util.SpeculativeSessionFactory
import mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
import mozilla.components.browser.engine.gecko.webextension.GeckoWebExtensionException
import mozilla.components.browser.engine.gecko.webnotifications.GeckoWebNotificationDelegate
import mozilla.components.browser.engine.gecko.webpush.GeckoWebPushDelegate
import mozilla.components.browser.engine.gecko.webpush.GeckoWebPushHandler
Expand Down Expand Up @@ -266,7 +267,7 @@ class GeckoEngine(
onSuccess(updatedExtension)
GeckoResult<Void>()
}, { throwable ->
onError(extension.id, throwable)
onError(extension.id, GeckoWebExtensionException(throwable))
GeckoResult<Void>()
})
}
Expand Down
@@ -0,0 +1,18 @@
/* 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 mozilla.components.browser.engine.gecko.webextension

import mozilla.components.concept.engine.webextension.WebExtensionException
import org.mozilla.geckoview.WebExtension.InstallException
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED

/**
* An unexpected gecko exception that occurs when trying to perform an action on the extension like
* (but not exclusively) installing/uninstalling, removing or updating.
*/
class GeckoWebExtensionException(throwable: Throwable) : WebExtensionException(throwable) {
override val isRecoverable: Boolean = throwable is InstallException &&
throwable.code == ERROR_USER_CANCELED
}

0 comments on commit 6883c17

Please sign in to comment.