chromium/chrome/android/junit/src/org/chromium/chrome/browser/searchwidget/SearchActivityUtilsUnitTest.java

// Copyright 2024 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.chrome.browser.searchwidget;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;

import android.app.Activity;
import android.app.SearchManager;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.Robolectric;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

import org.chromium.base.ContextUtils;
import org.chromium.base.IntentUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.omnibox.suggestions.OmniboxLoadUrlParams;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityExtras;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityExtras.IntentOrigin;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityExtras.SearchType;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.content_public.common.ResourceRequestBodyJni;
import org.chromium.ui.base.PageTransition;
import org.chromium.url.GURL;

import java.util.List;

@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {SearchActivityUtilsUnitTest.ShadowUrlFormatter.class})
public class SearchActivityUtilsUnitTest {
    // Placeholder Activity class that guarantees the PackageName is valid for IntentUtils.
    private static class TestActivity extends Activity {}

    private static final GURL GOOD_URL = new GURL("https://abc.xyz");
    private static final GURL EMPTY_URL = GURL.emptyGURL();
    private static final OmniboxLoadUrlParams LOAD_URL_PARAMS_NULL_URL =
            new OmniboxLoadUrlParams.Builder(null, PageTransition.TYPED).build();
    private static final OmniboxLoadUrlParams LOAD_URL_PARAMS_INVALID_URL =
            new OmniboxLoadUrlParams.Builder("abcde", PageTransition.TYPED).build();
    private static final ComponentName COMPONENT_TRUSTED =
            new ComponentName(ContextUtils.getApplicationContext(), SearchActivity.class);
    private static final ComponentName COMPONENT_UNTRUSTED =
            new ComponentName("com.some.package", "com.some.package.test.Activity");

    private Activity mActivity = Robolectric.buildActivity(TestActivity.class).setup().get();

    // UrlFormatter call intercepting mock.
    private interface TestUrlFormatter {
        GURL fixupUrl(String uri);
    }

    @Implements(UrlFormatter.class)
    public static class ShadowUrlFormatter {
        static TestUrlFormatter sMockFormatter;

        @Implementation
        public static GURL fixupUrl(String uri) {
            return sMockFormatter.fixupUrl(uri);
        }
    }

    public @Rule MockitoRule mMockitoRule = MockitoJUnit.rule();
    public @Rule JniMocker mJniMocker = new JniMocker();
    private @Mock TestUrlFormatter mFormatter;
    private @Mock ResourceRequestBodyJni mResourceRequestBodyJni;

    @Before
    public void setUp() {
        ShadowUrlFormatter.sMockFormatter = mFormatter;
        mJniMocker.mock(ResourceRequestBodyJni.TEST_HOOKS, mResourceRequestBodyJni);
        doAnswer(i -> i.getArgument(0))
                .when(mResourceRequestBodyJni)
                .createResourceRequestBodyFromBytes(any());

        doAnswer(i -> new GURL(i.getArgument(0))).when(mFormatter).fixupUrl(any());
    }

    private OmniboxLoadUrlParams.Builder getLoadUrlParamsBuilder() {
        return new OmniboxLoadUrlParams.Builder("https://abc.xyz", PageTransition.TYPED);
    }

    @Test
    public void getIntentOrigin_trustedIntent() {
        SearchActivityClientImpl.requestOmniboxForResult(mActivity, EMPTY_URL, null);

        var intent = Shadows.shadowOf(mActivity).getNextStartedActivityForResult().intent;
        assertEquals(IntentOrigin.CUSTOM_TAB, SearchActivityUtils.getIntentOrigin(intent));
    }

