summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/service/digitalassetlinks/src/test
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/android-components/components/service/digitalassetlinks/src/test')
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/AndroidAssetFinderTest.kt170
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/api/DigitalAssetLinksApiTest.kt229
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementApiTest.kt356
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementRelationCheckerTest.kt89
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker1
-rw-r--r--mobile/android/android-components/components/service/digitalassetlinks/src/test/resources/robolectric.properties1
6 files changed, 846 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/AndroidAssetFinderTest.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/AndroidAssetFinderTest.kt
new file mode 100644
index 0000000000..32c244286d
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/AndroidAssetFinderTest.kt
@@ -0,0 +1,170 @@
+/* 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.digitalassetlinks
+
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.Signature
+import android.content.pm.SigningInfo
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class AndroidAssetFinderTest {
+
+ private lateinit var assetFinder: AndroidAssetFinder
+ private lateinit var packageInfo: PackageInfo
+
+ @Mock lateinit var packageManager: PackageManager
+
+ @Mock lateinit var signingInfo: SigningInfo
+
+ @Before
+ fun setup() {
+ assetFinder = spy(AndroidAssetFinder())
+
+ MockitoAnnotations.openMocks(this)
+ packageInfo = PackageInfo()
+ @Suppress("DEPRECATION")
+ `when`(packageManager.getPackageInfo(anyString(), anyInt())).thenReturn(packageInfo)
+ }
+
+ @Test
+ fun `test getAndroidAppAsset returns empty list if name not found`() {
+ @Suppress("DEPRECATION")
+ `when`(packageManager.getPackageInfo(anyString(), anyInt()))
+ .thenThrow(PackageManager.NameNotFoundException::class.java)
+
+ assertEquals(
+ emptyList<AssetDescriptor.Android>(),
+ assetFinder.getAndroidAppAsset("com.test.app", packageManager).toList(),
+ )
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.P])
+ @Test
+ fun `test getAndroidAppAsset on P SDK`() {
+ val signature = mock<Signature>()
+ packageInfo.signingInfo = signingInfo
+ `when`(signingInfo.hasMultipleSigners()).thenReturn(false)
+ `when`(signingInfo.signingCertificateHistory).thenReturn(arrayOf(signature, mock()))
+ doReturn("01:BB:AA:10:30").`when`(assetFinder).getCertificateSHA256Fingerprint(signature)
+
+ assertEquals(
+ listOf(AssetDescriptor.Android("com.test.app", "01:BB:AA:10:30")),
+ assetFinder.getAndroidAppAsset("com.test.app", packageManager).toList(),
+ )
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.P])
+ @Test
+ fun `test getAndroidAppAsset with multiple signatures on P SDK`() {
+ val signature1 = mock<Signature>()
+ val signature2 = mock<Signature>()
+ packageInfo.signingInfo = signingInfo
+ `when`(signingInfo.hasMultipleSigners()).thenReturn(true)
+ `when`(signingInfo.apkContentsSigners).thenReturn(arrayOf(signature1, signature2))
+ doReturn("01:BB:AA:10:30").`when`(assetFinder).getCertificateSHA256Fingerprint(signature1)
+ doReturn("FF:CC:AA:99:77").`when`(assetFinder).getCertificateSHA256Fingerprint(signature2)
+
+ assertEquals(
+ listOf(
+ AssetDescriptor.Android("org.test.app", "01:BB:AA:10:30"),
+ AssetDescriptor.Android("org.test.app", "FF:CC:AA:99:77"),
+ ),
+ assetFinder.getAndroidAppAsset("org.test.app", packageManager).toList(),
+ )
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.P])
+ @Test
+ fun `test getAndroidAppAsset with empty history`() {
+ packageInfo.signingInfo = signingInfo
+ `when`(signingInfo.hasMultipleSigners()).thenReturn(false)
+ `when`(signingInfo.signingCertificateHistory).thenReturn(emptyArray())
+
+ assertEquals(
+ emptyList<AssetDescriptor.Android>(),
+ assetFinder.getAndroidAppAsset("com.test.app", packageManager).toList(),
+ )
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
+ @Suppress("Deprecation")
+ @Test
+ fun `test getAndroidAppAsset on deprecated SDK`() {
+ val signature = mock<Signature>()
+ packageInfo.signatures = arrayOf(signature)
+ doReturn("01:BB:AA:10:30").`when`(assetFinder).getCertificateSHA256Fingerprint(signature)
+
+ assertEquals(
+ listOf(AssetDescriptor.Android("com.test.app", "01:BB:AA:10:30")),
+ assetFinder.getAndroidAppAsset("com.test.app", packageManager).toList(),
+ )
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
+ @Suppress("Deprecation")
+ @Test
+ fun `test getAndroidAppAsset with multiple signatures on deprecated SDK`() {
+ val signature1 = mock<Signature>()
+ val signature2 = mock<Signature>()
+ packageInfo.signatures = arrayOf(signature1, signature2)
+ doReturn("01:BB:AA:10:30").`when`(assetFinder).getCertificateSHA256Fingerprint(signature1)
+ doReturn("FF:CC:AA:99:77").`when`(assetFinder).getCertificateSHA256Fingerprint(signature2)
+
+ assertEquals(
+ listOf(
+ AssetDescriptor.Android("org.test.app", "01:BB:AA:10:30"),
+ AssetDescriptor.Android("org.test.app", "FF:CC:AA:99:77"),
+ ),
+ assetFinder.getAndroidAppAsset("org.test.app", packageManager).toList(),
+ )
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
+ @Suppress("Deprecation")
+ @Test
+ fun `test getAndroidAppAsset is lazily computed`() {
+ val signature1 = mock<Signature>()
+ val signature2 = mock<Signature>()
+ packageInfo.signatures = arrayOf(signature1, signature2)
+ doReturn("01:BB:AA:10:30").`when`(assetFinder).getCertificateSHA256Fingerprint(signature1)
+ doReturn("FF:CC:AA:99:77").`when`(assetFinder).getCertificateSHA256Fingerprint(signature2)
+
+ val result = assetFinder.getAndroidAppAsset("android.package", packageManager).first()
+ assertEquals(
+ AssetDescriptor.Android("android.package", "01:BB:AA:10:30"),
+ result,
+ )
+
+ verify(assetFinder, times(1)).getCertificateSHA256Fingerprint(any())
+ }
+
+ @Test
+ fun `test byteArrayToHexString`() {
+ val array = byteArrayOf(0xaa.toByte(), 0xbb.toByte(), 0xcc.toByte(), 0x10, 0x20, 0x30, 0x01, 0x02)
+ assertEquals(
+ "AA:BB:CC:10:20:30:01:02",
+ assetFinder.byteArrayToHexString(array),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/api/DigitalAssetLinksApiTest.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/api/DigitalAssetLinksApiTest.kt
new file mode 100644
index 0000000000..53418751b7
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/api/DigitalAssetLinksApiTest.kt
@@ -0,0 +1,229 @@
+/* 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.digitalassetlinks.api
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+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.concept.fetch.Response.Companion.SUCCESS
+import mozilla.components.service.digitalassetlinks.AssetDescriptor
+import mozilla.components.service.digitalassetlinks.Relation.HANDLE_ALL_URLS
+import mozilla.components.service.digitalassetlinks.Relation.USE_AS_ORIGIN
+import mozilla.components.service.digitalassetlinks.Statement
+import mozilla.components.service.digitalassetlinks.TIMEOUT
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class DigitalAssetLinksApiTest {
+
+ private val webAsset = AssetDescriptor.Web(site = "https://mozilla.org")
+ private val androidAsset = AssetDescriptor.Android(
+ packageName = "com.mozilla.fenix",
+ sha256CertFingerprint = "01:23:45:67:89",
+ )
+ private val baseRequest = Request(
+ url = "https://mozilla.org",
+ method = Request.Method.GET,
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ )
+ private val apiKey = "X"
+ private lateinit var client: Client
+ private lateinit var api: DigitalAssetLinksApi
+
+ @Before
+ fun setup() {
+ client = mock()
+ api = DigitalAssetLinksApi(client, apiKey)
+
+ doReturn(mockResponse("")).`when`(client).fetch(any())
+ }
+
+ @Test
+ fun `reject for invalid status`() {
+ val response = mockResponse("").copy(status = 400)
+ doReturn(response).`when`(client).fetch(any())
+
+ assertFalse(api.checkRelationship(webAsset, USE_AS_ORIGIN, androidAsset))
+ assertEquals(emptyList<Statement>(), api.listStatements(webAsset).toList())
+ }
+
+ @Test
+ fun `reject check for invalid json`() {
+ doReturn(mockResponse("")).`when`(client).fetch(any())
+ assertFalse(api.checkRelationship(webAsset, USE_AS_ORIGIN, webAsset))
+
+ doReturn(mockResponse("{}")).`when`(client).fetch(any())
+ assertFalse(api.checkRelationship(webAsset, USE_AS_ORIGIN, androidAsset))
+
+ doReturn(mockResponse("[]")).`when`(client).fetch(any())
+ assertFalse(api.checkRelationship(webAsset, USE_AS_ORIGIN, androidAsset))
+
+ doReturn(mockResponse("{\"lnkd\":true}")).`when`(client).fetch(any())
+ assertFalse(api.checkRelationship(webAsset, USE_AS_ORIGIN, androidAsset))
+ }
+
+ @Test
+ fun `reject list for invalid json`() {
+ val empty = emptyList<Statement>()
+
+ doReturn(mockResponse("")).`when`(client).fetch(any())
+ assertEquals(empty, api.listStatements(webAsset).toList())
+
+ doReturn(mockResponse("{}")).`when`(client).fetch(any())
+ assertEquals(empty, api.listStatements(webAsset).toList())
+
+ doReturn(mockResponse("[]")).`when`(client).fetch(any())
+ assertEquals(empty, api.listStatements(webAsset).toList())
+
+ doReturn(mockResponse("{\"stmt\":[]}")).`when`(client).fetch(any())
+ assertEquals(empty, api.listStatements(webAsset).toList())
+ }
+
+ @Test
+ fun `return linked from json`() {
+ doReturn(mockResponse("{\"linked\":true,\"maxAge\":\"3s\"}")).`when`(client).fetch(any())
+ assertTrue(api.checkRelationship(webAsset, USE_AS_ORIGIN, androidAsset))
+
+ doReturn(mockResponse("{\"linked\":false}\"maxAge\":\"3s\"}")).`when`(client).fetch(any())
+ assertFalse(api.checkRelationship(webAsset, USE_AS_ORIGIN, androidAsset))
+ }
+
+ @Test
+ fun `return empty list if json doesn't match expected format`() {
+ val jsonPrefix = "{\"statements\":["
+ val jsonSuffix = "],\"maxAge\":\"3s\"}"
+ doReturn(mockResponse(jsonPrefix + jsonSuffix)).`when`(client).fetch(any())
+ assertEquals(emptyList<Statement>(), api.listStatements(webAsset).toList())
+
+ val invalidRelation = """
+ {
+ "source": {"web":{"site": "https://mozilla.org"}},
+ "target": {"web":{"site": "https://mozilla.org"}},
+ "relation": "not-a-relation"
+ }
+ """
+ doReturn(mockResponse(jsonPrefix + invalidRelation + jsonSuffix)).`when`(client).fetch(any())
+ assertEquals(emptyList<Statement>(), api.listStatements(webAsset).toList())
+
+ val invalidTarget = """
+ {
+ "source": {"web":{"site": "https://mozilla.org"}},
+ "target": {},
+ "relation": "delegate_permission/common.use_as_origin"
+ }
+ """
+ doReturn(mockResponse(jsonPrefix + invalidTarget + jsonSuffix)).`when`(client).fetch(any())
+ assertEquals(emptyList<Statement>(), api.listStatements(webAsset).toList())
+ }
+
+ @Test
+ fun `parses json statement list with web target`() {
+ val webStatement = """
+ {"statements": [{
+ "source": {"web":{"site": "https://mozilla.org"}},
+ "target": {"web":{"site": "https://mozilla.org"}},
+ "relation": "delegate_permission/common.use_as_origin"
+ }], "maxAge": "59s"}
+ """
+ doReturn(mockResponse(webStatement)).`when`(client).fetch(any())
+ assertEquals(
+ listOf(
+ Statement(
+ relation = USE_AS_ORIGIN,
+ target = webAsset,
+ ),
+ ),
+ api.listStatements(webAsset).toList(),
+ )
+ }
+
+ @Test
+ fun `parses json statement list with android target`() {
+ val androidStatement = """
+ {"statements": [{
+ "source": {"web":{"site": "https://mozilla.org"}},
+ "target": {"androidApp":{
+ "packageName": "com.mozilla.fenix",
+ "certificate": {"sha256Fingerprint": "01:23:45:67:89"}
+ }},
+ "relation": "delegate_permission/common.handle_all_urls"
+ }], "maxAge": "2m"}
+ """
+ doReturn(mockResponse(androidStatement)).`when`(client).fetch(any())
+ assertEquals(
+ listOf(
+ Statement(
+ relation = HANDLE_ALL_URLS,
+ target = androidAsset,
+ ),
+ ),
+ api.listStatements(webAsset).toList(),
+ )
+ }
+
+ @Test
+ fun `passes data in get check request URL for android target`() {
+ api.checkRelationship(webAsset, USE_AS_ORIGIN, androidAsset)
+ verify(client).fetch(
+ baseRequest.copy(
+ url = "https://digitalassetlinks.googleapis.com/v1/assetlinks:check?" +
+ "prettyPrint=false&key=X&relation=delegate_permission%2Fcommon.use_as_origin&" +
+ "source.web.site=${Uri.encode("https://mozilla.org")}&" +
+ "target.androidApp.packageName=com.mozilla.fenix&" +
+ "target.androidApp.certificate.sha256Fingerprint=${Uri.encode("01:23:45:67:89")}",
+ ),
+ )
+ }
+
+ @Test
+ fun `passes data in get check request URL for web target`() {
+ api.checkRelationship(webAsset, HANDLE_ALL_URLS, webAsset)
+ verify(client).fetch(
+ baseRequest.copy(
+ url = "https://digitalassetlinks.googleapis.com/v1/assetlinks:check?" +
+ "prettyPrint=false&key=X&relation=delegate_permission%2Fcommon.handle_all_urls&" +
+ "source.web.site=${Uri.encode("https://mozilla.org")}&" +
+ "target.web.site=${Uri.encode("https://mozilla.org")}",
+ ),
+ )
+ }
+
+ @Test
+ fun `passes data in get list request URL`() {
+ api.listStatements(webAsset)
+ verify(client).fetch(
+ baseRequest.copy(
+ url = "https://digitalassetlinks.googleapis.com/v1/statements:list?" +
+ "prettyPrint=false&key=X&source.web.site=${Uri.encode("https://mozilla.org")}",
+ ),
+ )
+ }
+
+ private fun mockResponse(data: String) = Response(
+ url = "",
+ status = SUCCESS,
+ headers = MutableHeaders(),
+ body = mockBody(data),
+ )
+
+ private fun mockBody(data: String): Response.Body {
+ val mockBody: Response.Body = mock()
+ doReturn(data).`when`(mockBody).string()
+ return mockBody
+ }
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementApiTest.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementApiTest.kt
new file mode 100644
index 0000000000..7ebd8ac67e
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementApiTest.kt
@@ -0,0 +1,356 @@
+/* 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.digitalassetlinks.local
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Headers.Names.CONTENT_TYPE
+import mozilla.components.concept.fetch.Headers.Values.CONTENT_TYPE_APPLICATION_JSON
+import mozilla.components.concept.fetch.Headers.Values.CONTENT_TYPE_FORM_URLENCODED
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.service.digitalassetlinks.AssetDescriptor
+import mozilla.components.service.digitalassetlinks.Relation
+import mozilla.components.service.digitalassetlinks.Statement
+import mozilla.components.service.digitalassetlinks.StatementListFetcher
+import mozilla.components.service.digitalassetlinks.TIMEOUT
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import java.io.ByteArrayInputStream
+import java.io.IOException
+
+@RunWith(AndroidJUnit4::class)
+class StatementApiTest {
+
+ @Mock private lateinit var httpClient: Client
+ private lateinit var listFetcher: StatementListFetcher
+ private val jsonHeaders = MutableHeaders(
+ CONTENT_TYPE to CONTENT_TYPE_APPLICATION_JSON,
+ )
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.openMocks(this)
+ listFetcher = StatementApi(httpClient)
+ }
+
+ @Test
+ fun `return empty list if request fails`() {
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "https://mozilla.org/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenThrow(IOException::class.java)
+
+ val source = AssetDescriptor.Web("https://mozilla.org")
+ assertEquals(emptyList<Statement>(), listFetcher.listStatements(source).toList())
+ }
+
+ @Test
+ fun `return empty list if response does not have status 200`() {
+ val response = Response(
+ url = "https://firefox.com/.well-known/assetlinks.json",
+ status = 201,
+ headers = jsonHeaders,
+ body = mock(),
+ )
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "https://firefox.com/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(response)
+
+ val source = AssetDescriptor.Web("https://firefox.com")
+ assertEquals(emptyList<Statement>(), listFetcher.listStatements(source).toList())
+ }
+
+ @Test
+ fun `return empty list if response does not have JSON content type`() {
+ val response = Response(
+ url = "https://firefox.com/.well-known/assetlinks.json",
+ status = 200,
+ headers = MutableHeaders(
+ CONTENT_TYPE to CONTENT_TYPE_FORM_URLENCODED,
+ ),
+ body = mock(),
+ )
+
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "https://firefox.com/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(response)
+
+ val source = AssetDescriptor.Web("https://firefox.com")
+ assertEquals(emptyList<Statement>(), listFetcher.listStatements(source).toList())
+ }
+
+ @Test
+ fun `return empty list if response is not valid JSON`() {
+ val response = Response(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ status = 200,
+ headers = jsonHeaders,
+ body = stringBody("not-json"),
+ )
+
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(response)
+
+ val source = AssetDescriptor.Web("http://firefox.com")
+ assertEquals(emptyList<Statement>(), listFetcher.listStatements(source).toList())
+ }
+
+ @Test
+ fun `return empty list if response is an empty JSON array`() {
+ val response = Response(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ status = 200,
+ headers = jsonHeaders,
+ body = stringBody("[]"),
+ )
+
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(response)
+
+ val source = AssetDescriptor.Web("http://firefox.com")
+ assertEquals(emptyList<Statement>(), listFetcher.listStatements(source).toList())
+ }
+
+ @Test
+ fun `parses example asset links file`() {
+ val response = Response(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ status = 200,
+ headers = jsonHeaders,
+ body = stringBody(
+ """
+ [{
+ "relation": [
+ "delegate_permission/common.handle_all_urls",
+ "delegate_permission/common.use_as_origin"
+ ],
+ "target": {
+ "namespace": "web",
+ "site": "https://www.google.com"
+ }
+ },{
+ "relation": ["delegate_permission/common.handle_all_urls"],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "org.digitalassetlinks.sampleapp",
+ "sha256_cert_fingerprints": [
+ "10:39:38:EE:45:37:E5:9E:8E:E7:92:F6:54:50:4F:B8:34:6F:C6:B3:46:D0:BB:C4:41:5F:C3:39:FC:FC:8E:C1"
+ ]
+ }
+ },{
+ "relation": ["delegate_permission/common.handle_all_urls"],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "org.digitalassetlinks.sampleapp2",
+ "sha256_cert_fingerprints": ["AA", "BB"]
+ }
+ }]
+ """,
+ ),
+ )
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(response)
+
+ val source = AssetDescriptor.Web("http://firefox.com")
+ assertEquals(
+ listOf(
+ Statement(
+ relation = Relation.HANDLE_ALL_URLS,
+ target = AssetDescriptor.Web("https://www.google.com"),
+ ),
+ Statement(
+ relation = Relation.USE_AS_ORIGIN,
+ target = AssetDescriptor.Web("https://www.google.com"),
+ ),
+ Statement(
+ relation = Relation.HANDLE_ALL_URLS,
+ target = AssetDescriptor.Android(
+ packageName = "org.digitalassetlinks.sampleapp",
+ sha256CertFingerprint = "10:39:38:EE:45:37:E5:9E:8E:E7:92:F6:54:50:4F:B8:34:6F:C6:B3:46:D0:BB:C4:41:5F:C3:39:FC:FC:8E:C1",
+ ),
+ ),
+ Statement(
+ relation = Relation.HANDLE_ALL_URLS,
+ target = AssetDescriptor.Android(
+ packageName = "org.digitalassetlinks.sampleapp2",
+ sha256CertFingerprint = "AA",
+ ),
+ ),
+ Statement(
+ relation = Relation.HANDLE_ALL_URLS,
+ target = AssetDescriptor.Android(
+ packageName = "org.digitalassetlinks.sampleapp2",
+ sha256CertFingerprint = "BB",
+ ),
+ ),
+ ),
+ listFetcher.listStatements(source).toList(),
+ )
+ }
+
+ @Test
+ fun `resolves include statements`() {
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(
+ Response(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ status = 200,
+ headers = jsonHeaders,
+ body = stringBody(
+ """
+ [{
+ "relation": ["delegate_permission/common.use_as_origin"],
+ "target": {
+ "namespace": "web",
+ "site": "https://www.google.com"
+ }
+ },{
+ "include": "https://example.com/includedstatements.json"
+ }]
+ """,
+ ),
+ ),
+ )
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "https://example.com/includedstatements.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(
+ Response(
+ url = "https://example.com/includedstatements.json",
+ status = 200,
+ headers = jsonHeaders,
+ body = stringBody(
+ """
+ [{
+ "relation": ["delegate_permission/common.use_as_origin"],
+ "target": {
+ "namespace": "web",
+ "site": "https://www.example.com"
+ }
+ }]
+ """,
+ ),
+ ),
+ )
+
+ val source = AssetDescriptor.Web("http://firefox.com")
+ assertEquals(
+ listOf(
+ Statement(
+ relation = Relation.USE_AS_ORIGIN,
+ target = AssetDescriptor.Web("https://www.google.com"),
+ ),
+ Statement(
+ relation = Relation.USE_AS_ORIGIN,
+ target = AssetDescriptor.Web("https://www.example.com"),
+ ),
+ ),
+ listFetcher.listStatements(source).toList(),
+ )
+ }
+
+ @Test
+ fun `no infinite loops`() {
+ `when`(
+ httpClient.fetch(
+ Request(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ connectTimeout = TIMEOUT,
+ readTimeout = TIMEOUT,
+ ),
+ ),
+ ).thenReturn(
+ Response(
+ url = "http://firefox.com/.well-known/assetlinks.json",
+ status = 200,
+ headers = jsonHeaders,
+ body = stringBody(
+ """
+ [{
+ "relation": ["delegate_permission/common.use_as_origin"],
+ "target": {
+ "namespace": "web",
+ "site": "https://example.com"
+ }
+ },{
+ "include": "http://firefox.com/.well-known/assetlinks.json"
+ }]
+ """,
+ ),
+ ),
+ )
+
+ val source = AssetDescriptor.Web("http://firefox.com")
+ assertEquals(
+ listOf(
+ Statement(
+ relation = Relation.USE_AS_ORIGIN,
+ target = AssetDescriptor.Web("https://example.com"),
+ ),
+ ),
+ listFetcher.listStatements(source).toList(),
+ )
+ }
+
+ private fun stringBody(data: String) = Response.Body(ByteArrayInputStream(data.toByteArray()))
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementRelationCheckerTest.kt b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementRelationCheckerTest.kt
new file mode 100644
index 0000000000..4498ab56a1
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/test/java/mozilla/components/service/digitalassetlinks/local/StatementRelationCheckerTest.kt
@@ -0,0 +1,89 @@
+/* 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.digitalassetlinks.local
+
+import mozilla.components.service.digitalassetlinks.AssetDescriptor
+import mozilla.components.service.digitalassetlinks.Relation
+import mozilla.components.service.digitalassetlinks.Statement
+import mozilla.components.service.digitalassetlinks.StatementListFetcher
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class StatementRelationCheckerTest {
+
+ @Test
+ fun `checks list lazily`() {
+ var numYields = 0
+ val target = AssetDescriptor.Web("https://mozilla.org")
+ val listFetcher = object : StatementListFetcher {
+ override fun listStatements(source: AssetDescriptor.Web) = sequence {
+ numYields = 1
+ yield(
+ Statement(
+ relation = Relation.USE_AS_ORIGIN,
+ target = target,
+ ),
+ )
+ numYields = 2
+ yield(
+ Statement(
+ relation = Relation.USE_AS_ORIGIN,
+ target = target,
+ ),
+ )
+ }
+ }
+
+ val checker = StatementRelationChecker(listFetcher)
+ assertEquals(0, numYields)
+
+ assertTrue(checker.checkRelationship(mock(), Relation.USE_AS_ORIGIN, target))
+ assertEquals(1, numYields)
+
+ // Sanity check that the mock can yield twice
+ numYields = 0
+ listFetcher.listStatements(mock()).toList()
+ assertEquals(2, numYields)
+ }
+
+ @Test
+ fun `fails if relation does not match`() {
+ val target = AssetDescriptor.Android("com.test", "AA:BB")
+ val listFetcher = object : StatementListFetcher {
+ override fun listStatements(source: AssetDescriptor.Web) = sequenceOf(
+ Statement(
+ relation = Relation.USE_AS_ORIGIN,
+ target = target,
+ ),
+ )
+ }
+
+ val checker = StatementRelationChecker(listFetcher)
+ assertFalse(checker.checkRelationship(mock(), Relation.HANDLE_ALL_URLS, target))
+ }
+
+ @Test
+ fun `fails if target does not match`() {
+ val target = AssetDescriptor.Web("https://mozilla.org")
+ val listFetcher = object : StatementListFetcher {
+ override fun listStatements(source: AssetDescriptor.Web) = sequenceOf(
+ Statement(
+ relation = Relation.HANDLE_ALL_URLS,
+ target = AssetDescriptor.Web("https://mozilla.com"),
+ ),
+ Statement(
+ relation = Relation.HANDLE_ALL_URLS,
+ target = AssetDescriptor.Web("http://mozilla.org"),
+ ),
+ )
+ }
+
+ val checker = StatementRelationChecker(listFetcher)
+ assertFalse(checker.checkRelationship(mock(), Relation.HANDLE_ALL_URLS, target))
+ }
+}
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/service/digitalassetlinks/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/digitalassetlinks/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/mobile/android/android-components/components/service/digitalassetlinks/src/test/resources/robolectric.properties b/mobile/android/android-components/components/service/digitalassetlinks/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/service/digitalassetlinks/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28