diff --git a/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/DefaultTopSitesStorage.kt b/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/DefaultTopSitesStorage.kt index c742e8436e9..daab81d4c7c 100644 --- a/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/DefaultTopSitesStorage.kt +++ b/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/DefaultTopSitesStorage.kt @@ -4,6 +4,7 @@ package mozilla.components.feature.top.sites +import android.content.Context import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -20,15 +21,17 @@ import kotlin.coroutines.CoroutineContext /** * Default implementation of [TopSitesStorage]. * - * @param pinnedSitesStorage An instance of [PinnedSiteStorage], used for storing pinned sites. - * @param historyStorage An instance of [PlacesHistoryStorage], used for retrieving top frecent + * @property context A reference to the application context. + * @property pinnedSitesStorage An instance of [PinnedSiteStorage], used for storing pinned sites. + * @property historyStorage An instance of [PlacesHistoryStorage], used for retrieving top frecent * sites from history. - * @param topSitesProvider An optional instance of [TopSitesProvider], used for retrieving + * @property topSitesProvider An optional instance of [TopSitesProvider], used for retrieving * additional top sites from a provider. The returned top sites are added before pinned sites. - * @param defaultTopSites A list containing a title to url pair of default top sites to be added + * @property defaultTopSites A list containing a title to url pair of default top sites to be added * to the [PinnedSiteStorage]. */ class DefaultTopSitesStorage( + private val context: Context, private val pinnedSitesStorage: PinnedSiteStorage, private val historyStorage: PlacesHistoryStorage, private val topSitesProvider: TopSitesProvider? = null, @@ -100,7 +103,7 @@ class DefaultTopSitesStorage( ) { try { val providerTopSites = topSitesProvider - .getTopSites(allowCache = true) + .getTopSites(context, allowCache = true) .take(numSitesRequired) topSites.addAll(providerTopSites) numSitesRequired -= providerTopSites.size diff --git a/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesProvider.kt b/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesProvider.kt index 4ac82009016..387cc7d67e2 100644 --- a/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesProvider.kt +++ b/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesProvider.kt @@ -4,6 +4,8 @@ package mozilla.components.feature.top.sites +import android.content.Context + /** * A contract that indicates how a top sites provider must behave. */ @@ -16,5 +18,5 @@ interface TopSitesProvider { * cached response. * @return a list of top sites from the provider. */ - suspend fun getTopSites(allowCache: Boolean = true): List + suspend fun getTopSites(context: Context, allowCache: Boolean = true): List } diff --git a/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/DefaultTopSitesStorageTest.kt b/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/DefaultTopSitesStorageTest.kt index 87aa180ea24..7041d11f121 100644 --- a/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/DefaultTopSitesStorageTest.kt +++ b/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/DefaultTopSitesStorageTest.kt @@ -13,6 +13,7 @@ import mozilla.components.concept.storage.TopFrecentSiteInfo import mozilla.components.feature.top.sites.ext.toTopSite 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.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -38,6 +39,7 @@ class DefaultTopSitesStorageTest { ) DefaultTopSitesStorage( + context = testContext, pinnedSitesStorage = pinnedSitesStorage, historyStorage = historyStorage, defaultTopSites = defaultTopSites, @@ -50,6 +52,7 @@ class DefaultTopSitesStorageTest { @Test fun `addPinnedSite`() = runBlockingTest { val defaultTopSitesStorage = DefaultTopSitesStorage( + context = testContext, pinnedSitesStorage = pinnedSitesStorage, historyStorage = historyStorage, defaultTopSites = listOf(), @@ -68,6 +71,7 @@ class DefaultTopSitesStorageTest { @Test fun `removeTopSite`() = runBlockingTest { val defaultTopSitesStorage = DefaultTopSitesStorage( + context = testContext, pinnedSitesStorage = pinnedSitesStorage, historyStorage = historyStorage, defaultTopSites = listOf(), @@ -113,6 +117,7 @@ class DefaultTopSitesStorageTest { @Test fun `updateTopSite`() = runBlockingTest { val defaultTopSitesStorage = DefaultTopSitesStorage( + context = testContext, pinnedSitesStorage = pinnedSitesStorage, historyStorage = historyStorage, defaultTopSites = listOf(), @@ -156,6 +161,7 @@ class DefaultTopSitesStorageTest { @Test fun `GIVEN frecencyConfig and providerConfig are null WHEN getTopSites is called THEN only default and pinned sites are returned`() = runBlockingTest { val defaultTopSitesStorage = DefaultTopSitesStorage( + context = testContext, pinnedSitesStorage = pinnedSitesStorage, historyStorage = historyStorage, defaultTopSites = listOf(), @@ -212,6 +218,7 @@ class DefaultTopSitesStorageTest { @Test fun `GIVEN providerConfig is specified WHEN getTopSites is called THEN default, pinned and provided top sites are returned`() = runBlockingTest { val defaultTopSitesStorage = DefaultTopSitesStorage( + context = testContext, pinnedSitesStorage = pinnedSitesStorage, historyStorage = historyStorage, topSitesProvider = topSitesProvider, @@ -247,7 +254,7 @@ class DefaultTopSitesStorageTest { pinnedSite ) ) - whenever(topSitesProvider.getTopSites()).thenReturn(listOf(providedSite)) + whenever(topSitesProvider.getTopSites(testContext)).thenReturn(listOf(providedSite)) var topSites = defaultTopSitesStorage.getTopSites(totalSites = 0) @@ -333,6 +340,7 @@ class DefaultTopSitesStorageTest { @Test fun `GIVEN frecencyConfig and providerConfig are specified WHEN getTopSites is called THEN default, pinned, provided and frecent top sites are returned`() = runBlockingTest { val defaultTopSitesStorage = DefaultTopSitesStorage( + context = testContext, pinnedSitesStorage = pinnedSitesStorage, historyStorage = historyStorage, topSitesProvider = topSitesProvider, @@ -368,7 +376,7 @@ class DefaultTopSitesStorageTest { pinnedSite ) ) - whenever(topSitesProvider.getTopSites()).thenReturn(listOf(providedSite)) + whenever(topSitesProvider.getTopSites(testContext)).thenReturn(listOf(providedSite)) val frecentSite1 = TopFrecentSiteInfo("https://mozilla.com", "Mozilla") whenever(historyStorage.getTopFrecentSites(anyInt(), any())).thenReturn(listOf(frecentSite1)) @@ -441,6 +449,7 @@ class DefaultTopSitesStorageTest { @Test fun `getTopSites returns pinned and frecent sites when frecencyConfig is specified`() = runBlockingTest { val defaultTopSitesStorage = DefaultTopSitesStorage( + context = testContext, pinnedSitesStorage = pinnedSitesStorage, historyStorage = historyStorage, defaultTopSites = listOf(), @@ -558,6 +567,7 @@ class DefaultTopSitesStorageTest { @Test fun `getTopSites filters out frecent sites that already exist in pinned sites`() = runBlockingTest { val defaultTopSitesStorage = DefaultTopSitesStorage( + context = testContext, pinnedSitesStorage = pinnedSitesStorage, historyStorage = historyStorage, defaultTopSites = listOf(), diff --git a/components/service/contile/build.gradle b/components/service/contile/build.gradle index fdbd3746a16..7c8d3c4af74 100644 --- a/components/service/contile/build.gradle +++ b/components/service/contile/build.gradle @@ -24,6 +24,7 @@ android { dependencies { implementation Dependencies.kotlin_stdlib implementation Dependencies.kotlin_coroutines + implementation Dependencies.androidx_work_runtime implementation project(':concept-fetch') implementation project(':support-ktx') @@ -32,6 +33,7 @@ dependencies { testImplementation Dependencies.androidx_test_core testImplementation Dependencies.androidx_test_junit + testImplementation Dependencies.androidx_work_testing testImplementation Dependencies.testing_robolectric testImplementation Dependencies.testing_mockito diff --git a/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesProvider.kt b/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesProvider.kt index 53b282f67c4..4479226b3ed 100644 --- a/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesProvider.kt +++ b/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesProvider.kt @@ -27,16 +27,14 @@ internal const val CACHE_FILE_NAME = "mozilla_components_service_contile.json" internal const val MINUTE_IN_MS = 60 * 1000 /** - * Provide access to the Contile services API. + * Provides access to the Contile services API. * - * @property context A reference to the application context. * @property client [Client] used for interacting with the Contile HTTP API. * @property endPointURL The url of the endpoint to fetch from. Defaults to [CONTILE_ENDPOINT_URL]. * @property maxCacheAgeInMinutes Maximum time (in minutes) the cache should remain valid * before a refresh is attempted. Defaults to -1, meaning no cache is being used by default. */ class ContileTopSitesProvider( - private val context: Context, private val client: Client, private val endPointURL: String = CONTILE_ENDPOINT_URL, private val maxCacheAgeInMinutes: Long = -1 @@ -51,15 +49,16 @@ class ContileTopSitesProvider( * Returns a cached response if [allowCache] is true and the cache is not expired * (@see [maxCacheAgeInMinutes]). * + * @param context A reference to the application context. * @param allowCache Whether or not the result may be provided from a previously cached * response. Note that a [maxCacheAgeInMinutes] must be provided in order for the cache to be * active. * @throws IOException if the request failed to fetch any top sites. */ @Throws(IOException::class) - override suspend fun getTopSites(allowCache: Boolean): List { - val cachedTopSites = if (allowCache && !isCacheExpired()) { - readFromDiskCache() + override suspend fun getTopSites(context: Context, allowCache: Boolean): List { + val cachedTopSites = if (allowCache && !isCacheExpired(context)) { + readFromDiskCache(context) } else { null } @@ -69,14 +68,14 @@ class ContileTopSitesProvider( } return try { - fetchTopSites() + fetchTopSites(context) } catch (e: IOException) { logger.error("Failed to fetch contile top sites", e) throw e } } - private fun fetchTopSites(): List { + private fun fetchTopSites(context: Context): List { client.fetch( Request(url = endPointURL) ).use { response -> @@ -86,7 +85,7 @@ class ContileTopSitesProvider( return try { JSONObject(responseBody).getTopSites().also { if (maxCacheAgeInMinutes > 0) { - writeToDiskCache(responseBody) + writeToDiskCache(context, responseBody) } } } catch (e: JSONException) { @@ -102,35 +101,35 @@ class ContileTopSitesProvider( } @VisibleForTesting - internal fun readFromDiskCache(): List? { + internal fun readFromDiskCache(context: Context): List? { synchronized(diskCacheLock) { - return getCacheFile().readAndDeserialize { + return getCacheFile(context).readAndDeserialize { JSONObject(it).getTopSites() } } } @VisibleForTesting - internal fun writeToDiskCache(responseBody: String) { + internal fun writeToDiskCache(context: Context, responseBody: String) { synchronized(diskCacheLock) { - getCacheFile().writeString { responseBody } + getCacheFile(context).writeString { responseBody } } } @VisibleForTesting - internal fun isCacheExpired() = - getCacheLastModified() < Date().time - maxCacheAgeInMinutes * MINUTE_IN_MS + internal fun isCacheExpired(context: Context) = + getCacheLastModified(context) < Date().time - maxCacheAgeInMinutes * MINUTE_IN_MS @VisibleForTesting - internal fun getCacheLastModified(): Long { - val file = getBaseCacheFile() + internal fun getCacheLastModified(context: Context): Long { + val file = getBaseCacheFile(context) return if (file.exists()) file.lastModified() else -1 } - private fun getCacheFile(): AtomicFile = AtomicFile(getBaseCacheFile()) + private fun getCacheFile(context: Context): AtomicFile = AtomicFile(getBaseCacheFile(context)) @VisibleForTesting - internal fun getBaseCacheFile(): File = File(context.filesDir, CACHE_FILE_NAME) + internal fun getBaseCacheFile(context: Context): File = File(context.filesDir, CACHE_FILE_NAME) } internal fun JSONObject.getTopSites(): List = diff --git a/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUpdater.kt b/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUpdater.kt new file mode 100644 index 00000000000..ec9f39f317f --- /dev/null +++ b/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUpdater.kt @@ -0,0 +1,91 @@ +/* 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.service.contile + +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import mozilla.components.support.base.log.logger.Logger +import java.util.concurrent.TimeUnit + +/** + * Provides functionality to schedule updates of Contile top sites. + * + * @property context A reference to the application context. + * @property provider An instance of [ContileTopSitesProvider] which provides access to the Contile + * services API for fetching top sites. + * @property frequency Optional [Frequency] that specifies how often the Contile top site updates + * should happen. + */ +class ContileTopSitesUpdater( + private val context: Context, + private val provider: ContileTopSitesProvider, + private val frequency: Frequency = Frequency(1, TimeUnit.DAYS) +) { + + private val logger = Logger("ContileTopSitesUpdater") + + /** + * Starts a work request in the background to periodically update the list of + * Contile top sites. + */ + fun startPeriodicWork() { + ContileTopSitesUseCases.initialize(provider) + + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + PERIODIC_WORK_TAG, + ExistingPeriodicWorkPolicy.KEEP, + createPeriodicWorkRequest() + ) + + logger.info("Started periodic work to update Contile top sites") + } + + /** + * Stops the work request to periodically update the list of Contile top sites. + */ + fun stopPeriodicWork() { + ContileTopSitesUseCases.destroy() + + WorkManager.getInstance(context).cancelUniqueWork(PERIODIC_WORK_TAG) + + logger.info("Stopped periodic work to update Contile top sites") + } + + @VisibleForTesting + internal fun createPeriodicWorkRequest() = + PeriodicWorkRequestBuilder( + repeatInterval = frequency.repeatInterval, + repeatIntervalTimeUnit = frequency.repeatIntervalTimeUnit + ).apply { + setConstraints(getWorkerConstraints()) + addTag(PERIODIC_WORK_TAG) + }.build() + + @VisibleForTesting + internal fun getWorkerConstraints() = Constraints.Builder() + .setRequiresStorageNotLow(true) + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + companion object { + internal const val PERIODIC_WORK_TAG = "mozilla.components.service.contile.periodicWork" + } +} + +/** + * Indicates how often Contile top sites should be updated. + * + * @property repeatInterval Long indicating how often the update should happen. + * @property repeatIntervalTimeUnit The time unit of the [repeatInterval]. + */ +data class Frequency( + val repeatInterval: Long, + val repeatIntervalTimeUnit: TimeUnit +) diff --git a/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorker.kt b/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorker.kt new file mode 100644 index 00000000000..fceba217523 --- /dev/null +++ b/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorker.kt @@ -0,0 +1,34 @@ +/* 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.service.contile + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import mozilla.components.support.base.log.logger.Logger + +/** + * An implementation of [CoroutineWorker] to perform Contile top site updates. + */ +internal class ContileTopSitesUpdaterWorker( + private val context: Context, + params: WorkerParameters +) : CoroutineWorker(context, params) { + + private val logger = Logger("ContileTopSitesUpdaterWorker") + + @Suppress("TooGenericExceptionCaught") + override suspend fun doWork(): Result = withContext(Dispatchers.IO) { + try { + ContileTopSitesUseCases(context).refreshContileTopSites.invoke() + Result.success() + } catch (e: Exception) { + logger.error("Failed to refresh Contile top sites", e) + Result.failure() + } + } +} diff --git a/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUseCases.kt b/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUseCases.kt new file mode 100644 index 00000000000..f49177a740b --- /dev/null +++ b/components/service/contile/src/main/java/mozilla/components/service/contile/ContileTopSitesUseCases.kt @@ -0,0 +1,64 @@ +/* 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.service.contile + +import android.content.Context +import androidx.annotation.VisibleForTesting + +/** + * Contains use cases related to the Contlie top sites feature. + */ +internal class ContileTopSitesUseCases(context: Context) { + + /** + * Refresh Contile top sites use case. + * + * @param context A reference to the application context. + */ + class RefreshContileTopSitesUseCase internal constructor(private val context: Context) { + /** + * Refreshes the Contile top sites. + */ + suspend operator fun invoke() { + requireContileTopSitesProvider().getTopSites( + context = context, + allowCache = false + ) + } + } + + internal companion object { + @VisibleForTesting internal var provider: ContileTopSitesProvider? = null + + /** + * Initializes the [ContileTopSitesProvider] which will providde access to the Contile + * services API. + */ + internal fun initialize(provider: ContileTopSitesProvider) { + this.provider = provider + } + + /** + * Unbinds the [ContileTopSitesProvider]. + */ + internal fun destroy() { + this.provider = null + } + + /** + * Returns the [ContileTopSitesProvider], otherwise throw an exception if the [provider] + * has not been initialized. + */ + internal fun requireContileTopSitesProvider(): ContileTopSitesProvider { + return requireNotNull(provider) { + "initialize must be called before trying to access the ContileTopSitesProvider" + } + } + } + + val refreshContileTopSites: RefreshContileTopSitesUseCase by lazy { + RefreshContileTopSitesUseCase(context) + } +} diff --git a/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesProviderTest.kt b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesProviderTest.kt index b9a4238e9c4..1ac7504e971 100644 --- a/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesProviderTest.kt +++ b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesProviderTest.kt @@ -31,8 +31,8 @@ class ContileTopSitesProviderTest { @Test fun `GIVEN a successful status response WHEN top sites are fetched THEN response should contain top sites`() = runBlocking { val client = prepareClient() - val provider = ContileTopSitesProvider(testContext, client) - val topSites = provider.getTopSites() + val provider = ContileTopSitesProvider(client) + val topSites = provider.getTopSites(testContext) var topSite = topSites.first() assertEquals(2, topSites.size) @@ -56,26 +56,26 @@ class ContileTopSitesProviderTest { @Test(expected = IOException::class) fun `GIVEN a 500 status response WHEN top sites are fetched THEN throw an exception`() = runBlocking { val client = prepareClient(status = 500) - val provider = ContileTopSitesProvider(testContext, client) - provider.getTopSites() + val provider = ContileTopSitesProvider(client) + provider.getTopSites(testContext) Unit } @Test fun `GIVEN a cache configuration is allowed and not expired WHEN top sites are fetched THEN read from the disk cache`() = runBlocking { val client = prepareClient() - val provider = spy(ContileTopSitesProvider(testContext, client)) + val provider = spy(ContileTopSitesProvider(client)) - provider.getTopSites(allowCache = false) - verify(provider, never()).readFromDiskCache() + provider.getTopSites(testContext, allowCache = false) + verify(provider, never()).readFromDiskCache(testContext) - whenever(provider.isCacheExpired()).thenReturn(true) - provider.getTopSites(allowCache = true) - verify(provider, never()).readFromDiskCache() + whenever(provider.isCacheExpired(testContext)).thenReturn(true) + provider.getTopSites(testContext, allowCache = true) + verify(provider, never()).readFromDiskCache(testContext) - whenever(provider.isCacheExpired()).thenReturn(false) - provider.getTopSites(allowCache = true) - verify(provider).readFromDiskCache() + whenever(provider.isCacheExpired(testContext)).thenReturn(false) + provider.getTopSites(testContext, allowCache = true) + verify(provider).readFromDiskCache(testContext) Unit } @@ -84,33 +84,32 @@ class ContileTopSitesProviderTest { fun `GIVEN a cache configuration is allowed WHEN top sites are fetched THEN write response to cache`() = runBlocking { val jsonResponse = loadResourceAsString("/contile/contile.json") val client = prepareClient(jsonResponse) - val provider = spy(ContileTopSitesProvider(testContext, client)) + val provider = spy(ContileTopSitesProvider(client)) val cachingProvider = spy( ContileTopSitesProvider( - context = testContext, client = client, maxCacheAgeInMinutes = 1L ) ) - provider.getTopSites() - verify(provider, never()).writeToDiskCache(jsonResponse) + provider.getTopSites(testContext) + verify(provider, never()).writeToDiskCache(testContext, jsonResponse) - cachingProvider.getTopSites() - verify(cachingProvider).writeToDiskCache(jsonResponse) + cachingProvider.getTopSites(testContext) + verify(cachingProvider).writeToDiskCache(testContext, jsonResponse) } @Test fun `WHEN the base cache file getter is called THEN return existing base cache file`() { val client = prepareClient() - val provider = spy(ContileTopSitesProvider(testContext, client)) + val provider = spy(ContileTopSitesProvider(client)) val file = File(testContext.filesDir, CACHE_FILE_NAME) file.createNewFile() assertTrue(file.exists()) - val cacheFile = provider.getBaseCacheFile() + val cacheFile = provider.getBaseCacheFile(testContext) assertTrue(cacheFile.exists()) assertEquals(file.name, cacheFile.name) @@ -122,28 +121,27 @@ class ContileTopSitesProviderTest { @Test fun `GIVEN a max cache age WHEN the cache expiration is checked THEN return whether the cache is expired`() { var provider = - spy(ContileTopSitesProvider(testContext, client = mock(), maxCacheAgeInMinutes = -1)) + spy(ContileTopSitesProvider(client = mock(), maxCacheAgeInMinutes = -1)) - whenever(provider.getCacheLastModified()).thenReturn(Date().time) - assertTrue(provider.isCacheExpired()) + whenever(provider.getCacheLastModified(testContext)).thenReturn(Date().time) + assertTrue(provider.isCacheExpired(testContext)) - whenever(provider.getCacheLastModified()).thenReturn(-1) - assertTrue(provider.isCacheExpired()) + whenever(provider.getCacheLastModified(testContext)).thenReturn(-1) + assertTrue(provider.isCacheExpired(testContext)) - provider = - spy(ContileTopSitesProvider(testContext, client = mock(), maxCacheAgeInMinutes = 10)) + provider = spy(ContileTopSitesProvider(client = mock(), maxCacheAgeInMinutes = 10)) - whenever(provider.getCacheLastModified()).thenReturn(-1) - assertTrue(provider.isCacheExpired()) + whenever(provider.getCacheLastModified(testContext)).thenReturn(-1) + assertTrue(provider.isCacheExpired(testContext)) - whenever(provider.getCacheLastModified()).thenReturn(Date().time - 60 * MINUTE_IN_MS) - assertTrue(provider.isCacheExpired()) + whenever(provider.getCacheLastModified(testContext)).thenReturn(Date().time - 60 * MINUTE_IN_MS) + assertTrue(provider.isCacheExpired(testContext)) - whenever(provider.getCacheLastModified()).thenReturn(Date().time) - assertFalse(provider.isCacheExpired()) + whenever(provider.getCacheLastModified(testContext)).thenReturn(Date().time) + assertFalse(provider.isCacheExpired(testContext)) - whenever(provider.getCacheLastModified()).thenReturn(Date().time + 60 * MINUTE_IN_MS) - assertFalse(provider.isCacheExpired()) + whenever(provider.getCacheLastModified(testContext)).thenReturn(Date().time + 60 * MINUTE_IN_MS) + assertFalse(provider.isCacheExpired(testContext)) } private fun prepareClient( diff --git a/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterTest.kt b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterTest.kt new file mode 100644 index 00000000000..de3fb5c745b --- /dev/null +++ b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterTest.kt @@ -0,0 +1,95 @@ +/* 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.service.contile + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.work.Configuration +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.await +import androidx.work.testing.WorkManagerTestInitHelper +import kotlinx.coroutines.runBlocking +import mozilla.components.service.contile.ContileTopSitesUpdater.Companion.PERIODIC_WORK_TAG +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ContileTopSitesUpdaterTest { + + @Before + fun setUp() { + WorkManagerTestInitHelper.initializeTestWorkManager( + testContext, + Configuration.Builder().build() + ) + } + + @After + fun tearDown() { + WorkManager.getInstance(testContext).cancelUniqueWork(PERIODIC_WORK_TAG) + } + + @Test + fun `WHEN periodic work is started THEN work is queued`() = runBlocking { + val updater = ContileTopSitesUpdater(testContext, provider = mock()) + val workManager = WorkManager.getInstance(testContext) + var workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await() + + assertTrue(workInfo.isEmpty()) + assertNull(ContileTopSitesUseCases.provider) + + updater.startPeriodicWork() + + assertNotNull(ContileTopSitesUseCases.provider) + assertNotNull(ContileTopSitesUseCases.requireContileTopSitesProvider()) + + workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await() + val work = workInfo.first() + + assertEquals(1, workInfo.size) + assertEquals(WorkInfo.State.ENQUEUED, work.state) + assertTrue(work.tags.contains(PERIODIC_WORK_TAG)) + } + + @Test + fun `GIVEN periodic work is started WHEN period work is stopped THEN no work is queued`() = runBlocking { + val updater = ContileTopSitesUpdater(testContext, provider = mock()) + val workManager = WorkManager.getInstance(testContext) + var workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await() + + assertTrue(workInfo.isEmpty()) + + updater.startPeriodicWork() + + workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await() + + assertEquals(1, workInfo.size) + + updater.stopPeriodicWork() + + workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await() + val work = workInfo.first() + + assertNull(ContileTopSitesUseCases.provider) + assertEquals(WorkInfo.State.CANCELLED, work.state) + } + + @Test + fun `WHEN period work request is created THEN it contains the correct constraints`() { + val updater = ContileTopSitesUpdater(testContext, provider = mock()) + val workRequest = updater.createPeriodicWorkRequest() + + assertTrue(workRequest.tags.contains(PERIODIC_WORK_TAG)) + assertEquals(updater.getWorkerConstraints(), workRequest.workSpec.constraints) + } +} diff --git a/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorkerTest.kt b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorkerTest.kt new file mode 100644 index 00000000000..5b469e89407 --- /dev/null +++ b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorkerTest.kt @@ -0,0 +1,68 @@ +/* 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.service.contile + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.work.ListenableWorker +import androidx.work.await +import androidx.work.testing.TestListenableWorkerBuilder +import kotlinx.coroutines.runBlocking +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.junit.After +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.spy +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class ContileTopSitesUpdaterWorkerTest { + + @After + fun cleanup() { + ContileTopSitesUseCases.destroy() + } + + @Test + fun `WHEN worker does successful work THEN return a success result`() = runBlocking { + val provider: ContileTopSitesProvider = mock() + val worker = spy( + TestListenableWorkerBuilder(testContext) + .build() + ) + + ContileTopSitesUseCases.initialize(provider) + + whenever(provider.getTopSites(any(), anyBoolean())).thenReturn(emptyList()) + + val result = worker.startWork().await() + + assertEquals(ListenableWorker.Result.success(), result) + } + + @Test + fun `WHEN worker does unsuccessful work THEN return a failure result`() = runBlocking { + val provider: ContileTopSitesProvider = mock() + val worker = spy( + TestListenableWorkerBuilder(testContext) + .build() + ) + val throwable = IOException("test") + + ContileTopSitesUseCases.initialize(provider) + + whenever(provider.getTopSites(any(), anyBoolean())).then { + throw throwable + } + + val result = worker.startWork().await() + + assertEquals(ListenableWorker.Result.failure(), result) + } +} diff --git a/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUseCasesTest.kt b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUseCasesTest.kt new file mode 100644 index 00000000000..f5148579006 --- /dev/null +++ b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUseCasesTest.kt @@ -0,0 +1,51 @@ +/* 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.service.contile + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.runBlocking +import mozilla.components.support.test.any +import mozilla.components.support.test.eq +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.whenever +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mockito.verify +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class ContileTopSitesUseCasesTest { + + @Test + fun `WHEN refresh contile top site use case is called THEN call the provider to fetch top sites bypassing the cache`() = runBlocking { + val provider: ContileTopSitesProvider = mock() + + ContileTopSitesUseCases.initialize(provider) + + whenever(provider.getTopSites(any(), anyBoolean())).thenReturn(emptyList()) + + ContileTopSitesUseCases(testContext).refreshContileTopSites.invoke() + + verify(provider).getTopSites(any(), eq(false)) + + Unit + } + + @Test(expected = IOException::class) + fun `GIVEN the provider fails to fetch contile top sites WHEN refresh contile top site use case is called THEN an exception is thrown`() = runBlocking { + val provider: ContileTopSitesProvider = mock() + val throwable = IOException("test") + + ContileTopSitesUseCases.initialize(provider) + + whenever(provider.getTopSites(any(), anyBoolean())).then { + throw throwable + } + + ContileTopSitesUseCases(testContext).refreshContileTopSites.invoke() + } +}