Skip to content

Commit

Permalink
Closes mozilla-mobile#8681: Do not retry to update an extension when …
Browse files Browse the repository at this point in the history
…an unrecoverable exception happens
  • Loading branch information
Amejia481 committed Oct 19, 2020
1 parent b78b41f commit 0028ed9
Show file tree
Hide file tree
Showing 15 changed files with 355 additions and 16 deletions.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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 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
}
Original file line number Diff line number Diff line change
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
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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 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
}
Original file line number Diff line number Diff line change
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
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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 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
}
Loading

0 comments on commit 0028ed9

Please sign in to comment.