// 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.assertFalse;
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 android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
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.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.omnibox.suggestions.OmniboxLoadUrlParams;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityClient;
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.content_public.browser.LoadUrlParams;
import org.chromium.content_public.common.ResourceRequestBodyJni;
import org.chromium.ui.base.PageTransition;
import org.chromium.url.GURL;
@RunWith(BaseRobolectricTestRunner.class)
public class SearchActivityClientImplUnitTest {
// 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 ComponentName COMPONENT_TRUSTED =
new ComponentName(ContextUtils.getApplicationContext(), SearchActivity.class);
private Activity mActivity = Robolectric.buildActivity(TestActivity.class).setup().get();
public @Rule MockitoRule mMockitoRule = MockitoJUnit.rule();
public @Rule JniMocker mJniMocker = new JniMocker();
private @Mock ResourceRequestBodyJni mResourceRequestBodyJni;
@Before
public void setUp() {
mJniMocker.mock(ResourceRequestBodyJni.TEST_HOOKS, mResourceRequestBodyJni);
doAnswer(i -> i.getArgument(0))
.when(mResourceRequestBodyJni)
.createResourceRequestBodyFromBytes(any());
}
private OmniboxLoadUrlParams.Builder getLoadUrlParamsBuilder() {
return new OmniboxLoadUrlParams.Builder("https://abc.xyz", PageTransition.TYPED);
}
@Test
public void createIntent_forTextSearch() {
@IntentOrigin
int[] origins =
new int[] {
IntentOrigin.SEARCH_WIDGET,
IntentOrigin.QUICK_ACTION_SEARCH_WIDGET,
IntentOrigin.CUSTOM_TAB,
};
SearchActivityClient client = new SearchActivityClientImpl();
for (int origin : origins) {
String action =
String.format(
SearchActivityClientImpl.ACTION_SEARCH_FORMAT, origin, SearchType.TEXT);
// null URL
var intent = client.createIntent(mActivity, origin, null, SearchType.TEXT);
assertEquals(action, intent.getAction());
assertNull(intent.getStringExtra(SearchActivityExtras.EXTRA_CURRENT_URL));
assertEquals(SearchType.TEXT, SearchActivityUtils.getIntentSearchType(intent));
assertEquals(origin, SearchActivityUtils.getIntentOrigin(intent));
// non-null URL
intent =
client.createIntent(
mActivity, origin, new GURL("http://abc.xyz"), SearchType.TEXT);
assertEquals(action, intent.getAction());
assertEquals(
"http://abc.xyz/",
intent.getStringExtra(SearchActivityExtras.EXTRA_CURRENT_URL));
assertEquals(SearchType.TEXT, SearchActivityUtils.getIntentSearchType(intent));
assertEquals(origin, SearchActivityUtils.getIntentOrigin(intent));
}
}
@Test
public void createIntent_forVoiceSearch() {
@IntentOrigin
int[] origins =
new int[] {
IntentOrigin.SEARCH_WIDGET,
IntentOrigin.QUICK_ACTION_SEARCH_WIDGET,
IntentOrigin.CUSTOM_TAB,
};
SearchActivityClient client = new SearchActivityClientImpl();
for (int origin : origins) {
String action =
String.format(
SearchActivityClientImpl.ACTION_SEARCH_FORMAT,
origin,
SearchType.VOICE);
// null URL
var intent = client.createIntent(mActivity, origin, null, SearchType.VOICE);
assertEquals(action, intent.getAction());
assertNull(intent.getStringExtra(SearchActivityExtras.EXTRA_CURRENT_URL));
assertEquals(SearchType.VOICE, SearchActivityUtils.getIntentSearchType(intent));
assertEquals(origin, SearchActivityUtils.getIntentOrigin(intent));
// non-null URL
intent =
client.createIntent(
mActivity, origin, new GURL("http://abc.xyz"), SearchType.VOICE);
assertEquals(action, intent.getAction());
assertEquals(
"http://abc.xyz/",
intent.getStringExtra(SearchActivityExtras.EXTRA_CURRENT_URL));
assertEquals(SearchType.VOICE, SearchActivityUtils.getIntentSearchType(intent));
assertEquals(origin, SearchActivityUtils.getIntentOrigin(intent));
}
}
@Test
public void createIntent_forLensSearch() {
@IntentOrigin
int[] origins =
new int[] {
IntentOrigin.SEARCH_WIDGET,
IntentOrigin.QUICK_ACTION_SEARCH_WIDGET,
IntentOrigin.CUSTOM_TAB,
};
SearchActivityClient client = new SearchActivityClientImpl();
for (int origin : origins) {
String action =
String.format(
SearchActivityClientImpl.ACTION_SEARCH_FORMAT, origin, SearchType.LENS);
// null URL
var intent = client.createIntent(mActivity, origin, null, SearchType.LENS);
assertEquals(action, intent.getAction());
assertNull(intent.getStringExtra(SearchActivityExtras.EXTRA_CURRENT_URL));
assertEquals(SearchType.LENS, SearchActivityUtils.getIntentSearchType(intent));
assertEquals(origin, SearchActivityUtils.getIntentOrigin(intent));
// non-null URL
intent =
client.createIntent(
mActivity, origin, new GURL("http://abc.xyz"), SearchType.LENS);
assertEquals(action, intent.getAction());
assertEquals(
"http://abc.xyz/",
intent.getStringExtra(SearchActivityExtras.EXTRA_CURRENT_URL));
assertEquals(SearchType.LENS, SearchActivityUtils.getIntentSearchType(intent));
assertEquals(origin, SearchActivityUtils.getIntentOrigin(intent));
}
}
@Test
public void buildTrustedIntent_appliesExpectedAction() {
var intent = SearchActivityClientImpl.buildTrustedIntent(mActivity, "abcd");
assertEquals("abcd", intent.getAction());
intent = SearchActivityClientImpl.buildTrustedIntent(mActivity, "1234");
assertEquals("1234", intent.getAction());
}
@Test
public void buildTrustedIntent_addressesSearchActivity() {
var intent = SearchActivityClientImpl.buildTrustedIntent(mActivity, "a");
assertEquals(
intent.getComponent().getClassName().toString(), SearchActivity.class.getName());
}
@Test
public void buildTrustedIntent_intentIsTrusted() {
var intent = SearchActivityClientImpl.buildTrustedIntent(mActivity, "a");
assertTrue(IntentUtils.isTrustedIntentFromSelf(intent));
}
@Test
public void requestOmniboxForResult_noActionWhenActivityIsNull() {
SearchActivityClientImpl.requestOmniboxForResult(null, EMPTY_URL, null);
}
@Test
public void requestOmniboxForResult_propagatesCurrentUrl() {
SearchActivityClientImpl.requestOmniboxForResult(mActivity, GOOD_URL, null);
var intentForResult = Shadows.shadowOf(mActivity).getNextStartedActivityForResult();
assertEquals(
IntentUtils.safeGetStringExtra(
intentForResult.intent, SearchActivityExtras.EXTRA_CURRENT_URL),
GOOD_URL.getSpec());
assertEquals(SearchActivityClientImpl.OMNIBOX_REQUEST_CODE, intentForResult.requestCode);
}
@Test
public void requestOmniboxForResult_acceptsEmptyUrl() {
// This is technically an invalid case. The test verifies we still do the right thing.
SearchActivityClientImpl.requestOmniboxForResult(mActivity, EMPTY_URL, null);
var intentForResult = Shadows.shadowOf(mActivity).getNextStartedActivityForResult();
assertTrue(
IntentUtils.safeHasExtra(
intentForResult.intent, SearchActivityExtras.EXTRA_CURRENT_URL));
assertTrue(
TextUtils.isEmpty(
IntentUtils.safeGetStringExtra(
intentForResult.intent, SearchActivityExtras.EXTRA_CURRENT_URL)));
assertEquals(SearchActivityClientImpl.OMNIBOX_REQUEST_CODE, intentForResult.requestCode);
}
@Test
public void isOmniboxResult_validResponse() {
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();
// Our own responses should always be valid.
assertTrue(
SearchActivityClientImpl.isOmniboxResult(
SearchActivityClientImpl.OMNIBOX_REQUEST_CODE, intent));
}
@Test
public void isOmniboxResult_invalidRequestCode() {
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();
assertFalse(
SearchActivityClientImpl.isOmniboxResult(
SearchActivityClientImpl.OMNIBOX_REQUEST_CODE - 1, intent));
assertFalse(SearchActivityClientImpl.isOmniboxResult(0, intent));
assertFalse(SearchActivityClientImpl.isOmniboxResult(~0, intent));
}
@Test
public void isOmniboxResult_untrustedReply() {
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();
intent.removeExtra(IntentUtils.TRUSTED_APPLICATION_CODE_EXTRA);
assertFalse(
SearchActivityClientImpl.isOmniboxResult(
SearchActivityClientImpl.OMNIBOX_REQUEST_CODE, intent));
}
@Test
public void isOmniboxResult_missingDestinationUrl() {
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();
intent.setData(null);
assertFalse(
SearchActivityClientImpl.isOmniboxResult(
SearchActivityClientImpl.OMNIBOX_REQUEST_CODE, intent));
}
@Test
public void getOmniboxResult_withInvalidUrl() {
var intent = new Intent();
intent.setComponent(COMPONENT_TRUSTED);
intent.setData(Uri.parse("a b"));
IntentUtils.addTrustedIntentExtras(intent);
assertNull(
SearchActivityClientImpl.getOmniboxResult(
SearchActivityClientImpl.OMNIBOX_REQUEST_CODE, Activity.RESULT_OK, intent));
}
@Test
public void getOmniboxResult_successfulResolution_simple() {
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();
LoadUrlParams result =
SearchActivityClientImpl.getOmniboxResult(
SearchActivityClientImpl.OMNIBOX_REQUEST_CODE, Activity.RESULT_OK, intent);
assertEquals("https://abc.xyz/", result.getUrl());
assertNull(result.getVerbatimHeaders());
assertNull(result.getPostData());
}
@Test
public void getOmniboxResult_successfulResolution_withPostDataOnly() {
var intent = new Intent();
intent.setComponent(COMPONENT_TRUSTED);
intent.setData(Uri.parse("https://abc.xyz"));
intent.putExtra(IntentHandler.EXTRA_POST_DATA, new byte[] {1, 2});
IntentUtils.addTrustedIntentExtras(intent);
LoadUrlParams result =
SearchActivityClientImpl.getOmniboxResult(
SearchActivityClientImpl.OMNIBOX_REQUEST_CODE, Activity.RESULT_OK, intent);
assertEquals("https://abc.xyz/", result.getUrl());
assertNull(result.getVerbatimHeaders());
assertNull(result.getPostData());
}
@Test
public void getOmniboxResult_successfulResolution_withPostDataTypeOnly() {
var intent = new Intent();
intent.setComponent(COMPONENT_TRUSTED);
intent.setData(Uri.parse("https://abc.xyz"));
intent.putExtra(IntentHandler.EXTRA_POST_DATA_TYPE, "data");
IntentUtils.addTrustedIntentExtras(intent);
LoadUrlParams result =
SearchActivityClientImpl.getOmniboxResult(
SearchActivityClientImpl.OMNIBOX_REQUEST_CODE, Activity.RESULT_OK, intent);
assertEquals("https://abc.xyz/", result.getUrl());
assertNull(result.getVerbatimHeaders());
assertNull(result.getPostData());
}
@Test
public void getOmniboxResult_successfulResolution_withEmptyPostData() {
var intent = new Intent();
intent.setComponent(COMPONENT_TRUSTED);
intent.setData(Uri.parse("https://abc.xyz"));
intent.putExtra(IntentHandler.EXTRA_POST_DATA_TYPE, "data");
intent.putExtra(IntentHandler.EXTRA_POST_DATA, new byte[] {});
IntentUtils.addTrustedIntentExtras(intent);
LoadUrlParams result =
SearchActivityClientImpl.getOmniboxResult(
SearchActivityClientImpl.OMNIBOX_REQUEST_CODE, Activity.RESULT_OK, intent);
assertEquals("https://abc.xyz/", result.getUrl());
assertNull(result.getVerbatimHeaders());
assertNull(result.getPostData());
}
@Test
public void getOmniboxResult_successfulResolution_withCompletePostData() {
var activity = Shadows.shadowOf(mActivity);
activity.setCallingActivity(
new ComponentName(ContextUtils.getApplicationContext(), TestActivity.class));
var params =
getLoadUrlParamsBuilder().setpostDataAndType(new byte[] {1, 2}, "data").build();
SearchActivityUtils.resolveOmniboxRequestForResult(mActivity, params);
// We should see the same URL on the receiving side.
var intent = Shadows.shadowOf(mActivity).getResultIntent();
LoadUrlParams result =
SearchActivityClientImpl.getOmniboxResult(
SearchActivityClientImpl.OMNIBOX_REQUEST_CODE, Activity.RESULT_OK, intent);
assertEquals("https://abc.xyz/", result.getUrl());
assertEquals("Content-Type: data", result.getVerbatimHeaders());
assertArrayEquals(new byte[] {1, 2}, result.getPostData().getEncodedNativeForm());
}
@Test
public void getOmniboxResult_returnsNullForNonOmniboxResult() {
// Resolve intent with GOOD_URL. Note, we don't want to get caught in early returns - make
// sure our intent is valid.
var activity = Shadows.shadowOf(mActivity);
activity.setCallingActivity(
new ComponentName(ContextUtils.getApplicationContext(), TestActivity.class));
var params = getLoadUrlParamsBuilder().build();
SearchActivityUtils.resolveOmniboxRequestForResult(mActivity, params);
// We should see no GURL object on the receiving side: this is not our intent.
var intent = Shadows.shadowOf(mActivity).getResultIntent();
assertNull(
SearchActivityClientImpl.getOmniboxResult(
/* requestCode= */ ~0, Activity.RESULT_OK, intent));
}
@Test
public void getOmniboxResult_returnsNullForCanceledNavigation() {
// Resolve intent with GOOD_URL. Note, we don't want to get caught in early returns - make
// sure our intent is valid.
var activity = Shadows.shadowOf(mActivity);
activity.setCallingActivity(
new ComponentName(ContextUtils.getApplicationContext(), TestActivity.class));
var params = getLoadUrlParamsBuilder().build();
SearchActivityUtils.resolveOmniboxRequestForResult(mActivity, params);
// We should see an empty GURL on the receiving side.
var intent = Shadows.shadowOf(mActivity).getResultIntent();
assertNull(
SearchActivityClientImpl.getOmniboxResult(
SearchActivityClientImpl.OMNIBOX_REQUEST_CODE,
Activity.RESULT_CANCELED,
intent));
}
}