    @Test
    public void getIntentOrigin_untrustedIntent() {
        SearchActivityClientImpl.requestOmniboxForResult(mActivity, EMPTY_URL, null);

        var intent = Shadows.shadowOf(mActivity).getNextStartedActivityForResult().intent;
        intent.removeExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA);
        assertEquals(IntentOrigin.UNKNOWN, SearchActivityUtils.getIntentOrigin(intent));
    }

    @Test
    public void getIntentSearchType_trustedIntent() {
        SearchActivityClientImpl.requestOmniboxForResult(mActivity, EMPTY_URL, null);

        var intent = Shadows.shadowOf(mActivity).getNextStartedActivityForResult().intent;
        assertEquals(IntentOrigin.CUSTOM_TAB, SearchActivityUtils.getIntentOrigin(intent));

        // Invalid variants
        intent.setAction(null);
        assertEquals(SearchType.TEXT, SearchActivityUtils.getIntentSearchType(intent));

        intent.setAction("abcd");
        assertEquals(SearchType.TEXT, SearchActivityUtils.getIntentSearchType(intent));
    }

    @Test
    public void getIntentSearchType_untrustedIntent() {
        SearchActivityClientImpl.requestOmniboxForResult(mActivity, EMPTY_URL, null);

        var intent = Shadows.shadowOf(mActivity).getNextStartedActivityForResult().intent;
        intent.removeExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA);
        assertEquals(SearchType.TEXT, SearchActivityUtils.getIntentSearchType(intent));
    }

    @Test
    public void getIntentUrl_forNullUrl() {
        SearchActivityClientImpl.requestOmniboxForResult(mActivity, null, null);
        var intent = Shadows.shadowOf(mActivity).getNextStartedActivityForResult().intent;
        assertNull(SearchActivityUtils.getIntentUrl(intent));
        // Remove trust
        intent.removeExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA);
        assertNull(SearchActivityUtils.getIntentUrl(intent));
    }

    @Test
    public void getIntentUrl_forEmptyUrl() {
        SearchActivityClientImpl.requestOmniboxForResult(mActivity, GURL.emptyGURL(), null);
        var intent = Shadows.shadowOf(mActivity).getNextStartedActivityForResult().intent;
        assertNull(SearchActivityUtils.getIntentUrl(intent));
        // Remove trust
        intent.removeExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA);
        assertNull(SearchActivityUtils.getIntentUrl(intent));
    }

    @Test
    public void getIntentUrl_forInvalidUrl() {
        SearchActivityClientImpl.requestOmniboxForResult(mActivity, new GURL("abcd"), null);
        var intent = Shadows.shadowOf(mActivity).getNextStartedActivityForResult().intent;
        assertNull(SearchActivityUtils.getIntentUrl(intent));
        // Remove trust
        intent.removeExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA);
        assertNull(SearchActivityUtils.getIntentUrl(intent));
    }

    @Test
    public void getIntentUrl_forValidUrl() {
        SearchActivityClientImpl.requestOmniboxForResult(
                mActivity, new GURL("https://abc.xyz"), null);
        var intent = Shadows.shadowOf(mActivity).getNextStartedActivityForResult().intent;
        assertEquals("https://abc.xyz/", SearchActivityUtils.getIntentUrl(intent).getSpec());
        // Remove trust
        intent.removeExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA);
        assertNull(SearchActivityUtils.getIntentUrl(intent));
    }

    @Test
    public void getIntentSearchType_emptyPackageName() {
        SearchActivityClientImpl.requestOmniboxForResult(mActivity, GOOD_URL, "");

        var intent = Shadows.shadowOf(mActivity).getNextStartedActivityForResult().intent;
        assertEquals(IntentOrigin.CUSTOM_TAB, SearchActivityUtils.getIntentOrigin(intent));
        assertNull(SearchActivityUtils.getReferrer(intent));

        // Remove trust
        intent.removeExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA);
        assertNull(SearchActivityUtils.getReferrer(intent));
    }

    @Test
    public void getIntentSearchType_nullPackageName() {
        SearchActivityClientImpl.requestOmniboxForResult(mActivity, GOOD_URL, null);

        var intent = Shadows.shadowOf(mActivity).getNextStartedActivityForResult().intent;
        assertEquals(IntentOrigin.CUSTOM_TAB, SearchActivityUtils.getIntentOrigin(intent));
        assertNull(SearchActivityUtils.getReferrer(intent));

        // Remove trust
        intent.removeExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA);
        assertNull(SearchActivityUtils.getReferrer(intent));
    }

    @Test
    public void getIntentSearchType_validPackageName() {
        var cases = List.of("ab", "a.b", "a-b", "0.9", "a.0", "k-9", "A_Z", "ABC123");

        for (var testCase : cases) {
            SearchActivityClientImpl.requestOmniboxForResult(mActivity, GOOD_URL, testCase);
            var intent = Shadows.shadowOf(mActivity).getNextStartedActivityForResult().intent;
            assertEquals(testCase, SearchActivityUtils.getReferrer(intent));
            // Remove trust
            intent.removeExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA);
            assertNull(SearchActivityUtils.getReferrer(intent));
        }
    }

    @Test
    public void getIntentSearchType_invalidPackageName() {
        var cases = List.of("a", "a.", ".a", "a&b", "a?b", "a+b", "a$b", "a_");

        for (var testCase : cases) {
            SearchActivityClientImpl.requestOmniboxForResult(mActivity, GOOD_URL, testCase);
            var intent = Shadows.shadowOf(mActivity).getNextStartedActivityForResult().intent;
            // Referrer will likely be stripped by the Client part...
            assertNull(IntentUtils.safeGetStringExtra(intent, SearchActivityExtras.EXTRA_REFERRER));

            // ... so let's simulate scenario where it's a custom origin:
            intent.putExtra(SearchActivityExtras.EXTRA_REFERRER, testCase);
            assertNull(SearchActivityUtils.getReferrer(intent));
        }
    }

    @Test
    public void resolveOmniboxRequestForResult_successfulResolutionForValidGURL() {
        // Simulate environment where we received an intent from self.
        var activity = Shadows.shadowOf(mActivity);
        activity.setCallingActivity(
                new ComponentName(ContextUtils.getApplicationContext(), TestActivity.class));

        var params = getLoadUrlParamsBuilder().build();
        SearchActivityUtils.resolveOmniboxRequestForResult(mActivity, params);

        var intent = Shadows.shadowOf(mActivity).getResultIntent();
        assertEquals("https://abc.xyz/", intent.getDataString());
        assertEquals(Activity.RESULT_OK, Shadows.shadowOf(mActivity).getResultCode());
    }

    @Test
    public void resolveOmniboxRequestForResult_noTrustedExtrasWithUnexpectedCallingPackage() {
        // An unlikely scenario where the caller somehow managed to pass a valid trusted token.
        var activity = Shadows.shadowOf(mActivity);
        activity.setCallingPackage("com.abc.xyz");

        var params = getLoadUrlParamsBuilder().build();
        SearchActivityUtils.resolveOmniboxRequestForResult(mActivity, params);

        var intent = Shadows.shadowOf(mActivity).getResultIntent();
        assertNull(intent);

        // Respectfully tell the caller we have nothing else to share.
        assertEquals(Activity.RESULT_CANCELED, Shadows.shadowOf(mActivity).getResultCode());
    }

    @Test
    public void resolveOmniboxRequestForResult_canceledResolutionForNullOrInvalidGURLs() {
        var activity = Shadows.shadowOf(mActivity);

        SearchActivityUtils.resolveOmniboxRequestForResult(mActivity, null);
        assertEquals(Activity.RESULT_CANCELED, activity.getResultCode());

        SearchActivityUtils.resolveOmniboxRequestForResult(mActivity, LOAD_URL_PARAMS_NULL_URL);
        assertEquals(Activity.RESULT_CANCELED, activity.getResultCode());

        SearchActivityUtils.resolveOmniboxRequestForResult(mActivity, LOAD_URL_PARAMS_INVALID_URL);
        assertEquals(Activity.RESULT_CANCELED, activity.getResultCode());
    }

    @Test
    public void getIntentQuery_noQuery() {
        var intent = new Intent();
        assertNull(SearchActivityUtils.getIntentQuery(intent));
    }

    @Test
    public void getIntentQuery_nullQuery() {
        var intent = new Intent();
        intent.putExtra(SearchManager.QUERY, (String) null);
        assertNull(SearchActivityUtils.getIntentQuery(intent));
    }

    @Test
    public void getIntentQuery_invalidQuery() {
        var intent = new Intent();
        intent.putExtra(SearchManager.QUERY, true);
        assertNull(SearchActivityUtils.getIntentQuery(intent));
    }

    @Test
    public void getIntentQuery_emptyQuery() {
        var intent = new Intent();
        intent.putExtra(SearchManager.QUERY, "");
        assertEquals("", SearchActivityUtils.getIntentQuery(intent));
    }

    @Test
    public void getIntentQuery_withQuery() {
        var intent = new Intent();
        intent.putExtra(SearchManager.QUERY, "query");
        assertEquals("query", SearchActivityUtils.getIntentQuery(intent));
    }

    @Test
    public void createLoadUrlIntent_nullParams() {
        assertNull(SearchActivityUtils.createLoadUrlIntent(mActivity, COMPONENT_TRUSTED, null));
    }

    @Test
    public void createLoadUrlIntent_nullUrl() {
        assertNull(
                SearchActivityUtils.createLoadUrlIntent(
                        mActivity, COMPONENT_TRUSTED, LOAD_URL_PARAMS_NULL_URL));
    }

    @Test
    public void createLoadUrlIntent_invalidUrl() {
        assertNull(
                SearchActivityUtils.createLoadUrlIntent(
                        mActivity, COMPONENT_TRUSTED, LOAD_URL_PARAMS_INVALID_URL));
    }

    @Test
    public void createLoadUrlIntent_invalidFixedUpUrl() {
        doReturn(null).when(mFormatter).fixupUrl(any());
        assertNull(
                SearchActivityUtils.createLoadUrlIntent(
                        mActivity, COMPONENT_TRUSTED, getLoadUrlParamsBuilder().build()));
    }

    @Test
    public void createLoadUrlIntent_untrustedRecipient() {
        Intent intent =
                SearchActivityUtils.createLoadUrlIntent(
                        mActivity, COMPONENT_UNTRUSTED, getLoadUrlParamsBuilder().build());
        assertNull(intent);
    }

    @Test
    public void createLoadUrlIntent_simpleParams() {
        Intent intent =
                SearchActivityUtils.createLoadUrlIntent(
                        mActivity, COMPONENT_TRUSTED, getLoadUrlParamsBuilder().build());
        assertNotNull(intent);

        assertEquals(Uri.parse("https://abc.xyz/"), intent.getData());
        assertTrue(intent.getBooleanExtra(SearchActivity.EXTRA_FROM_SEARCH_ACTIVITY, false));
        assertEquals(COMPONENT_TRUSTED.getClassName(), intent.getComponent().getClassName());
        assertNull(intent.getStringExtra(IntentHandler.EXTRA_POST_DATA_TYPE));
        assertNull(intent.getByteArrayExtra(IntentHandler.EXTRA_POST_DATA));
        assertTrue(intent.hasExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA));
    }

    @Test
    public void createLoadUrlIntent_paramsWithNullPostData() {
        var params = getLoadUrlParamsBuilder().setpostDataAndType(null, "abc").build();
        Intent intent =
                SearchActivityUtils.createLoadUrlIntent(mActivity, COMPONENT_TRUSTED, params);
        assertNotNull(intent);

        assertEquals(Uri.parse("https://abc.xyz/"), intent.getData());
        assertTrue(intent.getBooleanExtra(SearchActivity.EXTRA_FROM_SEARCH_ACTIVITY, false));
        assertEquals(COMPONENT_TRUSTED.getClassName(), intent.getComponent().getClassName());
        assertNull(intent.getStringExtra(IntentHandler.EXTRA_POST_DATA_TYPE));
        assertNull(intent.getByteArrayExtra(IntentHandler.EXTRA_POST_DATA));
        assertTrue(intent.hasExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA));
    }

    @Test
    public void createLoadUrlIntent_paramsWithEmptyPostData() {
        var params = getLoadUrlParamsBuilder().setpostDataAndType(new byte[] {}, "abc").build();
        Intent intent =
                SearchActivityUtils.createLoadUrlIntent(mActivity, COMPONENT_TRUSTED, params);
        assertNotNull(intent);

        assertEquals(Uri.parse("https://abc.xyz/"), intent.getData());
        assertTrue(intent.getBooleanExtra(SearchActivity.EXTRA_FROM_SEARCH_ACTIVITY, false));
        assertEquals(COMPONENT_TRUSTED.getClassName(), intent.getComponent().getClassName());
        assertNull(intent.getStringExtra(IntentHandler.EXTRA_POST_DATA_TYPE));
        assertNull(intent.getByteArrayExtra(IntentHandler.EXTRA_POST_DATA));
        assertTrue(intent.hasExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA));
    }

    @Test
    public void createLoadUrlIntent_paramsWithNullPostDataType() {
        var params =
                getLoadUrlParamsBuilder().setpostDataAndType(new byte[] {1, 2, 3}, null).build();
        Intent intent =
                SearchActivityUtils.createLoadUrlIntent(mActivity, COMPONENT_TRUSTED, params);
        assertNotNull(intent);

        assertEquals(Uri.parse("https://abc.xyz/"), intent.getData());
        assertTrue(intent.getBooleanExtra(SearchActivity.EXTRA_FROM_SEARCH_ACTIVITY, false));
        assertEquals(COMPONENT_TRUSTED.getClassName(), intent.getComponent().getClassName());
        assertNull(intent.getStringExtra(IntentHandler.EXTRA_POST_DATA_TYPE));
        assertNull(intent.getByteArrayExtra(IntentHandler.EXTRA_POST_DATA));
        assertTrue(intent.hasExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA));
    }

    @Test
    public void createLoadUrlIntent_paramsWithEmptyPostDataType() {
        var params = getLoadUrlParamsBuilder().setpostDataAndType(new byte[] {1, 2, 3}, "").build();
        Intent intent =
                SearchActivityUtils.createLoadUrlIntent(mActivity, COMPONENT_TRUSTED, params);
        assertNotNull(intent);

        assertEquals(Uri.parse("https://abc.xyz/"), intent.getData());
        assertTrue(intent.getBooleanExtra(SearchActivity.EXTRA_FROM_SEARCH_ACTIVITY, false));
        assertEquals(COMPONENT_TRUSTED.getClassName(), intent.getComponent().getClassName());
        assertNull(intent.getStringExtra(IntentHandler.EXTRA_POST_DATA_TYPE));
        assertNull(intent.getByteArrayExtra(IntentHandler.EXTRA_POST_DATA));
        assertTrue(intent.hasExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA));
    }

    @Test
    public void createLoadUrlIntent_paramsWithValidPostDataType() {
        var params =
                getLoadUrlParamsBuilder().setpostDataAndType(new byte[] {1, 2, 3}, "test").build();
        Intent intent =
                SearchActivityUtils.createLoadUrlIntent(mActivity, COMPONENT_TRUSTED, params);
        assertNotNull(intent);

        assertEquals(Uri.parse("https://abc.xyz/"), intent.getData());
        assertTrue(intent.getBooleanExtra(SearchActivity.EXTRA_FROM_SEARCH_ACTIVITY, false));
        assertEquals(COMPONENT_TRUSTED.getClassName(), intent.getComponent().getClassName());
        assertEquals("test", intent.getStringExtra(IntentHandler.EXTRA_POST_DATA_TYPE));
        assertArrayEquals(
                new byte[] {1, 2, 3}, intent.getByteArrayExtra(IntentHandler.EXTRA_POST_DATA));
        assertTrue(intent.hasExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA));
    }

    @Test
    public void createIntentForStartActivity_fromUntrustedSource() {
        Activity untrustedActivity = spy(Robolectric.buildActivity(Activity.class).setup().get());
        doReturn("com.some.app").when(untrustedActivity).getPackageName();
        var intent =
                SearchActivityUtils.createIntentForStartActivity(
                        untrustedActivity, getLoadUrlParamsBuilder().build());
        assertNull(intent);
    }

    @Test
    public void createIntentForStartActivity_fromSelf() {
        var intent =
                SearchActivityUtils.createIntentForStartActivity(
                        mActivity, getLoadUrlParamsBuilder().build());

        assertEquals(Intent.ACTION_VIEW, intent.getAction());
        assertEquals(
                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT,
                intent.getFlags());
        assertEquals(Uri.parse("https://abc.xyz/"), intent.getData());
        assertTrue(intent.getBooleanExtra(SearchActivity.EXTRA_FROM_SEARCH_ACTIVITY, false));
        assertEquals(ChromeLauncherActivity.class.getName(), intent.getComponent().getClassName());
    }
}