AddonManagerTest.kt (45114B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 package mozilla.components.feature.addons 6 7 import android.graphics.Bitmap 8 import androidx.test.ext.junit.runners.AndroidJUnit4 9 import kotlinx.coroutines.CoroutineScope 10 import kotlinx.coroutines.Dispatchers 11 import kotlinx.coroutines.TimeoutCancellationException 12 import kotlinx.coroutines.launch 13 import mozilla.components.browser.state.action.WebExtensionAction 14 import mozilla.components.browser.state.state.BrowserState 15 import mozilla.components.browser.state.state.WebExtensionState 16 import mozilla.components.browser.state.state.createTab 17 import mozilla.components.browser.state.store.BrowserStore 18 import mozilla.components.concept.engine.Engine 19 import mozilla.components.concept.engine.EngineSession 20 import mozilla.components.concept.engine.webextension.ActionHandler 21 import mozilla.components.concept.engine.webextension.DisabledFlags 22 import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.APP_SUPPORT 23 import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.APP_VERSION 24 import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.BLOCKLIST 25 import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.SIGNATURE 26 import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.SOFT_BLOCKLIST 27 import mozilla.components.concept.engine.webextension.DisabledFlags.Companion.USER 28 import mozilla.components.concept.engine.webextension.EnableSource 29 import mozilla.components.concept.engine.webextension.InstallationMethod 30 import mozilla.components.concept.engine.webextension.Metadata 31 import mozilla.components.concept.engine.webextension.WebExtension 32 import mozilla.components.feature.addons.ui.translateName 33 import mozilla.components.feature.addons.update.AddonUpdater.Status 34 import mozilla.components.support.test.any 35 import mozilla.components.support.test.argumentCaptor 36 import mozilla.components.support.test.eq 37 import mozilla.components.support.test.mock 38 import mozilla.components.support.test.robolectric.testContext 39 import mozilla.components.support.test.rule.MainCoroutineRule 40 import mozilla.components.support.test.rule.runTestOnMain 41 import mozilla.components.support.test.whenever 42 import mozilla.components.support.webextensions.WebExtensionSupport 43 import org.junit.After 44 import org.junit.Assert.assertEquals 45 import org.junit.Assert.assertFalse 46 import org.junit.Assert.assertNotEquals 47 import org.junit.Assert.assertNotNull 48 import org.junit.Assert.assertNull 49 import org.junit.Assert.assertTrue 50 import org.junit.Before 51 import org.junit.Rule 52 import org.junit.Test 53 import org.junit.runner.RunWith 54 import org.mockito.ArgumentMatchers.anyBoolean 55 import org.mockito.ArgumentMatchers.anyString 56 import org.mockito.Mockito.doNothing 57 import org.mockito.Mockito.doThrow 58 import org.mockito.Mockito.never 59 import org.mockito.Mockito.spy 60 import org.mockito.Mockito.times 61 import org.mockito.Mockito.verify 62 63 @RunWith(AndroidJUnit4::class) 64 class AddonManagerTest { 65 66 @get:Rule 67 val coroutinesTestRule = MainCoroutineRule() 68 private val dispatcher = coroutinesTestRule.testDispatcher 69 70 @Before 71 fun setup() { 72 WebExtensionSupport.installedExtensions.clear() 73 } 74 75 @After 76 fun after() { 77 WebExtensionSupport.installedExtensions.clear() 78 } 79 80 @Test 81 fun `getAddons - queries addons from provider and updates installation state`() = runTestOnMain { 82 // Prepare addons provider 83 // addon1 (ext1) is a featured extension that is already installed. 84 // addon2 (ext2) is a featured extension that is not installed. 85 // addon3 (ext3) is a featured extension that is marked as disabled. 86 // addon4 (ext4) and addon5 (ext5) are not featured extensions but they are installed. 87 val addonsProvider: AddonsProvider = mock() 88 89 whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(3L), language = anyString())).thenReturn(listOf(Addon(id = "ext1"), Addon(id = "ext2"), Addon(id = "ext3"))) 90 91 // Prepare engine 92 val engine: Engine = mock() 93 val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>() 94 whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer { 95 callbackCaptor.value.invoke(emptyList()) 96 } 97 val store = BrowserStore( 98 BrowserState( 99 extensions = mapOf( 100 "ext1" to WebExtensionState("ext1", "url"), 101 "ext4" to WebExtensionState("ext4", "url"), 102 "ext5" to WebExtensionState("ext5", "url"), 103 // ext6 is a temporarily loaded extension. 104 "ext6" to WebExtensionState("ext6", "url"), 105 // ext7 is a built-in extension. 106 "ext7" to WebExtensionState("ext7", "url"), 107 ), 108 ), 109 ) 110 111 WebExtensionSupport.initialize(engine, store) 112 val ext1: WebExtension = mock() 113 whenever(ext1.id).thenReturn("ext1") 114 whenever(ext1.isEnabled()).thenReturn(true) 115 WebExtensionSupport.installedExtensions["ext1"] = ext1 116 117 // Make `ext3` an extension that is disabled because it wasn't supported. 118 val newlySupportedExtension: WebExtension = mock() 119 val metadata: Metadata = mock() 120 whenever(newlySupportedExtension.isEnabled()).thenReturn(false) 121 whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(APP_SUPPORT)) 122 whenever(metadata.optionsPageUrl).thenReturn("http://options-page.moz") 123 whenever(metadata.openOptionsPageInTab).thenReturn(true) 124 whenever(newlySupportedExtension.id).thenReturn("ext3") 125 whenever(newlySupportedExtension.url).thenReturn("site_url") 126 whenever(newlySupportedExtension.getMetadata()).thenReturn(metadata) 127 WebExtensionSupport.installedExtensions["ext3"] = newlySupportedExtension 128 129 val ext4: WebExtension = mock() 130 whenever(ext4.id).thenReturn("ext4") 131 whenever(ext4.isEnabled()).thenReturn(true) 132 val ext4Metadata: Metadata = mock() 133 whenever(ext4Metadata.temporary).thenReturn(false) 134 whenever(ext4.getMetadata()).thenReturn(ext4Metadata) 135 WebExtensionSupport.installedExtensions["ext4"] = ext4 136 137 val ext5: WebExtension = mock() 138 whenever(ext5.id).thenReturn("ext5") 139 whenever(ext5.isEnabled()).thenReturn(true) 140 val ext5Metadata: Metadata = mock() 141 whenever(ext5Metadata.temporary).thenReturn(false) 142 whenever(ext5.getMetadata()).thenReturn(ext5Metadata) 143 WebExtensionSupport.installedExtensions["ext5"] = ext5 144 145 val ext6: WebExtension = mock() 146 whenever(ext6.id).thenReturn("ext6") 147 whenever(ext6.url).thenReturn("some url") 148 whenever(ext6.isEnabled()).thenReturn(true) 149 val ext6Metadata: Metadata = mock() 150 whenever(ext6Metadata.name).thenReturn("temporarily loaded extension - ext6") 151 whenever(ext6Metadata.temporary).thenReturn(true) 152 whenever(ext6.getMetadata()).thenReturn(ext6Metadata) 153 WebExtensionSupport.installedExtensions["ext6"] = ext6 154 155 val ext7: WebExtension = mock() 156 whenever(ext7.id).thenReturn("ext7") 157 whenever(ext7.isEnabled()).thenReturn(true) 158 whenever(ext7.isBuiltIn()).thenReturn(true) 159 WebExtensionSupport.installedExtensions["ext7"] = ext7 160 161 // Verify add-ons were updated with state provided by the engine/store. 162 val addons = AddonManager(store, mock(), addonsProvider, mock()).getAddons() 163 assertEquals(6, addons.size) 164 165 // ext1 should be installed. 166 val addon1 = addons.find { it.id == "ext1" }!! 167 168 assertEquals("ext1", addon1.id) 169 assertNotNull(addon1.installedState) 170 assertEquals("ext1", addon1.installedState!!.id) 171 assertTrue(addon1.isEnabled()) 172 assertFalse(addon1.isDisabledAsUnsupported()) 173 assertNull(addon1.installedState.optionsPageUrl) 174 assertFalse(addon1.installedState.openOptionsPageInTab) 175 176 // ext2 should not be installed. 177 val addon2 = addons.find { it.id == "ext2" }!! 178 assertEquals("ext2", addon2.id) 179 assertNull(addon2.installedState) 180 181 // ext3 should now be marked as supported but still be disabled as unsupported. 182 val addon3 = addons.find { it.id == "ext3" }!! 183 assertEquals("ext3", addon3.id) 184 assertNotNull(addon3.installedState) 185 assertEquals("ext3", addon3.installedState!!.id) 186 assertTrue(addon3.isSupported()) 187 assertFalse(addon3.isEnabled()) 188 assertTrue(addon3.isDisabledAsUnsupported()) 189 assertEquals("http://options-page.moz", addon3.installedState.optionsPageUrl) 190 assertTrue(addon3.installedState.openOptionsPageInTab) 191 192 // ext4 should be installed. 193 val addon4 = addons.find { it.id == "ext4" }!! 194 assertEquals("ext4", addon4.id) 195 assertNotNull(addon4.installedState) 196 assertEquals("ext4", addon4.installedState!!.id) 197 assertTrue(addon4.isEnabled()) 198 assertFalse(addon4.isDisabledAsUnsupported()) 199 assertNull(addon4.installedState.optionsPageUrl) 200 assertFalse(addon4.installedState.openOptionsPageInTab) 201 202 // ext5 should be installed. 203 val addon5 = addons.find { it.id == "ext5" }!! 204 assertEquals("ext5", addon5.id) 205 assertNotNull(addon5.installedState) 206 assertEquals("ext5", addon5.installedState!!.id) 207 assertTrue(addon5.isEnabled()) 208 assertFalse(addon5.isDisabledAsUnsupported()) 209 assertNull(addon5.installedState.optionsPageUrl) 210 assertFalse(addon5.installedState.openOptionsPageInTab) 211 212 // ext6 should be installed. 213 val addon6 = addons.find { it.id == "ext6" }!! 214 assertEquals("ext6", addon6.id) 215 assertNotNull(addon6.installedState) 216 assertEquals("ext6", addon6.installedState!!.id) 217 assertTrue(addon6.isEnabled()) 218 assertFalse(addon6.isDisabledAsUnsupported()) 219 assertNull(addon6.installedState.optionsPageUrl) 220 assertFalse(addon6.installedState.openOptionsPageInTab) 221 } 222 223 @Test 224 fun `getAddons - returns temporary add-ons as supported`() = runTestOnMain { 225 val addonsProvider: AddonsProvider = mock() 226 whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(listOf()) 227 228 // Prepare engine 229 val engine: Engine = mock() 230 val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>() 231 whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer { 232 callbackCaptor.value.invoke(emptyList()) 233 } 234 235 val store = BrowserStore() 236 WebExtensionSupport.initialize(engine, store) 237 238 // Add temporary extension 239 val temporaryExtension: WebExtension = mock() 240 val temporaryExtensionIcon: Bitmap = mock() 241 val temporaryExtensionMetadata: Metadata = mock() 242 whenever(temporaryExtensionMetadata.temporary).thenReturn(true) 243 whenever(temporaryExtensionMetadata.name).thenReturn("name") 244 whenever(temporaryExtension.id).thenReturn("temp_ext") 245 whenever(temporaryExtension.url).thenReturn("site_url") 246 whenever(temporaryExtension.getMetadata()).thenReturn(temporaryExtensionMetadata) 247 WebExtensionSupport.installedExtensions["temp_ext"] = temporaryExtension 248 249 val addonManager = spy(AddonManager(store, mock(), addonsProvider, mock())) 250 251 whenever(addonManager.loadIcon(temporaryExtension)).thenReturn(temporaryExtensionIcon) 252 253 val addons = addonManager.getAddons() 254 assertEquals(1, addons.size) 255 256 // Temporary extension should be returned and marked as supported 257 assertEquals("temp_ext", addons[0].id) 258 assertEquals(1, addons[0].translatableName.size) 259 assertNotNull(addons[0].translatableName[addons[0].defaultLocale]) 260 assertTrue(addons[0].translatableName.containsValue("name")) 261 assertNotNull(addons[0].installedState) 262 assertTrue(addons[0].isSupported()) 263 assertEquals(temporaryExtensionIcon, addons[0].installedState!!.icon) 264 } 265 266 @Test 267 fun `getAddons - filters unneeded locales on featured add-ons`() = runTestOnMain { 268 val addon = Addon( 269 id = "addon1", 270 translatableName = mapOf(Addon.DEFAULT_LOCALE to "name", "invalid1" to "Name", "invalid2" to "nombre"), 271 translatableDescription = mapOf(Addon.DEFAULT_LOCALE to "description", "invalid1" to "Beschreibung", "invalid2" to "descripción"), 272 translatableSummary = mapOf(Addon.DEFAULT_LOCALE to "summary", "invalid1" to "Kurzfassung", "invalid2" to "resumen"), 273 ) 274 275 val store = BrowserStore() 276 277 val engine: Engine = mock() 278 val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>() 279 whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer { 280 callbackCaptor.value.invoke(emptyList()) 281 } 282 283 val addonsProvider: AddonsProvider = mock() 284 whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(listOf(addon)) 285 WebExtensionSupport.initialize(engine, store) 286 287 val addons = AddonManager(store, mock(), addonsProvider, mock()).getAddons() 288 assertEquals(1, addons[0].translatableName.size) 289 assertTrue(addons[0].translatableName.contains(addons[0].defaultLocale)) 290 assertEquals(1, addons[0].translatableDescription.size) 291 assertTrue(addons[0].translatableDescription.contains(addons[0].defaultLocale)) 292 assertEquals(1, addons[0].translatableSummary.size) 293 assertTrue(addons[0].translatableSummary.contains(addons[0].defaultLocale)) 294 } 295 296 @Test 297 fun `getAddons - filters unneeded locales on non-featured installed add-ons`() = runTestOnMain { 298 val addon = Addon( 299 id = "addon1", 300 translatableName = mapOf(Addon.DEFAULT_LOCALE to "name", "invalid1" to "Name", "invalid2" to "nombre"), 301 translatableDescription = mapOf(Addon.DEFAULT_LOCALE to "description", "invalid1" to "Beschreibung", "invalid2" to "descripción"), 302 translatableSummary = mapOf(Addon.DEFAULT_LOCALE to "summary", "invalid1" to "Kurzfassung", "invalid2" to "resumen"), 303 ) 304 305 val store = BrowserStore() 306 307 val engine: Engine = mock() 308 val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>() 309 whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer { 310 callbackCaptor.value.invoke(emptyList()) 311 } 312 313 val addonsProvider: AddonsProvider = mock() 314 whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(emptyList()) 315 WebExtensionSupport.initialize(engine, store) 316 val extension: WebExtension = mock() 317 whenever(extension.id).thenReturn(addon.id) 318 whenever(extension.isEnabled()).thenReturn(true) 319 whenever(extension.getMetadata()).thenReturn(mock()) 320 WebExtensionSupport.installedExtensions[addon.id] = extension 321 322 val addons = AddonManager(store, mock(), addonsProvider, mock()).getAddons() 323 assertEquals(1, addons[0].translatableName.size) 324 assertTrue(addons[0].translatableName.contains(addons[0].defaultLocale)) 325 assertEquals(1, addons[0].translatableDescription.size) 326 assertTrue(addons[0].translatableDescription.contains(addons[0].defaultLocale)) 327 assertEquals(1, addons[0].translatableSummary.size) 328 assertTrue(addons[0].translatableSummary.contains(addons[0].defaultLocale)) 329 } 330 331 @Test 332 fun `getAddons - suspends until pending actions are completed`() = runTestOnMain { 333 val addon = Addon( 334 id = "ext1", 335 installedState = Addon.InstalledState("ext1", "1.0", "", true), 336 ) 337 338 val extension: WebExtension = mock() 339 whenever(extension.id).thenReturn("ext1") 340 341 val store = BrowserStore() 342 343 val engine: Engine = mock() 344 val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>() 345 whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer { 346 callbackCaptor.value.invoke(emptyList()) 347 } 348 val addonsProvider: AddonsProvider = mock() 349 350 whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(listOf(addon)) 351 WebExtensionSupport.initialize(engine, store) 352 WebExtensionSupport.installedExtensions[addon.id] = extension 353 354 val addonManager = AddonManager(store, mock(), addonsProvider, mock(), dispatcher) 355 addonManager.installAddon(url = addon.downloadUrl) 356 addonManager.enableAddon(addon) 357 addonManager.disableAddon(addon) 358 addonManager.uninstallAddon(addon) 359 assertEquals(4, addonManager.pendingAddonActions.size) 360 361 var getAddonsResult: List<Addon>? = null 362 val nonSuspendingJob = CoroutineScope(Dispatchers.IO).launch { 363 getAddonsResult = addonManager.getAddons(waitForPendingActions = false) 364 } 365 366 nonSuspendingJob.join() 367 assertNotNull(getAddonsResult) 368 369 getAddonsResult = null 370 val suspendingJob = CoroutineScope(Dispatchers.IO).launch { 371 getAddonsResult = addonManager.getAddons(waitForPendingActions = true) 372 } 373 374 addonManager.pendingAddonActions.forEach { it.complete(Unit) } 375 376 suspendingJob.join() 377 assertNotNull(getAddonsResult) 378 } 379 380 @Test 381 fun `getAddons - passes on allowCache parameter`() = runTestOnMain { 382 val store = BrowserStore() 383 384 val engine: Engine = mock() 385 val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>() 386 whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer { 387 callbackCaptor.value.invoke(emptyList()) 388 } 389 WebExtensionSupport.initialize(engine, store) 390 391 val addonsProvider: AddonsProvider = mock() 392 whenever(addonsProvider.getFeaturedAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(emptyList()) 393 val addonsManager = AddonManager(store, mock(), addonsProvider, mock(), dispatcher) 394 395 addonsManager.getAddons() 396 verify(addonsProvider).getFeaturedAddons(eq(true), eq(null), language = anyString()) 397 398 addonsManager.getAddons(allowCache = false) 399 verify(addonsProvider).getFeaturedAddons(eq(false), eq(null), language = anyString()) 400 Unit 401 } 402 403 @Test 404 fun `getAddons - passes readTimeoutInSeconds parameter dependent on installed extensions`() = runTestOnMain { 405 val store = BrowserStore() 406 407 val engine: Engine = mock() 408 val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>() 409 whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer { 410 callbackCaptor.value.invoke(emptyList()) 411 } 412 WebExtensionSupport.initialize(engine, store) 413 414 // Built-in extensions don't count as an installed extension, as they are not displayed to the user. 415 val ext1: WebExtension = mock() 416 whenever(ext1.id).thenReturn("ext1") 417 whenever(ext1.isEnabled()).thenReturn(true) 418 whenever(ext1.isBuiltIn()).thenReturn(true) 419 WebExtensionSupport.installedExtensions["ext1"] = ext1 420 421 val addonsProvider: AddonsProvider = mock() 422 whenever(addonsProvider.getFeaturedAddons(anyBoolean(), readTimeoutInSeconds = any(), language = anyString())).thenReturn(emptyList()) 423 val addonsManager = AddonManager(store, mock(), addonsProvider, mock(), dispatcher) 424 425 addonsManager.getAddons() 426 verify(addonsProvider).getFeaturedAddons(eq(true), readTimeoutInSeconds = eq(null), language = anyString()) 427 // ^ readTimeoutInSeconds is null, so the default is used. 428 429 // This counts as an installed extension 430 val ext2: WebExtension = mock() 431 whenever(ext2.id).thenReturn("ext2") 432 whenever(ext2.isEnabled()).thenReturn(true) 433 whenever(ext2.isBuiltIn()).thenReturn(false) 434 WebExtensionSupport.installedExtensions["ext2"] = ext2 435 436 addonsManager.getAddons() 437 verify(addonsProvider).getFeaturedAddons(eq(true), readTimeoutInSeconds = eq(3L), language = anyString()) 438 // ^ readTimeoutInSeconds is now minimal to ensure quick loading (bug 1949963). 439 Unit 440 } 441 442 @Test 443 fun `updateAddon - when a extension is updated successfully`() { 444 val engine: Engine = mock() 445 val engineSession: EngineSession = mock() 446 val store = spy( 447 BrowserStore( 448 BrowserState( 449 tabs = listOf( 450 createTab(id = "1", url = "https://www.mozilla.org", engineSession = engineSession), 451 ), 452 extensions = mapOf("extensionId" to mock()), 453 ), 454 ), 455 ) 456 val onSuccessCaptor = argumentCaptor<((WebExtension?) -> Unit)>() 457 var updateStatus: Status? = null 458 val manager = AddonManager(store, engine, mock(), mock(), mock()) 459 460 val updatedExt: WebExtension = mock() 461 whenever(updatedExt.id).thenReturn("extensionId") 462 whenever(updatedExt.url).thenReturn("url") 463 whenever(updatedExt.supportActions).thenReturn(true) 464 465 WebExtensionSupport.installedExtensions["extensionId"] = mock() 466 467 val oldExt = WebExtensionSupport.installedExtensions["extensionId"] 468 469 manager.updateAddon("extensionId") { status -> 470 updateStatus = status 471 } 472 473 val actionHandlerCaptor = argumentCaptor<ActionHandler>() 474 val actionCaptor = argumentCaptor<WebExtensionAction.UpdateWebExtensionAction>() 475 476 // Verifying we returned the right status 477 verify(engine).updateWebExtension(any(), onSuccessCaptor.capture(), any()) 478 onSuccessCaptor.value.invoke(updatedExt) 479 assertEquals(Status.SuccessfullyUpdated, updateStatus) 480 481 // Verifying we updated the extension in WebExtensionSupport 482 assertNotEquals(oldExt, WebExtensionSupport.installedExtensions["extensionId"]) 483 assertEquals(updatedExt, WebExtensionSupport.installedExtensions["extensionId"]) 484 485 // Verifying we updated the extension in the store 486 verify(store).dispatch(actionCaptor.capture()) 487 assertEquals( 488 WebExtensionState(updatedExt.id, updatedExt.url, updatedExt.getMetadata()?.name, updatedExt.isEnabled()), 489 actionCaptor.allValues.last().updatedExtension, 490 ) 491 492 // Verify that we registered an action handler for all existing sessions on the extension 493 verify(updatedExt).registerActionHandler(eq(engineSession), actionHandlerCaptor.capture()) 494 actionHandlerCaptor.value.onBrowserAction(updatedExt, engineSession, mock()) 495 } 496 497 @Test 498 fun `updateAddon - when extension is not installed`() { 499 var updateStatus: Status? = null 500 501 val manager = AddonManager(mock(), mock(), mock(), mock(), mock()) 502 503 manager.updateAddon("extensionId") { status -> 504 updateStatus = status 505 } 506 507 assertEquals(Status.NotInstalled, updateStatus) 508 } 509 510 @Test 511 fun `updateAddon - when extension is not supported`() { 512 var updateStatus: Status? = null 513 514 val extension: WebExtension = mock() 515 whenever(extension.id).thenReturn("unsupportedExt") 516 517 val metadata: Metadata = mock() 518 whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(APP_SUPPORT)) 519 whenever(extension.getMetadata()).thenReturn(metadata) 520 521 WebExtensionSupport.installedExtensions["extensionId"] = extension 522 523 val manager = AddonManager(mock(), mock(), mock(), mock(), mock()) 524 manager.updateAddon("extensionId") { status -> 525 updateStatus = status 526 } 527 528 assertEquals(Status.NotInstalled, updateStatus) 529 } 530 531 @Test 532 fun `updateAddon - when an error happens while updating`() { 533 val engine: Engine = mock() 534 val onErrorCaptor = argumentCaptor<((String, Throwable) -> Unit)>() 535 var updateStatus: Status? = null 536 val manager = AddonManager(mock(), engine, mock(), mock(), mock()) 537 538 WebExtensionSupport.installedExtensions["extensionId"] = mock() 539 540 manager.updateAddon("extensionId") { status -> 541 updateStatus = status 542 } 543 544 // Verifying we returned the right status 545 verify(engine).updateWebExtension(any(), any(), onErrorCaptor.capture()) 546 onErrorCaptor.value.invoke("message", Exception()) 547 assertTrue(updateStatus is Status.Error) 548 } 549 550 @Test 551 fun `updateAddon - when there is not new updates for the extension`() { 552 val engine: Engine = mock() 553 val onSuccessCaptor = argumentCaptor<((WebExtension?) -> Unit)>() 554 var updateStatus: Status? = null 555 val manager = AddonManager(mock(), engine, mock(), mock(), mock()) 556 557 WebExtensionSupport.installedExtensions["extensionId"] = mock() 558 manager.updateAddon("extensionId") { status -> 559 updateStatus = status 560 } 561 562 verify(engine).updateWebExtension(any(), onSuccessCaptor.capture(), any()) 563 onSuccessCaptor.value.invoke(null) 564 assertEquals(Status.NoUpdateAvailable, updateStatus) 565 } 566 567 @Test 568 fun `installAddon successfully`() { 569 val addon = Addon(id = "ext1") 570 val engine: Engine = mock() 571 val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>() 572 573 var installedAddon: Addon? = null 574 val manager = AddonManager(mock(), engine, mock(), mock(), mock()) 575 manager.installAddon( 576 url = addon.downloadUrl, 577 installationMethod = InstallationMethod.MANAGER, 578 onSuccess = { 579 installedAddon = it 580 }, 581 ) 582 583 verify(engine).installWebExtension( 584 any(), 585 eq(InstallationMethod.MANAGER), 586 onSuccessCaptor.capture(), 587 any(), 588 ) 589 590 val metadata: Metadata = mock() 591 val extension: WebExtension = mock() 592 whenever(metadata.name).thenReturn("nameFromMetadata") 593 whenever(extension.id).thenReturn("ext1") 594 whenever(extension.getMetadata()).thenReturn(metadata) 595 onSuccessCaptor.value.invoke(extension) 596 assertNotNull(installedAddon) 597 assertEquals(addon.id, installedAddon!!.id) 598 assertEquals("nameFromMetadata", installedAddon.translateName(testContext)) 599 assertTrue(manager.pendingAddonActions.isEmpty()) 600 } 601 602 @Test 603 fun `installAddon failure`() { 604 val addon = Addon(id = "ext1") 605 val engine: Engine = mock() 606 val onErrorCaptor = argumentCaptor<((Throwable) -> Unit)>() 607 608 var throwable: Throwable? = null 609 val manager = AddonManager(mock(), engine, mock(), mock(), mock()) 610 manager.installAddon( 611 url = addon.downloadUrl, 612 installationMethod = InstallationMethod.FROM_FILE, 613 onError = { caught -> 614 throwable = caught 615 }, 616 ) 617 618 verify(engine).installWebExtension( 619 url = any(), 620 installationMethod = eq(InstallationMethod.FROM_FILE), 621 onSuccess = any(), 622 onError = onErrorCaptor.capture(), 623 ) 624 625 onErrorCaptor.value.invoke(IllegalStateException("test")) 626 assertNotNull(throwable!!) 627 assertTrue(manager.pendingAddonActions.isEmpty()) 628 } 629 630 @Test 631 fun `uninstallAddon successfully`() { 632 val installedAddon = Addon( 633 id = "ext1", 634 installedState = Addon.InstalledState("ext1", "1.0", "", true), 635 ) 636 637 val extension: WebExtension = mock() 638 whenever(extension.id).thenReturn("ext1") 639 WebExtensionSupport.installedExtensions[installedAddon.id] = extension 640 641 val engine: Engine = mock() 642 val onSuccessCaptor = argumentCaptor<(() -> Unit)>() 643 644 var successCallbackInvoked = false 645 val manager = AddonManager(mock(), engine, mock(), mock(), mock()) 646 manager.uninstallAddon( 647 installedAddon, 648 onSuccess = { 649 successCallbackInvoked = true 650 }, 651 ) 652 verify(engine).uninstallWebExtension(eq(extension), onSuccessCaptor.capture(), any()) 653 654 onSuccessCaptor.value.invoke() 655 assertTrue(successCallbackInvoked) 656 assertTrue(manager.pendingAddonActions.isEmpty()) 657 } 658 659 @Test 660 fun `uninstallAddon failure cases`() { 661 val addon = Addon(id = "ext1") 662 val engine: Engine = mock() 663 val onErrorCaptor = argumentCaptor<((String, Throwable) -> Unit)>() 664 var throwable: Throwable? = null 665 var msg: String? = null 666 val errorCallback = { errorMsg: String, caught: Throwable -> 667 throwable = caught 668 msg = errorMsg 669 } 670 val manager = AddonManager(mock(), engine, mock(), mock(), mock()) 671 672 // Extension is not installed so we're invoking the error callback and never the engine 673 manager.uninstallAddon(addon, onError = errorCallback) 674 verify(engine, never()).uninstallWebExtension(any(), any(), onErrorCaptor.capture()) 675 assertNotNull(throwable!!) 676 assertEquals("Addon is not installed", throwable.localizedMessage) 677 678 // Install extension and try again 679 val extension: WebExtension = mock() 680 whenever(extension.id).thenReturn("ext1") 681 WebExtensionSupport.installedExtensions[addon.id] = extension 682 manager.uninstallAddon(addon, onError = errorCallback) 683 verify(engine, never()).uninstallWebExtension(any(), any(), onErrorCaptor.capture()) 684 685 // Make sure engine error is forwarded to caller 686 val installedAddon = addon.copy(installedState = Addon.InstalledState(addon.id, "1.0", "", true)) 687 manager.uninstallAddon(installedAddon, onError = errorCallback) 688 verify(engine).uninstallWebExtension(eq(extension), any(), onErrorCaptor.capture()) 689 onErrorCaptor.value.invoke(addon.id, IllegalStateException("test")) 690 assertNotNull(throwable) 691 assertEquals("test", throwable.localizedMessage) 692 assertEquals(msg, addon.id) 693 assertTrue(manager.pendingAddonActions.isEmpty()) 694 } 695 696 @Test 697 fun `add optional permissions successfully`() { 698 val permission = listOf("permission1") 699 val origin = listOf("origin") 700 val addon = Addon( 701 id = "ext1", 702 installedState = Addon.InstalledState("ext1", "1.0", "", true), 703 ) 704 705 val extension: WebExtension = mock() 706 whenever(extension.id).thenReturn("ext1") 707 WebExtensionSupport.installedExtensions[addon.id] = extension 708 val metadata: Metadata = mock() 709 whenever(extension.getMetadata()).thenReturn(metadata) 710 whenever(metadata.optionalPermissions).thenReturn(permission) 711 whenever(metadata.grantedOptionalPermissions).thenReturn(permission) 712 whenever(metadata.optionalOrigins).thenReturn(origin) 713 whenever(metadata.grantedOptionalOrigins).thenReturn(origin) 714 val engine: Engine = mock() 715 val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>() 716 717 var updateAddon: Addon? = null 718 val manager = AddonManager(mock(), engine, mock(), mock(), mock()) 719 manager.addOptionalPermission( 720 addon, 721 permission, 722 origin, 723 onSuccess = { 724 updateAddon = it 725 }, 726 ) 727 728 verify(engine).addOptionalPermissions(eq(extension.id), any(), any(), any(), onSuccessCaptor.capture(), any()) 729 onSuccessCaptor.value.invoke(extension) 730 assertNotNull(updateAddon) 731 assertEquals(addon.id, updateAddon!!.id) 732 assertEquals("permission1", updateAddon.optionalPermissions.first().name) 733 assertEquals(true, updateAddon.optionalPermissions.first().granted) 734 assertEquals("origin", updateAddon.optionalOrigins.first().name) 735 assertEquals(true, updateAddon.optionalOrigins.first().granted) 736 assertTrue(manager.pendingAddonActions.isEmpty()) 737 } 738 739 @Test 740 fun `add optional with empty permissions and origins`() { 741 var onErrorWasExecuted = false 742 val manager = AddonManager(mock(), mock(), mock(), mock(), mock()) 743 744 manager.addOptionalPermission( 745 mock(), 746 emptyList(), 747 emptyList(), 748 onError = { 749 onErrorWasExecuted = true 750 }, 751 ) 752 753 assertTrue(onErrorWasExecuted) 754 } 755 756 @Test 757 fun `remove optional permissions successfully`() { 758 val permission = listOf("permission1") 759 val origins = listOf("origin") 760 val addon = Addon( 761 id = "ext1", 762 installedState = Addon.InstalledState("ext1", "1.0", "", true), 763 ) 764 765 val extension: WebExtension = mock() 766 whenever(extension.id).thenReturn("ext1") 767 WebExtensionSupport.installedExtensions[addon.id] = extension 768 769 val engine: Engine = mock() 770 val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>() 771 772 var updateAddon: Addon? = null 773 val manager = AddonManager(mock(), engine, mock(), mock(), mock()) 774 manager.removeOptionalPermission( 775 addon, 776 permission, 777 origins, 778 onSuccess = { 779 updateAddon = it 780 }, 781 ) 782 783 verify(engine).removeOptionalPermissions(eq(extension.id), any(), any(), any(), onSuccessCaptor.capture(), any()) 784 onSuccessCaptor.value.invoke(extension) 785 assertNotNull(updateAddon) 786 assertEquals(addon.id, updateAddon!!.id) 787 assertTrue(manager.pendingAddonActions.isEmpty()) 788 } 789 790 @Test 791 fun `remove optional with empty permissions and origins`() { 792 var onErrorWasExecuted = false 793 val manager = AddonManager(mock(), mock(), mock(), mock(), mock()) 794 795 manager.removeOptionalPermission( 796 mock(), 797 emptyList(), 798 emptyList(), 799 onError = { 800 onErrorWasExecuted = true 801 }, 802 ) 803 804 assertTrue(onErrorWasExecuted) 805 } 806 807 @Test 808 fun `enableAddon successfully`() { 809 val addon = Addon( 810 id = "ext1", 811 installedState = Addon.InstalledState("ext1", "1.0", "", true), 812 ) 813 814 val extension: WebExtension = mock() 815 whenever(extension.id).thenReturn("ext1") 816 WebExtensionSupport.installedExtensions[addon.id] = extension 817 818 val engine: Engine = mock() 819 val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>() 820 821 var enabledAddon: Addon? = null 822 val manager = AddonManager(mock(), engine, mock(), mock(), mock()) 823 manager.enableAddon( 824 addon, 825 onSuccess = { 826 enabledAddon = it 827 }, 828 ) 829 830 verify(engine).enableWebExtension(eq(extension), any(), onSuccessCaptor.capture(), any()) 831 onSuccessCaptor.value.invoke(extension) 832 assertNotNull(enabledAddon) 833 assertEquals(addon.id, enabledAddon!!.id) 834 assertTrue(manager.pendingAddonActions.isEmpty()) 835 } 836 837 @Test 838 fun `enableAddon failure cases`() { 839 val addon = Addon(id = "ext1") 840 val engine: Engine = mock() 841 val onErrorCaptor = argumentCaptor<((Throwable) -> Unit)>() 842 var throwable: Throwable? = null 843 val errorCallback = { caught: Throwable -> 844 throwable = caught 845 } 846 val manager = AddonManager(mock(), engine, mock(), mock(), mock()) 847 848 // Extension is not installed so we're invoking the error callback and never the engine 849 manager.enableAddon(addon, onError = errorCallback) 850 verify(engine, never()).enableWebExtension(any(), any(), any(), onErrorCaptor.capture()) 851 assertNotNull(throwable!!) 852 assertEquals("Addon is not installed", throwable.localizedMessage) 853 854 // Install extension and try again 855 val extension: WebExtension = mock() 856 whenever(extension.id).thenReturn("ext1") 857 WebExtensionSupport.installedExtensions[addon.id] = extension 858 manager.enableAddon(addon, onError = errorCallback) 859 verify(engine, never()).enableWebExtension(any(), any(), any(), onErrorCaptor.capture()) 860 861 // Make sure engine error is forwarded to caller 862 val installedAddon = addon.copy(installedState = Addon.InstalledState(addon.id, "1.0", "", true)) 863 manager.enableAddon(installedAddon, source = EnableSource.APP_SUPPORT, onError = errorCallback) 864 verify(engine).enableWebExtension(eq(extension), eq(EnableSource.APP_SUPPORT), any(), onErrorCaptor.capture()) 865 onErrorCaptor.value.invoke(IllegalStateException("test")) 866 assertNotNull(throwable) 867 assertEquals("test", throwable.localizedMessage) 868 assertTrue(manager.pendingAddonActions.isEmpty()) 869 } 870 871 @Test 872 fun `disableAddon successfully`() { 873 val addon = Addon( 874 id = "ext1", 875 installedState = Addon.InstalledState("ext1", "1.0", "", true), 876 ) 877 878 val extension: WebExtension = mock() 879 whenever(extension.id).thenReturn("ext1") 880 WebExtensionSupport.installedExtensions[addon.id] = extension 881 882 val engine: Engine = mock() 883 val onSuccessCaptor = argumentCaptor<((WebExtension) -> Unit)>() 884 885 var disabledAddon: Addon? = null 886 val manager = AddonManager(mock(), engine, mock(), mock(), mock()) 887 manager.disableAddon( 888 addon, 889 source = EnableSource.APP_SUPPORT, 890 onSuccess = { 891 disabledAddon = it 892 }, 893 ) 894 895 verify(engine).disableWebExtension(eq(extension), eq(EnableSource.APP_SUPPORT), onSuccessCaptor.capture(), any()) 896 onSuccessCaptor.value.invoke(extension) 897 assertNotNull(disabledAddon) 898 assertEquals(addon.id, disabledAddon!!.id) 899 assertTrue(manager.pendingAddonActions.isEmpty()) 900 } 901 902 @Test 903 fun `disableAddon failure cases`() { 904 val addon = Addon(id = "ext1") 905 val engine: Engine = mock() 906 val onErrorCaptor = argumentCaptor<((Throwable) -> Unit)>() 907 var throwable: Throwable? = null 908 val errorCallback = { caught: Throwable -> 909 throwable = caught 910 } 911 val manager = AddonManager(mock(), engine, mock(), mock(), mock()) 912 913 // Extension is not installed so we're invoking the error callback and never the engine 914 manager.disableAddon(addon, onError = errorCallback) 915 verify(engine, never()).disableWebExtension(any(), any(), any(), onErrorCaptor.capture()) 916 assertNotNull(throwable!!) 917 assertEquals("Addon is not installed", throwable.localizedMessage) 918 919 // Install extension and try again 920 val extension: WebExtension = mock() 921 whenever(extension.id).thenReturn("ext1") 922 WebExtensionSupport.installedExtensions[addon.id] = extension 923 manager.disableAddon(addon, onError = errorCallback) 924 verify(engine, never()).disableWebExtension(any(), any(), any(), onErrorCaptor.capture()) 925 926 // Make sure engine error is forwarded to caller 927 val installedAddon = addon.copy(installedState = Addon.InstalledState(addon.id, "1.0", "", true)) 928 manager.disableAddon(installedAddon, onError = errorCallback) 929 verify(engine).disableWebExtension(eq(extension), any(), any(), onErrorCaptor.capture()) 930 onErrorCaptor.value.invoke(IllegalStateException("test")) 931 assertNotNull(throwable) 932 assertEquals("test", throwable.localizedMessage) 933 assertTrue(manager.pendingAddonActions.isEmpty()) 934 } 935 936 @Test 937 fun `toInstalledState read from icon cache`() { 938 val extension: WebExtension = mock() 939 val metadata: Metadata = mock() 940 941 val manager = spy(AddonManager(mock(), mock(), mock(), mock(), mock())) 942 943 manager.iconsCache["ext1"] = mock() 944 whenever(extension.id).thenReturn("ext1") 945 whenever(extension.getMetadata()).thenReturn(metadata) 946 whenever(extension.isEnabled()).thenReturn(true) 947 whenever(extension.getDisabledReason()).thenReturn(null) 948 whenever(extension.isAllowedInPrivateBrowsing()).thenReturn(true) 949 whenever(metadata.version).thenReturn("version") 950 whenever(metadata.optionsPageUrl).thenReturn("optionsPageUrl") 951 whenever(metadata.openOptionsPageInTab).thenReturn(true) 952 953 val installedExtension = manager.toInstalledState(extension) 954 955 assertEquals(manager.iconsCache["ext1"], installedExtension.icon) 956 assertEquals("version", installedExtension.version) 957 assertEquals("optionsPageUrl", installedExtension.optionsPageUrl) 958 assertNull(installedExtension.disabledReason) 959 assertTrue(installedExtension.openOptionsPageInTab) 960 assertTrue(installedExtension.enabled) 961 assertTrue(installedExtension.allowedInPrivateBrowsing) 962 963 verify(manager, times(0)).loadIcon(eq(extension)) 964 } 965 966 @Test 967 fun `toInstalledState load icon when cache is not available`() { 968 val extension: WebExtension = mock() 969 val metadata: Metadata = mock() 970 971 val manager = spy(AddonManager(mock(), mock(), mock(), mock(), mock())) 972 973 whenever(extension.id).thenReturn("ext1") 974 whenever(extension.getMetadata()).thenReturn(metadata) 975 whenever(extension.isEnabled()).thenReturn(true) 976 whenever(extension.getDisabledReason()).thenReturn(null) 977 whenever(extension.isAllowedInPrivateBrowsing()).thenReturn(true) 978 whenever(metadata.version).thenReturn("version") 979 whenever(metadata.optionsPageUrl).thenReturn("optionsPageUrl") 980 whenever(metadata.openOptionsPageInTab).thenReturn(true) 981 982 val installedExtension = manager.toInstalledState(extension) 983 984 assertEquals(manager.iconsCache["ext1"], installedExtension.icon) 985 assertEquals("version", installedExtension.version) 986 assertEquals("optionsPageUrl", installedExtension.optionsPageUrl) 987 assertNull(installedExtension.disabledReason) 988 assertTrue(installedExtension.openOptionsPageInTab) 989 assertTrue(installedExtension.enabled) 990 assertTrue(installedExtension.allowedInPrivateBrowsing) 991 992 verify(manager).loadIcon(extension) 993 } 994 995 @Test 996 fun `loadIcon try to load the icon from extension`() = runTestOnMain { 997 val extension: WebExtension = mock() 998 999 val manager = spy(AddonManager(mock(), mock(), mock(), mock(), mock())) 1000 1001 whenever(extension.loadIcon(AddonManager.ADDON_ICON_SIZE)).thenReturn(mock()) 1002 1003 val icon = manager.loadIcon(extension) 1004 1005 assertNotNull(icon) 1006 } 1007 1008 @Test 1009 fun `loadIcon calls tryLoadIconInBackground when TimeoutCancellationException`() = 1010 runTestOnMain { 1011 val extension: WebExtension = mock() 1012 1013 val manager = spy(AddonManager(mock(), mock(), mock(), mock(), mock())) 1014 doNothing().`when`(manager).tryLoadIconInBackground(extension) 1015 1016 doThrow(mock<TimeoutCancellationException>()).`when`(extension) 1017 .loadIcon(AddonManager.ADDON_ICON_SIZE) 1018 1019 val icon = manager.loadIcon(extension) 1020 1021 assertNull(icon) 1022 verify(manager).loadIcon(extension) 1023 } 1024 1025 @Test 1026 fun `getDisabledReason cases`() { 1027 val extension: WebExtension = mock() 1028 val metadata: Metadata = mock() 1029 whenever(extension.getMetadata()).thenReturn(metadata) 1030 1031 whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(BLOCKLIST)) 1032 assertEquals(Addon.DisabledReason.BLOCKLISTED, extension.getDisabledReason()) 1033 1034 whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(APP_SUPPORT)) 1035 assertEquals(Addon.DisabledReason.UNSUPPORTED, extension.getDisabledReason()) 1036 1037 whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(USER)) 1038 assertEquals(Addon.DisabledReason.USER_REQUESTED, extension.getDisabledReason()) 1039 1040 whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(SIGNATURE)) 1041 assertEquals(Addon.DisabledReason.NOT_CORRECTLY_SIGNED, extension.getDisabledReason()) 1042 1043 whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(APP_VERSION)) 1044 assertEquals(Addon.DisabledReason.INCOMPATIBLE, extension.getDisabledReason()) 1045 1046 whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(SOFT_BLOCKLIST)) 1047 assertEquals(Addon.DisabledReason.SOFT_BLOCKED, extension.getDisabledReason()) 1048 1049 whenever(metadata.disabledFlags).thenReturn(DisabledFlags.select(0)) 1050 assertNull(extension.getDisabledReason()) 1051 } 1052 }