diff options
Diffstat (limited to 'mobile/android/android-components/components/service/digitalassetlinks/src/test')
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 |