chromium/components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidatorTest.java

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.webapk.lib.client;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import static org.chromium.components.webapk.lib.common.WebApkMetaDataKeys.SCOPE;
import static org.chromium.components.webapk.lib.common.WebApkMetaDataKeys.SHELL_APK_VERSION;
import static org.chromium.components.webapk.lib.common.WebApkMetaDataKeys.START_URL;
import static org.chromium.components.webapk.lib.common.WebApkMetaDataKeys.WEB_MANIFEST_URL;

import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.os.Bundle;
import android.widget.TextView;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.shadows.ShadowToast;

import org.chromium.base.ContextUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.testing.local.TestDir;
import org.chromium.ui.widget.ToastManager;

import java.net.URISyntaxException;

/** Unit tests for {@link org.chromium.webapk.lib.client.WebApkValidator}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class WebApkValidatorTest {
    private static final String WEBAPK_PACKAGE_NAME = "org.chromium.webapk.foo";
    private static final String INVALID_WEBAPK_PACKAGE_NAME = "invalid.org.chromium.webapk.foo";
    private static final String URL_OF_WEBAPK = "https://www.foo.com";
    private static final String URL_WITHOUT_WEBAPK = "https://www.other.com";
    private static final String TEST_DATA_DIR = "webapks/";
    private static final String TEST_STARTURL = "https://non-empty.com/starturl";
    private static final String MAPSLITE_PACKAGE_NAME = "com.google.android.apps.mapslite";
    private static final String MAPSLITE_EXAMPLE_STARTURL = "https://www.google.com/maps";
    private static final String MANIFEST_URL = "https://www.foo.com/manifest.json";
    private static final int SHELL_VERSION = 100;

    private static final byte[] EXPECTED_SIGNATURE =
            new byte[] {
                48, -126, 3, -121, 48, -126, 2, 111, -96, 3, 2, 1, 2, 2, 4, 20, -104, -66, -126, 48,
                13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 116, 49, 11, 48, 9, 6, 3,
                85, 4, 6, 19, 2, 67, 65, 49, 16, 48, 14, 6, 3, 85, 4, 8, 19, 7, 79, 110, 116, 97,
                114, 105, 111, 49, 17, 48, 15, 6, 3, 85, 4, 7, 19, 8, 87, 97, 116, 101, 114, 108,
                111, 111, 49, 17, 48, 15, 6, 3, 85, 4, 10, 19, 8, 67, 104, 114, 111, 109, 105, 117,
                109, 49, 17, 48
            };

    private static final byte[] SIGNATURE_1 =
            new byte[] {
                13, 52, 51, 48, 51, 48, 51, 49, 53, 49, 54, 52, 52, 90, 48, 116, 49, 11, 48, 9, 6,
                3, 85, 4, 6, 19, 2, 67, 65, 49, 16, 48, 14, 6, 3, 85, 4, 8, 19, 7, 79, 110, 116, 97,
                114
            };

    private static final byte[] SIGNATURE_2 =
            new byte[] {
                49, 17, 48, 15, 6, 3, 85, 4, 10, 19, 8, 67, 104, 114, 111, 109, 105, 117, 109, 49,
                17, 48, 15, 6, 3, 85, 4, 11, 19, 8, 67, 104, 114, 111, 109, 105, 117, 109, 49, 26,
                48, 24
            };

    // This is the public key used for the test files (chrome/test/data/webapks/public.der)
    private static final byte[] PUBLIC_KEY =
            new byte[] {
                48, 89, 48, 19, 6, 7, 42, -122, 72, -50, 61, 2, 1, 6, 8, 42, -122, 72, -50, 61, 3,
                1, 7, 3, 66, 0, 4, -67, 14, 37, -20, 103, 121, 124, -60, -21, 83, -114, -120, -87,
                -38, 26, 78, 82, 55, 44, -23, -2, 104, 115, 82, -55, -104, 105, -19, -48, 89, -65,
                12, -31, 16, -35, 4, -121, -70, -89, 23, 56, 115, 112, 78, -65, 114, -103, 120, -88,
                -112, -102, -61, 72, -16, 74, 53, 50, 49, -56, -48, -90, 5, -116, 78
            };

    private ShadowPackageManager mPackageManager;

    @Before
    public void setUp() {
        mPackageManager = Shadows.shadowOf(RuntimeEnvironment.application.getPackageManager());
        WebApkValidator.init(EXPECTED_SIGNATURE, PUBLIC_KEY);
    }

    @After
    public void tearDown() {
        ToastManager.resetForTesting();
        ShadowToast.reset();
    }

    /**
     * Tests {@link WebApkValidator.queryFirstWebApkPackage()} returns a WebAPK's package name if
     * the WebAPK can handle the given URL and the WebAPK is valid.
     */
    @Test
    public void testQueryWebApkPackageReturnsPackageIfTheURLCanBeHandled() {
        try {
            Intent intent = Intent.parseUri(URL_OF_WEBAPK, Intent.URI_INTENT_SCHEME);
            intent.addCategory(Intent.CATEGORY_BROWSABLE);

            mPackageManager.addResolveInfoForIntent(intent, newResolveInfo(WEBAPK_PACKAGE_NAME));
            mPackageManager.addPackage(
                    newPackageInfoWithBrowserSignature(
                            WEBAPK_PACKAGE_NAME,
                            new Signature(EXPECTED_SIGNATURE),
                            TEST_STARTURL,
                            null));

            assertEquals(
                    WEBAPK_PACKAGE_NAME,
                    WebApkValidator.queryFirstWebApkPackage(
                            RuntimeEnvironment.application, URL_OF_WEBAPK));
        } catch (URISyntaxException e) {
            Assert.fail("URI is invalid.");
        }
    }

    /**
     * Tests {@link WebApkValidator.queryFirstWebApkPackage()} returns null for a non-browsable
     * Intent.
     */
    @Test
    public void testQueryWebApkPackageReturnsNullForNonBrowsableIntent() {
        try {
            Intent intent = Intent.parseUri(URL_OF_WEBAPK, Intent.URI_INTENT_SCHEME);

            mPackageManager.addResolveInfoForIntent(intent, newResolveInfo(WEBAPK_PACKAGE_NAME));
            mPackageManager.addPackage(
                    newPackageInfoWithBrowserSignature(
                            WEBAPK_PACKAGE_NAME,
                            new Signature(EXPECTED_SIGNATURE),
                            TEST_STARTURL,
                            null));

            assertNull(
                    WebApkValidator.queryFirstWebApkPackage(
                            RuntimeEnvironment.application, URL_OF_WEBAPK));
        } catch (URISyntaxException e) {
            Assert.fail("URI is invalid.");
        }
    }

    /**
     * Tests {@link WebApkValidator.queryFirstWebApkPackage()} returns null if no WebAPK handles the
     * given URL.
     */
    @Test
    public void testQueryWebApkPackageReturnsNullWhenNoWebApkHandlesTheURL() {
        try {
            Intent intent = Intent.parseUri(URL_OF_WEBAPK, Intent.URI_INTENT_SCHEME);
            intent.addCategory(Intent.CATEGORY_BROWSABLE);

            mPackageManager.addResolveInfoForIntent(intent, newResolveInfo(WEBAPK_PACKAGE_NAME));
            mPackageManager.addPackage(
                    newPackageInfoWithBrowserSignature(
                            WEBAPK_PACKAGE_NAME,
                            new Signature(EXPECTED_SIGNATURE),
                            TEST_STARTURL,
                            null));

            assertNull(
                    WebApkValidator.queryFirstWebApkPackage(
                            RuntimeEnvironment.application, URL_WITHOUT_WEBAPK));
        } catch (URISyntaxException e) {
            Assert.fail("URI is invalid.");
        }
    }

    /**
     * Tests {@link WebApkValidator.canWebApkHandleUrl()} returns true if the WebAPK can handle the
     * given URL and the WebAPK is valid.
     */
    @Test
    public void testCanWebApkHandleUrlReturnsTrueIfTheURLCanBeHandled() {
        try {
            Intent intent = Intent.parseUri(URL_OF_WEBAPK, Intent.URI_INTENT_SCHEME);
            intent.addCategory(Intent.CATEGORY_BROWSABLE);
            intent.setPackage(WEBAPK_PACKAGE_NAME);

            mPackageManager.addResolveInfoForIntent(intent, newResolveInfo(WEBAPK_PACKAGE_NAME));
            mPackageManager.addPackage(
                    newPackageInfoWithBrowserSignature(
                            WEBAPK_PACKAGE_NAME,
                            new Signature(EXPECTED_SIGNATURE),
                            TEST_STARTURL,
                            null));

            assertTrue(
                    WebApkValidator.canWebApkHandleUrl(
                            RuntimeEnvironment.application, WEBAPK_PACKAGE_NAME, URL_OF_WEBAPK, 0));
        } catch (URISyntaxException e) {
            Assert.fail("URI is invalid.");
        }
    }

    /**
     * Tests {@link WebApkValidator.canWebApkHandleUrl()} returns false if the given APK package
     * name is not signed with the WebAPK signature.
     */
    @Test
    public void testCanWebApkHandleUrlReturnsFalseIfWebApkIsNotValid() {
        try {
            Intent intent = Intent.parseUri(URL_OF_WEBAPK, Intent.URI_INTENT_SCHEME);
            intent.addCategory(Intent.CATEGORY_BROWSABLE);
            intent.setPackage(WEBAPK_PACKAGE_NAME);

            mPackageManager.addResolveInfoForIntent(intent, newResolveInfo(WEBAPK_PACKAGE_NAME));
            mPackageManager.addPackage(
                    newPackageInfoWithBrowserSignature(
                            WEBAPK_PACKAGE_NAME, new Signature(SIGNATURE_1), TEST_STARTURL, null));

            assertFalse(
                    WebApkValidator.canWebApkHandleUrl(
                            RuntimeEnvironment.application, WEBAPK_PACKAGE_NAME, URL_OF_WEBAPK, 0));
        } catch (URISyntaxException e) {
            Assert.fail("URI is invalid.");
        }
    }

    /** Tests {@link WebApkValidator.canWebApkHandleUrl()} returns false for a non-browsable WebAPK. */
    @Test
    public void testCanWebApkHandleUrlReturnsFalseForNonBrowsableIntent() {
        try {
            Intent intent = Intent.parseUri(URL_OF_WEBAPK, Intent.URI_INTENT_SCHEME);
            intent.setPackage(WEBAPK_PACKAGE_NAME);

            mPackageManager.addResolveInfoForIntent(intent, newResolveInfo(WEBAPK_PACKAGE_NAME));
            mPackageManager.addPackage(
                    newPackageInfoWithBrowserSignature(
                            WEBAPK_PACKAGE_NAME,
                            new Signature(EXPECTED_SIGNATURE),
                            TEST_STARTURL,
                            null));

            assertFalse(
                    WebApkValidator.canWebApkHandleUrl(
                            RuntimeEnvironment.application, WEBAPK_PACKAGE_NAME, URL_OF_WEBAPK, 0));
        } catch (URISyntaxException e) {
            Assert.fail("URI is invalid.");
        }
    }

    /**
     * Tests {@link WebApkValidator.canWebApkHandleUrl()} returns false if the specific WebAPK does
     * not handle the given URL.
     */
    @Test
    public void testCanWebApkHandleUrlReturnsFalseWhenNoWebApkHandlesTheURL() {
        try {
            Intent intent = Intent.parseUri(URL_OF_WEBAPK, Intent.URI_INTENT_SCHEME);
            intent.addCategory(Intent.CATEGORY_BROWSABLE);
            intent.setPackage(WEBAPK_PACKAGE_NAME);

            mPackageManager.addResolveInfoForIntent(intent, newResolveInfo(WEBAPK_PACKAGE_NAME));
            mPackageManager.addPackage(
                    newPackageInfoWithBrowserSignature(
                            WEBAPK_PACKAGE_NAME,
                            new Signature(EXPECTED_SIGNATURE),
                            TEST_STARTURL,
                            null));

            assertFalse(
                    WebApkValidator.canWebApkHandleUrl(
                            RuntimeEnvironment.application,
                            WEBAPK_PACKAGE_NAME,
                            URL_WITHOUT_WEBAPK,
                            0));
        } catch (URISyntaxException e) {
            Assert.fail("URI is invalid.");
        }
    }

    /**
     * Tests {@link WebApkValidator.isValidWebApk} returns true if a package name corresponds to a
     * WebAPK and the WebAPK is valid.
     */
    @Test
    public void testIsValidWebApkReturnsTrueForValidWebApk() {
        mPackageManager.addPackage(
                newPackageInfoWithBrowserSignature(
                        WEBAPK_PACKAGE_NAME,
                        new Signature(EXPECTED_SIGNATURE),
                        TEST_STARTURL,
                        null));

        assertTrue(
                WebApkValidator.isValidWebApk(RuntimeEnvironment.application, WEBAPK_PACKAGE_NAME));
    }

    /**
     * Tests {@link WebApkValidator.isValidWebApk} returns false if the package name is not valid
     * for WebApks (and isn't comment-signed).
     */
    @Test
    public void testIsValidWebApkFalseForInvalidPackageName() {
        mPackageManager.addPackage(
                newPackageInfoWithBrowserSignature(
                        INVALID_WEBAPK_PACKAGE_NAME,
                        new Signature(EXPECTED_SIGNATURE),
                        TEST_STARTURL,
                        null));

        assertFalse(
                WebApkValidator.isValidWebApk(
                        RuntimeEnvironment.application, INVALID_WEBAPK_PACKAGE_NAME));
    }

    /**
     * Tests {@link WebApkValidator.isValidWebApk} returns true if the package name is maps lite and
     * the start url matches the correct prefix.
     */
    @Test
    public void testIsValidWebApkForMapsLite() {
        mPackageManager.addPackage(
                newPackageInfoWithBrowserSignature(
                        MAPSLITE_PACKAGE_NAME,
                        new Signature(SIGNATURE_1),
                        MAPSLITE_EXAMPLE_STARTURL,
                        null));
        mPackageManager.addPackage(
                newPackageInfoWithBrowserSignature(
                        MAPSLITE_PACKAGE_NAME + ".other",
                        new Signature(SIGNATURE_1),
                        MAPSLITE_EXAMPLE_STARTURL,
                        null));

        assertTrue(
                WebApkValidator.isValidWebApk(
                        RuntimeEnvironment.application, MAPSLITE_PACKAGE_NAME));
        assertFalse(
                WebApkValidator.isValidWebApk(
                        RuntimeEnvironment.application, MAPSLITE_PACKAGE_NAME + ".other"));
        assertFalse(
                WebApkValidator.isValidWebApk(
                        RuntimeEnvironment.application, MAPSLITE_PACKAGE_NAME + ".notfound"));
    }

    /** Tests {@link WebApkValidator.canWebApkHandleUrl} returns false and shows a toast. */
    @Test
    public void testMapsLiteWebApkShowsWarning() {
        // Invalid MapsLite WebAPK is not verified and does not show toast.
        addWebApkResolveInfoWithPackageName(
                MAPSLITE_EXAMPLE_STARTURL, MAPSLITE_PACKAGE_NAME + ".other", SIGNATURE_1);
        assertFalse(
                WebApkValidator.canWebApkHandleUrl(
                        RuntimeEnvironment.application,
                        MAPSLITE_PACKAGE_NAME + ".other",
                        MAPSLITE_EXAMPLE_STARTURL,
                        0));
        assertNull(ShadowToast.getLatestToast());

        // Valid MapsLite WebAPK returns false as "not handled" and shows a toast.
        addWebApkResolveInfoWithPackageName(
                MAPSLITE_EXAMPLE_STARTURL, MAPSLITE_PACKAGE_NAME, SIGNATURE_1);
        assertFalse(
                WebApkValidator.canWebApkHandleUrl(
                        RuntimeEnvironment.application,
                        MAPSLITE_PACKAGE_NAME,
                        MAPSLITE_EXAMPLE_STARTURL,
                        0));
        assertNotNull(ShadowToast.getLatestToast());
        // assertTextFromLatestToast(R.string.copied);
        TextView textView = (TextView) ShadowToast.getLatestToast().getView();
        String actualText = textView == null ? "" : textView.getText().toString();
        assertEquals(
                ContextUtils.getApplicationContext()
                        .getString(R.string.webapk_mapsgo_deprecation_warning, ""),
                actualText);
    }

    /**
     * Tests {@link WebApkValidator.canWebApkHandleUrl} returns false and shows a toast when the
     * shell version is out-of-date (older than the min_version).
     */
    @Test
    public void testOldShellWebApkShowsWarning() {
        addWebApkResolveInfoWithPackageName(URL_OF_WEBAPK, WEBAPK_PACKAGE_NAME, EXPECTED_SIGNATURE);

        // Current Shell Version larger than min_version, can handle URL.
        assertTrue(
                WebApkValidator.canWebApkHandleUrl(
                        RuntimeEnvironment.application,
                        WEBAPK_PACKAGE_NAME,
                        URL_OF_WEBAPK,
                        SHELL_VERSION - 1));
        assertNull(ShadowToast.getLatestToast());

        // Current Shell Version smaller than min_version, returns false as "not handled" and shows
        // a toast.
        assertFalse(
                WebApkValidator.canWebApkHandleUrl(
                        RuntimeEnvironment.application,
                        WEBAPK_PACKAGE_NAME,
                        URL_OF_WEBAPK,
                        SHELL_VERSION + 1));
        assertNotNull(ShadowToast.getLatestToast());
        // assertTextFromLatestToast(R.string.copied);
        TextView textView = (TextView) ShadowToast.getLatestToast().getView();
        String actualText = textView == null ? "" : textView.getText().toString();
        assertEquals(
                ContextUtils.getApplicationContext()
                        .getString(R.string.webapk_deprecation_warning, ""),
                actualText);
    }

    /**
     * Tests {@link WebApkValidator.isValidWebApk} returns false when the startUrl is not correct.
     */
    @Test
    public void testIsNotValidWebApkForMapsLiteBadStartUrl() {
        mPackageManager.addPackage(
                newPackageInfoWithBrowserSignature(
                        MAPSLITE_PACKAGE_NAME, new Signature(SIGNATURE_1), TEST_STARTURL, null));
        assertFalse(
                WebApkValidator.isValidWebApk(
                        RuntimeEnvironment.application, MAPSLITE_PACKAGE_NAME));
    }

    /**
     * Tests {@link WebApkValidator.isValidWebApk} returns false if a WebAPK has more than 2
     * signatures, even if the second one matches the expected signature.
     */
    @Test
    public void testIsValidWebApkReturnsFalseForMoreThanTwoSignatures() {
        Signature[] signatures =
                new Signature[] {
                    new Signature(SIGNATURE_1),
                    new Signature(EXPECTED_SIGNATURE),
                    new Signature(SIGNATURE_2)
                };
        mPackageManager.addPackage(
                newPackageInfo(WEBAPK_PACKAGE_NAME, signatures, null, TEST_STARTURL, null));

        assertFalse(
                WebApkValidator.isValidWebApk(RuntimeEnvironment.application, WEBAPK_PACKAGE_NAME));
    }

    /**
     * Tests {@link WebApkValidator.isValidWebApk} returns false if a WebAPK has multiple signatures
     * but none of the signatures match the expected signature.
     */
    @Test
    public void testIsValidWebApkReturnsFalseForWebApkWithMultipleSignaturesWithoutAnyMatched() {
        Signature[] signatures =
                new Signature[] {new Signature(SIGNATURE_1), new Signature(SIGNATURE_2)};
        mPackageManager.addPackage(
                newPackageInfo(WEBAPK_PACKAGE_NAME, signatures, null, TEST_STARTURL, null));

        assertFalse(
                WebApkValidator.isValidWebApk(RuntimeEnvironment.application, WEBAPK_PACKAGE_NAME));
    }

    /** Tests {@link WebApkValidator#isValidWebApk()} for valid comment signed webapks. */
    @Test
    public void testIsValidWebApkCommentSigned() {
        String[] filenames = {"example.apk", "java-example.apk", "v2-signed-ok.apk"};
        String packageName = "com.webapk.a9c419502bb98fcb7";
        Signature[] signature = new Signature[] {new Signature(SIGNATURE_1)};

        for (String filename : filenames) {
            mPackageManager.removePackage(packageName);
            mPackageManager.addPackage(
                    newPackageInfo(
                            packageName, signature, testFilePath(filename), TEST_STARTURL, null));
            assertTrue(
                    filename + " did not verify",
                    WebApkValidator.isValidWebApk(RuntimeEnvironment.application, packageName));
        }
    }

    /**
     * Tests {@link WebApkValidator#isValidWebApk()} for failing comment signed webapks. These
     * WebAPKs were modified to fail in specific ways.
     */
    @Test
    public void testIsValidWebApkCommentSignedFailures() {
        String[] filenames = {
            "bad-sig.apk",
            "bad-utf8-fname.apk",
            "empty.apk",
            "extra-len-too-large.apk",
            "fcomment-too-large.apk",
            "no-cd.apk",
            "no-comment.apk",
            "no-eocd.apk",
            "no-lfh.apk",
            "not-an.apk",
            "too-many-metainf.apk",
            "truncated.apk",
            "zeros.apk",
            "zeros-at-end.apk",
            "block-before-first.apk",
            "block-at-end.apk",
            "block-before-eocd.apk",
            "block-before-cd.apk",
            "block-middle.apk",
            "v2-signed-too-large.apk",
        };
        String packageName = "com.webapk.a9c419502bb98fcb7";
        Signature[] signature = new Signature[] {new Signature(SIGNATURE_1)};

        for (String filename : filenames) {
            mPackageManager.removePackage(packageName);
            mPackageManager.addPackage(
                    newPackageInfo(
                            packageName, signature, testFilePath(filename), TEST_STARTURL, null));
            assertFalse(
                    filename,
                    WebApkValidator.isValidWebApk(RuntimeEnvironment.application, packageName));
        }
    }

    /**
     * Tests {@link WebApkValidator.isValidV1WebApk} returns true if a package name corresponds to a
     * WebAPK and the WebAPK is valid.
     */
    @Test
    public void testIsValidV1WebApkReturnsTrueForValidWebApk() {
        mPackageManager.addPackage(
                newPackageInfoWithBrowserSignature(
                        WEBAPK_PACKAGE_NAME,
                        new Signature(EXPECTED_SIGNATURE),
                        TEST_STARTURL,
                        null));

        assertTrue(
                WebApkValidator.isValidV1WebApk(
                        RuntimeEnvironment.application, WEBAPK_PACKAGE_NAME));
    }

    /**
     * Tests {@link WebApkValidator.isValidV1WebApk} returns false if the package name is not valid
     * for WebApks.
     */
    @Test
    public void testIsValidV1WebApkFalseForInvalidPackageName() {
        mPackageManager.addPackage(
                newPackageInfoWithBrowserSignature(
                        INVALID_WEBAPK_PACKAGE_NAME,
                        new Signature(EXPECTED_SIGNATURE),
                        TEST_STARTURL,
                        null));

        assertFalse(
                WebApkValidator.isValidV1WebApk(
                        RuntimeEnvironment.application, INVALID_WEBAPK_PACKAGE_NAME));
    }

    /** Tests {@link WebApkValidator.isValidV1WebApk} returns false if the package name is maps lite. */
    @Test
    public void testIsValidV1WebApkFalseForMapsLite() {
        mPackageManager.addPackage(
                newPackageInfoWithBrowserSignature(
                        MAPSLITE_PACKAGE_NAME,
                        new Signature(SIGNATURE_1),
                        MAPSLITE_EXAMPLE_STARTURL,
                        null));
        mPackageManager.addPackage(
                newPackageInfoWithBrowserSignature(
                        MAPSLITE_PACKAGE_NAME + ".other",
                        new Signature(SIGNATURE_1),
                        MAPSLITE_EXAMPLE_STARTURL,
                        null));

        assertFalse(
                WebApkValidator.isValidV1WebApk(
                        RuntimeEnvironment.application, MAPSLITE_PACKAGE_NAME));
    }

    /** Tests {@link WebApkValidator#queryBoundWebApkForManifestUrl()} for a valid installed entry. */
    @Test
    public void testQueryBoundWebApkForManifestUrl() {
        mPackageManager.addPackage(
                newPackageInfoWithBrowserSignature(
                        WEBAPK_PACKAGE_NAME,
                        new Signature(EXPECTED_SIGNATURE),
                        null,
                        MANIFEST_URL));

        assertEquals(
                WEBAPK_PACKAGE_NAME,
                WebApkValidator.queryBoundWebApkForManifestUrl(
                        RuntimeEnvironment.application, MANIFEST_URL));
    }

    /** Tests {@link WebApkValidator#queryBoundWebApkForManifestUrl()} with an invalid package name. */
    @Test
    public void testQueryBoundWebApkForManifestUrlWithInvalidPackageName() {
        mPackageManager.addPackage(
                newPackageInfoWithBrowserSignature(
                        INVALID_WEBAPK_PACKAGE_NAME,
                        new Signature(EXPECTED_SIGNATURE),
                        null,
                        MANIFEST_URL));

        assertNull(
                WebApkValidator.queryBoundWebApkForManifestUrl(
                        RuntimeEnvironment.application, MANIFEST_URL));
    }

    /** Tests {@link WebApkValidator#queryBoundWebApkForManifestUrl()} with an invalid signature. */
    @Test
    public void testQueryBoundWebApkForManifestUrlWithInvalidSignature() {
        mPackageManager.addPackage(
                newPackageInfoWithBrowserSignature(
                        WEBAPK_PACKAGE_NAME, new Signature(SIGNATURE_1), null, MANIFEST_URL));

        assertNull(
                WebApkValidator.queryBoundWebApkForManifestUrl(
                        RuntimeEnvironment.application, MANIFEST_URL));
    }

    /** Tests {@link WebApkValidator#queryBoundWebApkForManifestUrl()} with an invalid manifest URL. */
    @Test
    public void testQueryBoundWebApkForManifestUrlWithInvalidManifestUrl() {
        mPackageManager.addPackage(
                newPackageInfoWithBrowserSignature(
                        WEBAPK_PACKAGE_NAME, new Signature(SIGNATURE_1), null, MANIFEST_URL));

        assertNull(
                WebApkValidator.queryBoundWebApkForManifestUrl(
                        RuntimeEnvironment.application, "https://evil.com/manifest.json"));
    }

    /**
     * Tests when override validation is set, {@link WebApkValidator.isValidWebApk} returns true
     * with invalid signature.
     */
    @Test
    public void testIsValidWebApkWithOverridesSignature() {
        mPackageManager.addPackage(
                newPackageInfoWithBrowserSignature(
                        WEBAPK_PACKAGE_NAME, new Signature(SIGNATURE_1), TEST_STARTURL, null));

        assertFalse(
                WebApkValidator.isValidWebApk(RuntimeEnvironment.application, WEBAPK_PACKAGE_NAME));

        WebApkValidator.setDisableValidationForTesting(true);
        assertTrue(
                WebApkValidator.isValidWebApk(RuntimeEnvironment.application, WEBAPK_PACKAGE_NAME));
    }

    /**
     * Tests when override validation is set, {@link WebApkValidator.isValidWebApk} returns false
     * when no START_URL.
     */
    @Test
    public void testIsValidWebApkOverridesReturnsFalseNoStartUrl() {
        PackageInfo webapkPackage =
                newPackageInfoWithBrowserSignature(
                        WEBAPK_PACKAGE_NAME,
                        new Signature(EXPECTED_SIGNATURE),
                        TEST_STARTURL,
                        null);
        webapkPackage.applicationInfo.metaData.remove(START_URL);
        mPackageManager.addPackage(webapkPackage);

        WebApkValidator.setDisableValidationForTesting(true);
        assertFalse(
                WebApkValidator.isValidWebApk(RuntimeEnvironment.application, WEBAPK_PACKAGE_NAME));
    }

    /**
     * Tests when override validation is set, {@link WebApkValidator.isValidWebApk} returns false
     * when package is not installed.
     */
    @Test
    public void testIsValidWebApkOverridesPackageNotFound() {
        WebApkValidator.setDisableValidationForTesting(true);

        assertFalse(
                WebApkValidator.isValidWebApk(
                        RuntimeEnvironment.application, INVALID_WEBAPK_PACKAGE_NAME));
    }

    @Test
    public void testQueryWebApkResolveInfoWithPackageName() {
        addWebApkResolveInfoWithPackageName(URL_OF_WEBAPK, WEBAPK_PACKAGE_NAME, EXPECTED_SIGNATURE);

        ResolveInfo resolveInfo =
                WebApkValidator.queryFirstWebApkResolveInfo(
                        RuntimeEnvironment.application, URL_OF_WEBAPK, WEBAPK_PACKAGE_NAME);

        assertNotNull(resolveInfo);
        assertEquals(resolveInfo.activityInfo.packageName, WEBAPK_PACKAGE_NAME);
    }

    @Test
    public void testQueryWebApkResolveInfoWithInvalidPackageName() {
        addWebApkResolveInfoWithPackageName(
                URL_OF_WEBAPK, INVALID_WEBAPK_PACKAGE_NAME, EXPECTED_SIGNATURE);

        ResolveInfo resolveInfo =
                WebApkValidator.queryFirstWebApkResolveInfo(
                        RuntimeEnvironment.application, URL_OF_WEBAPK, INVALID_WEBAPK_PACKAGE_NAME);

        assertNull(resolveInfo);
    }

    @Test
    public void testQueryWebApkResolveInfoWithInvalidSignature() {
        addWebApkResolveInfoWithPackageName(URL_OF_WEBAPK, WEBAPK_PACKAGE_NAME, SIGNATURE_1);

        ResolveInfo resolveInfo =
                WebApkValidator.queryFirstWebApkResolveInfo(
                        RuntimeEnvironment.application, URL_OF_WEBAPK, WEBAPK_PACKAGE_NAME);

        assertNull(resolveInfo);
    }

    // Get the full test file path.
    private static String testFilePath(String fileName) {
        return TestDir.getTestFilePath(TEST_DATA_DIR + fileName);
    }

    private static ResolveInfo newResolveInfo(String packageName) {
        ActivityInfo activityInfo = new ActivityInfo();
        activityInfo.packageName = packageName;
        ResolveInfo resolveInfo = new ResolveInfo();
        resolveInfo.activityInfo = activityInfo;
        return resolveInfo;
    }

    private static PackageInfo newPackageInfo(
            String packageName,
            Signature[] signatures,
            String sourceDir,
            String startUrl,
            String manifestUrl) {
        PackageInfo packageInfo = new PackageInfo();
        packageInfo.packageName = packageName;
        packageInfo.signatures = signatures;
        packageInfo.applicationInfo = new ApplicationInfo();
        packageInfo.applicationInfo.metaData = new Bundle();
        packageInfo.applicationInfo.metaData.putString(START_URL, startUrl + "?morestuff");
        packageInfo.applicationInfo.metaData.putString(SCOPE, startUrl);
        packageInfo.applicationInfo.metaData.putString(WEB_MANIFEST_URL, manifestUrl);
        packageInfo.applicationInfo.metaData.putInt(SHELL_APK_VERSION, SHELL_VERSION);
        packageInfo.applicationInfo.sourceDir = sourceDir;
        return packageInfo;
    }

    // The browser signature is expected to always be the second signature - the first (and any
    // additional ones after the second) are ignored.
    private static PackageInfo newPackageInfoWithBrowserSignature(
            String packageName, Signature signature, String startUrl, String manifestUrl) {
        return newPackageInfo(
                packageName,
                new Signature[] {new Signature(""), signature},
                null,
                startUrl,
                manifestUrl);
    }

    private void addWebApkResolveInfoWithPackageName(
            String startUrl, String packageName, byte[] signature) {
        try {
            Intent intent = Intent.parseUri(startUrl, Intent.URI_INTENT_SCHEME);
            intent.addCategory(Intent.CATEGORY_BROWSABLE);
            intent.setPackage(packageName);

            mPackageManager.addResolveInfoForIntent(intent, newResolveInfo(packageName));
            mPackageManager.addPackage(
                    newPackageInfoWithBrowserSignature(
                            packageName, new Signature(signature), startUrl, null));
        } catch (URISyntaxException e) {
            Assert.fail("URI is invalid.");
        }
    }
}