diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
commit | d8bbc7858622b6d9c278469aab701ca0b609cddf (patch) | |
tree | eff41dc61d9f714852212739e6b3738b82a2af87 /mobile/android/android-components/components/service/location | |
parent | Releasing progress-linux version 125.0.3-1~progress7.99u1. (diff) | |
download | firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip |
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/service/location')
11 files changed, 757 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/service/location/.gitignore b/mobile/android/android-components/components/service/location/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/mobile/android/android-components/components/service/location/.gitignore @@ -0,0 +1 @@ +/build diff --git a/mobile/android/android-components/components/service/location/README.md b/mobile/android/android-components/components/service/location/README.md new file mode 100644 index 0000000000..9b333a44f9 --- /dev/null +++ b/mobile/android/android-components/components/service/location/README.md @@ -0,0 +1,19 @@ +# [Android Components](../../../README.md) > Service > Location + + A library for accessing Mozilla's and other location services. + +## Usage + +### Setting up the dependency + +Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)): + +```Groovy +implementation "org.mozilla.components:service-location:{latest-version}" +``` + +## License + + 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/ diff --git a/mobile/android/android-components/components/service/location/build.gradle b/mobile/android/android-components/components/service/location/build.gradle new file mode 100644 index 0000000000..e9446bf656 --- /dev/null +++ b/mobile/android/android-components/components/service/location/build.gradle @@ -0,0 +1,44 @@ +/* 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/. */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + defaultConfig { + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + namespace 'mozilla.components.service.location' +} + +dependencies { + implementation ComponentsDependencies.kotlin_coroutines + + implementation project(':concept-fetch') + implementation project(':support-ktx') + implementation project(':support-ktx') + implementation project(':support-base') + + testImplementation project(':support-test') + testImplementation ComponentsDependencies.androidx_test_junit + testImplementation ComponentsDependencies.testing_robolectric + testImplementation ComponentsDependencies.testing_mockwebserver + testImplementation ComponentsDependencies.testing_coroutines + + testImplementation project(':lib-fetch-httpurlconnection') +} + +apply from: '../../../android-lint.gradle' +apply from: '../../../publish.gradle' +ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) diff --git a/mobile/android/android-components/components/service/location/proguard-rules.pro b/mobile/android/android-components/components/service/location/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/components/service/location/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/components/service/location/src/main/AndroidManifest.xml b/mobile/android/android-components/components/service/location/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e16cda1d34 --- /dev/null +++ b/mobile/android/android-components/components/service/location/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ +<!-- 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/. --> +<manifest /> diff --git a/mobile/android/android-components/components/service/location/src/main/java/mozilla/components/service/location/LocationService.kt b/mobile/android/android-components/components/service/location/src/main/java/mozilla/components/service/location/LocationService.kt new file mode 100644 index 0000000000..8838b51bd1 --- /dev/null +++ b/mobile/android/android-components/components/service/location/src/main/java/mozilla/components/service/location/LocationService.kt @@ -0,0 +1,57 @@ +/* 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.location + +/** + * Interface describing a [LocationService] that returns a [Region]. + */ +interface LocationService { + /** + * Determines the current [Region] of the user. + */ + suspend fun fetchRegion(readFromCache: Boolean = true): Region? + + /** + * Get if there is already a cached region. + */ + fun hasRegionCached(): Boolean + + /** + * A [Region] returned by the location service. + * + * The [Region] use region codes and names from the GENC dataset, which is for the most part + * compatible with the ISO 3166 standard. While the API endpoint and [Region] class refers to + * country, no claim about the political status of any region is made by this service. + * + * @param countryCode Country code; ISO 3166. + * @param countryName Name of the country (English); ISO 3166. + */ + data class Region( + val countryCode: String, + val countryName: String, + ) + + companion object { + /** + * Creates a dummy [LocationService] implementation that always returns `null`. + */ + fun dummy() = object : LocationService { + override suspend fun fetchRegion(readFromCache: Boolean): Region? = null + override fun hasRegionCached(): Boolean = false + } + + /** + * Creates a default [LocationService] implementation that always returns the "XX" region. + * + * The advantage of using the default implementation over the dummy implementations is that + * code may stop retrying fetching a region if a region was returned from the service + * instead of `null` which indicates a failure. + */ + fun default() = object : LocationService { + override suspend fun fetchRegion(readFromCache: Boolean): Region? = Region("XX", "None") + override fun hasRegionCached(): Boolean = true + } + } +} diff --git a/mobile/android/android-components/components/service/location/src/main/java/mozilla/components/service/location/MozillaLocationService.kt b/mobile/android/android-components/components/service/location/src/main/java/mozilla/components/service/location/MozillaLocationService.kt new file mode 100644 index 0000000000..663ec7c5fe --- /dev/null +++ b/mobile/android/android-components/components/service/location/src/main/java/mozilla/components/service/location/MozillaLocationService.kt @@ -0,0 +1,187 @@ +/* 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.location + +import android.content.Context +import android.content.SharedPreferences +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.Companion.NONE +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import mozilla.components.Build +import mozilla.components.concept.fetch.Client +import mozilla.components.concept.fetch.Headers +import mozilla.components.concept.fetch.MutableHeaders +import mozilla.components.concept.fetch.Request +import mozilla.components.concept.fetch.Response +import mozilla.components.concept.fetch.isSuccess +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.ktx.kotlin.sanitizeURL +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.util.concurrent.TimeUnit + +private const val GEOIP_SERVICE_URL = "https://location.services.mozilla.com/v1/" +private const val CONNECT_TIMEOUT_SECONDS = 10L +private const val READ_TIMEOUT_SECONDS = 10L +private const val USER_AGENT = "MozAC/" + Build.version +private const val EMPTY_REQUEST_BODY = "{}" +private const val CACHE_FILE = "mozac.service.location.region" +private const val KEY_COUNTRY_CODE = "country_code" +private const val KEY_COUNTRY_NAME = "country_name" +private const val KEY_CACHED_AT = "cached_at" + +// The amount of time (in seconds) to cache the result of `MozillaLocationService.fetchRegion()`. +private const val CACHE_LIFETIME_IN_MS = 24 * 60 * 60 * 1000 + +/** + * The Mozilla Location Service (MLS) is an open service which lets devices determine their location + * based on network infrastructure like Bluetooth beacons, cell towers and WiFi access points. + * + * - https://location.services.mozilla.com/ + * - https://mozilla.github.io/ichnaea/api/index.html + * + * Note: Accessing the Mozilla Location Service requires an API token: + * https://location.services.mozilla.com/contact + * + * @param client The HTTP client that this [MozillaLocationService] should use for requests. + * @param apiKey The API key that is used to access the Mozilla location service. + * @param serviceUrl An optional URL override usually only needed for testing. + */ +class MozillaLocationService( + private val context: Context, + private val client: Client, + apiKey: String, + serviceUrl: String = GEOIP_SERVICE_URL, + private val currentTime: () -> Long = { System.currentTimeMillis() }, +) : LocationService { + private val regionServiceUrl = (serviceUrl + "country?key=%s").format(apiKey) + + /** + * Determines the current [LocationService.Region] based on the IP address used to access the service. + * + * https://mozilla.github.io/ichnaea/api/region.html + * + * @param readFromCache Whether a previously returned region (from the cache) can be returned + * (default) or whether a request to the service should always be made. + */ + override suspend fun fetchRegion( + readFromCache: Boolean, + ): LocationService.Region? = withContext(Dispatchers.IO) { + if (readFromCache && isCacheValid()) { + context.loadCachedRegion()?.let { return@withContext it } + } + + client.fetchRegion(regionServiceUrl)?.also { + context.cacheRegion(it) + } + } + + /** + * Get if there is already a cached region. + * This does not guarantee we have the current actual region but only the last value + * which may be obsolete at this time. + */ + override fun hasRegionCached(): Boolean { + return context.hasCachedRegion() + } + + /** + * Check to see if the cache is still valid. + */ + private fun isCacheValid(): Boolean { + return currentTime() < context.cachedAt() + CACHE_LIFETIME_IN_MS + } + + private fun Context.cacheRegion(region: LocationService.Region) { + regionCache() + .edit() + .putString(KEY_COUNTRY_CODE, region.countryCode) + .putString(KEY_COUNTRY_NAME, region.countryName) + .putLong(KEY_CACHED_AT, currentTime()) + .apply() + } +} + +private fun Context.loadCachedRegion(): LocationService.Region? { + val cache = regionCache() + + return if (cache.contains(KEY_COUNTRY_CODE) && cache.contains(KEY_COUNTRY_NAME)) { + LocationService.Region( + cache.getString(KEY_COUNTRY_CODE, null)!!, + cache.getString(KEY_COUNTRY_NAME, null)!!, + ) + } else { + null + } +} + +private fun Context.cachedAt(): Long { + val cache = regionCache() + return cache.getLong(KEY_CACHED_AT, 0L) +} + +private fun Context.hasCachedRegion(): Boolean { + val cache = regionCache() + return cache.contains(KEY_COUNTRY_CODE) && cache.contains(KEY_COUNTRY_NAME) +} + +@VisibleForTesting(otherwise = NONE) +internal fun Context.clearRegionCache() { + regionCache() + .edit() + .clear() + .apply() +} + +private fun Context.regionCache(): SharedPreferences { + return getSharedPreferences(CACHE_FILE, Context.MODE_PRIVATE) +} + +private fun Client.fetchRegion(regionServiceUrl: String): LocationService.Region? { + val request = Request( + url = regionServiceUrl.sanitizeURL(), + method = Request.Method.POST, + headers = MutableHeaders( + Headers.Names.CONTENT_TYPE to Headers.Values.CONTENT_TYPE_APPLICATION_JSON, + Headers.Names.USER_AGENT to USER_AGENT, + ), + // We are posting an empty request body here. This means the service will only use the IP + // address to provide a region response. Technically it's possible to also provide data + // about nearby Bluetooth, cell or WiFi networks. + body = Request.Body.fromString(EMPTY_REQUEST_BODY), + connectTimeout = Pair(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS), + readTimeout = Pair(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS), + conservative = true, + ) + + return try { + fetch(request).toRegion() + } catch (e: IOException) { + Logger.debug(message = "Could not fetch region from location service", throwable = e) + null + } +} + +private fun Response.toRegion(): LocationService.Region? { + if (!isSuccess) { + close() + return null + } + + use { + return try { + val json = JSONObject(body.string(Charsets.UTF_8)) + LocationService.Region( + json.getString(KEY_COUNTRY_CODE), + json.getString(KEY_COUNTRY_NAME), + ) + } catch (e: JSONException) { + Logger.debug(message = "Could not parse JSON returned from location service", throwable = e) + null + } + } +} diff --git a/mobile/android/android-components/components/service/location/src/test/java/mozilla/components/service/location/LocationServiceTest.kt b/mobile/android/android-components/components/service/location/src/test/java/mozilla/components/service/location/LocationServiceTest.kt new file mode 100644 index 0000000000..eca31d268f --- /dev/null +++ b/mobile/android/android-components/components/service/location/src/test/java/mozilla/components/service/location/LocationServiceTest.kt @@ -0,0 +1,23 @@ +/* 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.location + +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LocationServiceTest { + @ExperimentalCoroutinesApi + @Test + fun `dummy implementation returns null`() { + runTest(UnconfinedTestDispatcher()) { + assertNull(LocationService.dummy().fetchRegion(false)) + assertNull(LocationService.dummy().fetchRegion(true)) + assertNull(LocationService.dummy().fetchRegion(false)) + } + } +} diff --git a/mobile/android/android-components/components/service/location/src/test/java/mozilla/components/service/location/MozillaLocationServiceTest.kt b/mobile/android/android-components/components/service/location/src/test/java/mozilla/components/service/location/MozillaLocationServiceTest.kt new file mode 100644 index 0000000000..d2cf7d64aa --- /dev/null +++ b/mobile/android/android-components/components/service/location/src/test/java/mozilla/components/service/location/MozillaLocationServiceTest.kt @@ -0,0 +1,399 @@ +/* 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.location + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.currentTime +import kotlinx.coroutines.test.runTest +import mozilla.components.concept.fetch.Client +import mozilla.components.concept.fetch.MutableHeaders +import mozilla.components.concept.fetch.Request +import mozilla.components.concept.fetch.Response +import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient +import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.fakes.FakeClock +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.doThrow +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import java.io.IOException + +@ExperimentalCoroutinesApi // for runTest +@RunWith(AndroidJUnit4::class) +class MozillaLocationServiceTest { + @Before + @After + fun cleanUp() { + testContext.clearRegionCache() + } + + @Test + fun `WHEN calling fetchRegion AND the service returns a region THEN a Region object is returned`() = runTest { + val server = MockWebServer() + server.enqueue(MockResponse().setBody("{\"country_name\": \"Germany\", \"country_code\": \"DE\"}")) + + try { + server.start() + + val service = MozillaLocationService( + testContext, + HttpURLConnectionClient(), + apiKey = "test", + serviceUrl = server.url("/").toString(), + ) + + val region = service.fetchRegion() + + assertNotNull(region!!) + + assertEquals("DE", region.countryCode) + assertEquals("Germany", region.countryName) + + val request = server.takeRequest() + + assertEquals(server.url("/country?key=test"), request.requestUrl) + } finally { + server.shutdown() + } + } + + @Test + fun `WHEN client throws IOException THEN the returned region is null`() = runTest { + val client: Client = mock() + doThrow(IOException()).`when`(client).fetch(any()) + + val service = MozillaLocationService(testContext, client, apiKey = "test") + val region = service.fetchRegion() + + assertNull(region) + } + + @Test + fun `WHEN fetching region THEN request is sent to the location service`() = runTest { + val client: Client = mock() + val response = Response( + url = "http://example.org", + status = 200, + headers = MutableHeaders(), + body = Response.Body("{\"country_name\": \"France\", \"country_code\": \"FR\"}".byteInputStream()), + ) + doReturn(response).`when`(client).fetch(any()) + + val service = MozillaLocationService(testContext, client, apiKey = "test") + val region = service.fetchRegion() + + assertNotNull(region!!) + + assertEquals("FR", region.countryCode) + assertEquals("France", region.countryName) + + val captor = argumentCaptor<Request>() + verify(client).fetch(captor.capture()) + + val request = captor.value + assertEquals("https://location.services.mozilla.com/v1/country?key=test", request.url) + } + + @Test + fun `WHEN fetching region AND service returns 404 THEN region is null`() = runTest { + val client: Client = mock() + val response = Response( + url = "http://example.org", + status = 404, + headers = MutableHeaders(), + body = Response.Body.empty(), + ) + doReturn(response).`when`(client).fetch(any()) + + val service = MozillaLocationService(testContext, client, apiKey = "test") + val region = service.fetchRegion() + + assertNull(region) + } + + @Test + fun `WHEN fetching region AND service returns 500 THEN region is null`() = runTest { + val client: Client = mock() + val response = Response( + url = "http://example.org", + status = 500, + headers = MutableHeaders(), + body = Response.Body("Internal Server Error".byteInputStream()), + ) + doReturn(response).`when`(client).fetch(any()) + + val service = MozillaLocationService(testContext, client, apiKey = "test") + val region = service.fetchRegion() + + assertNull(region) + } + + @Test + fun `WHEN fetching region AND service returns broken JSON THEN region is null`() = runTest { + val client: Client = mock() + val response = Response( + url = "http://example.org", + status = 200, + headers = MutableHeaders(), + body = Response.Body("{\"country_name\": \"France\",".byteInputStream()), + ) + doReturn(response).`when`(client).fetch(any()) + + val service = MozillaLocationService(testContext, client, apiKey = "test") + val region = service.fetchRegion() + + assertNull(region) + } + + @Test + fun `WHEN fetching region AND service returns empty JSON object THEN region is null`() = runTest { + val client: Client = mock() + val response = Response( + url = "http://example.org", + status = 200, + headers = MutableHeaders(), + body = Response.Body("{}".byteInputStream()), + ) + doReturn(response).`when`(client).fetch(any()) + + val service = MozillaLocationService(testContext, client, apiKey = "test") + val region = service.fetchRegion() + + assertNull(region) + } + + @Test + fun `WHEN fetching region AND service returns incomplete JSON THEN region is null`() = runTest { + val client: Client = mock() + val response = Response( + url = "http://example.org", + status = 200, + headers = MutableHeaders(), + body = Response.Body("{\"country_code\": \"DE\"}".byteInputStream()), + ) + doReturn(response).`when`(client).fetch(any()) + + val service = MozillaLocationService(testContext, client, apiKey = "test") + val region = service.fetchRegion() + + assertNull(region) + } + + @Test + fun `WHEN fetching region for the second time THEN region is read from cache`() = runTest { + run { + val client: Client = mock() + val response = Response( + url = "http://example.org", + status = 200, + headers = MutableHeaders(), + body = Response.Body("{\"country_name\": \"Nepal\", \"country_code\": \"NP\"}".byteInputStream()), + ) + doReturn(response).`when`(client).fetch(any()) + + val service = MozillaLocationService(testContext, client, apiKey = "test") + val region = service.fetchRegion() + + assertNotNull(region!!) + + assertEquals("NP", region.countryCode) + assertEquals("Nepal", region.countryName) + + verify(client).fetch(any()) + } + + run { + val client: Client = mock() + + val service = MozillaLocationService(testContext, client, apiKey = "test") + val region = service.fetchRegion() + + assertNotNull(region!!) + + assertEquals("NP", region.countryCode) + assertEquals("Nepal", region.countryName) + + verify(client, never()).fetch(any()) + } + } + + @Test + fun `WHEN fetching region for the second time and setting readFromCache = false THEN request is sent again`() = runTest { + run { + val client: Client = mock() + val response = Response( + url = "http://example.org", + status = 200, + headers = MutableHeaders(), + body = Response.Body("{\"country_name\": \"Nepal\", \"country_code\": \"NP\"}".byteInputStream()), + ) + doReturn(response).`when`(client).fetch(any()) + + val service = MozillaLocationService(testContext, client, apiKey = "test") + val region = service.fetchRegion() + + assertNotNull(region!!) + + assertEquals("NP", region.countryCode) + assertEquals("Nepal", region.countryName) + + verify(client).fetch(any()) + } + + run { + val client: Client = mock() + val response = Response( + url = "http://example.org", + status = 200, + headers = MutableHeaders(), + body = Response.Body("{\"country_name\": \"Liberia\", \"country_code\": \"LR\"}".byteInputStream()), + ) + doReturn(response).`when`(client).fetch(any()) + + val service = MozillaLocationService(testContext, client, apiKey = "test") + val region = service.fetchRegion(readFromCache = false) + + assertNotNull(region!!) + + assertEquals("LR", region.countryCode) + assertEquals("Liberia", region.countryName) + + verify(client).fetch(any()) + } + } + + @Test + fun `WHEN fetching region and the cache is valid THEN request is not sent again`() = runTest { + val clock = FakeClock() + + run { + val client: Client = mock() + val response = Response( + url = "http://example.org", + status = 200, + headers = MutableHeaders(), + body = Response.Body("{\"country_name\": \"Nepal\", \"country_code\": \"NP\"}".byteInputStream()), + ) + doReturn(response).`when`(client).fetch(any()) + + val service = MozillaLocationService( + testContext, + client, + apiKey = "test", + currentTime = clock::time, + ) + val region = service.fetchRegion(readFromCache = true) + + assertNotNull(region!!) + + assertEquals("NP", region.countryCode) + assertEquals("Nepal", region.countryName) + + verify(client).fetch(any()) + } + + // Let's jump 23 hours in the future + clock.advanceBy(23 * 60 * 60 * 1000) + + run { + val client: Client = mock() + val response = Response( + url = "http://example.org", + status = 200, + headers = MutableHeaders(), + body = Response.Body("{\"country_name\": \"Liberia\", \"country_code\": \"LR\"}".byteInputStream()), + ) + doReturn(response).`when`(client).fetch(any()) + + val service = MozillaLocationService( + testContext, + client, + apiKey = "test", + currentTime = clock::time, + ) + val region = service.fetchRegion(readFromCache = true) + + assertNotNull(region!!) + + assertEquals("NP", region.countryCode) + assertEquals("Nepal", region.countryName) + + verify(client, never()).fetch(any()) + } + } + + @Test + fun `WHEN fetching region and the cache is invalid THEN request is sent again`() = runTest { + val clock = FakeClock() + + run { + val client: Client = mock() + val response = Response( + url = "http://example.org", + status = 200, + headers = MutableHeaders(), + body = Response.Body("{\"country_name\": \"Nepal\", \"country_code\": \"NP\"}".byteInputStream()), + ) + doReturn(response).`when`(client).fetch(any()) + + val service = MozillaLocationService( + testContext, + client, + apiKey = "test", + currentTime = clock::time, + ) + val region = service.fetchRegion(readFromCache = true) + + assertNotNull(region!!) + + assertEquals("NP", region.countryCode) + assertEquals("Nepal", region.countryName) + + verify(client).fetch(any()) + } + + // Let's jump 24 hours in the future + clock.advanceBy(24 * 60 * 60 * 1000) + + run { + val client: Client = mock() + val response = Response( + url = "http://example.org", + status = 200, + headers = MutableHeaders(), + body = Response.Body("{\"country_name\": \"Liberia\", \"country_code\": \"LR\"}".byteInputStream()), + ) + doReturn(response).`when`(client).fetch(any()) + + val service = MozillaLocationService( + testContext, + client, + apiKey = "test", + currentTime = clock::time, + ) + val region = service.fetchRegion(readFromCache = true) + + assertNotNull(region!!) + + assertEquals("LR", region.countryCode) + assertEquals("Liberia", region.countryName) + + verify(client).fetch(any()) + } + } +} diff --git a/mobile/android/android-components/components/service/location/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/service/location/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/mobile/android/android-components/components/service/location/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/mobile/android/android-components/components/service/location/src/test/resources/robolectric.properties b/mobile/android/android-components/components/service/location/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..932b01b9eb --- /dev/null +++ b/mobile/android/android-components/components/service/location/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=28 |