blob: a5923aef8d6af31da5bd8674e4556cea8007b7b2 [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/contextual_search/contextual_search_service.h"
#include "base/test/task_environment.h"
#include "components/contextual_search/contextual_search_types.h"
#include "components/contextual_search/fake_variations_client.h"
#include "components/contextual_search/mock_contextual_search_context_controller.h"
#include "components/contextual_search/pref_names.h"
#include "components/prefs/testing_pref_service.h"
#include "components/search_engines/search_engines_test_environment.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "components/variations/variations_client.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace contextual_search {
namespace {
using testing::_;
using testing::IsNull;
using testing::NotNull;
} // namespace
class ContextualSearchServiceTest : public testing::Test {
public:
ContextualSearchServiceTest()
: identity_test_env_(std::make_unique<signin::IdentityTestEnvironment>()),
test_shared_loader_factory_(
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory_)) {}
void SetUp() override {
service_ = std::make_unique<ContextualSearchService>(
identity_test_env_->identity_manager(), test_shared_loader_factory_,
search_engines_test_environment_.template_url_service(),
&fake_variations_client_, version_info::Channel::UNKNOWN, "en-US");
ContextualSearchService::RegisterProfilePrefs(pref_service_.registry());
}
lens::ClientToAimMessage CaptureClientToAimRequest(
std::unique_ptr<
ContextualSearchContextController::CreateClientToAimRequestInfo>
create_client_to_aim_request_info) {
captured_client_to_aim_message_ =
std::move(create_client_to_aim_request_info);
return lens::ClientToAimMessage();
}
ContextualSearchContextController::CreateClientToAimRequestInfo*
GetCapturedClientToAimRequest() {
return captured_client_to_aim_message_.get();
}
protected:
base::test::TaskEnvironment task_environment_;
std::unique_ptr<signin::IdentityTestEnvironment> identity_test_env_;
network::TestURLLoaderFactory test_url_loader_factory_;
scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;
search_engines::SearchEnginesTestEnvironment search_engines_test_environment_;
FakeVariationsClient fake_variations_client_;
std::unique_ptr<ContextualSearchService> service_;
std::unique_ptr<
ContextualSearchContextController::CreateClientToAimRequestInfo>
captured_client_to_aim_message_;
TestingPrefServiceSimple pref_service_;
};
TEST_F(ContextualSearchServiceTest, Session) {
// Try to get a session that does not exist.
auto bad_handle = service_->GetSession(base::UnguessableToken::Create(),
/*invocation_source=*/std::nullopt);
ASSERT_THAT(bad_handle, IsNull());
// Create a new session.
auto config_params1 =
std::make_unique<ContextualSearchContextController::ConfigParams>();
config_params1->send_lns_surface = false;
config_params1->suppress_lns_surface_param_if_no_image = true;
config_params1->enable_viewport_images = false;
auto session1_handle1 = service_->CreateSession(
std::move(config_params1), ContextualSearchSource::kUnknown,
/*invocation_source=*/std::nullopt);
// Check the search content sharing settings to notify the session handle
// that the client is properly checking the pref value.
session1_handle1->CheckSearchContentSharingSettings(&pref_service_);
ASSERT_THAT(session1_handle1, NotNull());
ASSERT_THAT(session1_handle1->GetController(), NotNull());
ASSERT_THAT(session1_handle1->GetMetricsRecorder(), NotNull());
// Create another new session.
auto config_params2 =
std::make_unique<ContextualSearchContextController::ConfigParams>();
config_params2->send_lns_surface = false;
config_params2->suppress_lns_surface_param_if_no_image = true;
config_params2->enable_viewport_images = false;
auto session2_handle1 = service_->CreateSession(
std::move(config_params2), ContextualSearchSource::kUnknown,
/*invocation_source=*/std::nullopt);
session2_handle1->CheckSearchContentSharingSettings(&pref_service_);
ASSERT_THAT(session2_handle1, NotNull());
ASSERT_THAT(session2_handle1->GetController(), NotNull());
ASSERT_THAT(session2_handle1->GetMetricsRecorder(), NotNull());
// Get a new handle to session two.
auto session2_handle2 = service_->GetSession(
session2_handle1->session_id(), /*invocation_source=*/std::nullopt);
session2_handle2->CheckSearchContentSharingSettings(&pref_service_);
ASSERT_THAT(session2_handle2, NotNull());
EXPECT_EQ(session2_handle2->GetController(),
session2_handle1->GetController());
EXPECT_EQ(session2_handle2->GetMetricsRecorder(),
session2_handle1->GetMetricsRecorder());
EXPECT_NE(session1_handle1->GetMetricsRecorder(),
session2_handle1->GetMetricsRecorder());
EXPECT_NE(session1_handle1->GetController(),
session2_handle1->GetController());
// Release the first handle to session two. The session should still be alive.
session2_handle1.reset();
auto session2_handle3 = service_->GetSession(
session2_handle2->session_id(), /*invocation_source=*/std::nullopt);
session2_handle3->CheckSearchContentSharingSettings(&pref_service_);
ASSERT_THAT(session2_handle3, NotNull());
EXPECT_EQ(session2_handle3->GetController(),
session2_handle2->GetController());
EXPECT_EQ(session2_handle3->GetMetricsRecorder(),
session2_handle2->GetMetricsRecorder());
// Release the remaining handles to session two. The session should be
// released.
auto session_id = session2_handle2->session_id();
session2_handle2.reset();
session2_handle3.reset();
auto session2_handle4 =
service_->GetSession(session_id, /*invocation_source=*/std::nullopt);
ASSERT_THAT(session2_handle4, IsNull());
// Get a new handle to session one.
auto session1_handle2 = service_->GetSession(
session1_handle1->session_id(), /*invocation_source=*/std::nullopt);
session1_handle2->CheckSearchContentSharingSettings(&pref_service_);
ASSERT_THAT(session1_handle2, NotNull());
EXPECT_EQ(session1_handle2->GetController(),
session1_handle1->GetController());
EXPECT_EQ(session1_handle2->GetMetricsRecorder(),
session1_handle1->GetMetricsRecorder());
}
TEST_F(ContextualSearchServiceTest, PendingContextTokens) {
auto mock_controller =
std::make_unique<MockContextualSearchContextController>();
auto metrics_recorder = std::make_unique<ContextualSearchMetricsRecorder>(
ContextualSearchSource::kUnknown);
MockContextualSearchContextController* mock_controller_ptr =
mock_controller.get();
auto session_handle = service_->CreateSessionForTesting(
std::move(mock_controller), std::move(metrics_recorder));
session_handle->CheckSearchContentSharingSettings(&pref_service_);
// Add some dummy tokens.
base::UnguessableToken token1 = base::UnguessableToken::Create();
base::UnguessableToken token2 = base::UnguessableToken::Create();
session_handle->GetUploadedContextTokensForTesting().push_back(token1);
session_handle->GetUploadedContextTokensForTesting().push_back(token2);
// Capture the tokens before they are moved.
std::vector<base::UnguessableToken> expected_request_tokens1;
expected_request_tokens1.push_back(token1);
expected_request_tokens1.push_back(token2);
// Expect CreateClientToAimRequest to be called and capture the message.
EXPECT_CALL(*mock_controller_ptr, CreateClientToAimRequest(_))
.WillOnce(testing::Invoke(
this, &ContextualSearchServiceTest::CaptureClientToAimRequest));
// Call CreateClientToAimRequest to move tokens to pending.
auto create_client_to_aim_request_info = std::make_unique<
ContextualSearchContextController::CreateClientToAimRequestInfo>();
session_handle->CreateClientToAimRequest(
std::move(create_client_to_aim_request_info));
// Verify that the request contains the correct unguessable tokens.
EXPECT_THAT(GetCapturedClientToAimRequest()->file_tokens,
testing::UnorderedElementsAreArray(expected_request_tokens1));
// Verify that submitted context tokens contains the moved tokens.
std::vector<base::UnguessableToken> expected_submitted_tokens;
expected_submitted_tokens.push_back(token1);
expected_submitted_tokens.push_back(token2);
EXPECT_THAT(session_handle->GetSubmittedContextTokens(),
testing::UnorderedElementsAreArray(expected_submitted_tokens));
EXPECT_TRUE(session_handle->GetUploadedContextTokens().empty());
// Add more tokens and call CreateClientToAimRequest again.
base::UnguessableToken token3 = base::UnguessableToken::Create();
session_handle->GetUploadedContextTokensForTesting().push_back(token3);
// Capture the tokens for the second request.
std::vector<base::UnguessableToken> expected_request_tokens2;
expected_request_tokens2.push_back(token3);
// Expect CreateClientToAimRequest to be called again and capture the message.
EXPECT_CALL(*mock_controller_ptr, CreateClientToAimRequest(_))
.WillOnce(testing::Invoke(
this, &ContextualSearchServiceTest::CaptureClientToAimRequest));
auto create_client_to_aim_request_info2 = std::make_unique<
ContextualSearchContextController::CreateClientToAimRequestInfo>();
session_handle->CreateClientToAimRequest(
std::move(create_client_to_aim_request_info2));
// Verify that the second request contains only the newly added token.
EXPECT_THAT(GetCapturedClientToAimRequest()->file_tokens,
testing::UnorderedElementsAreArray(expected_request_tokens2));
// Verify that the submitted context tokens has both the previous and the new
// tokens.
expected_submitted_tokens.push_back(token3);
EXPECT_THAT(session_handle->GetSubmittedContextTokens(),
testing::UnorderedElementsAreArray(expected_submitted_tokens));
EXPECT_TRUE(session_handle->GetUploadedContextTokens().empty());
// Clear the pending tokens.
session_handle->ClearSubmittedContextTokens();
// Verify that the submitted context tokens are empty.
EXPECT_TRUE(session_handle->GetSubmittedContextTokens().empty());
}
TEST_F(ContextualSearchServiceTest, FileInfoTest) {
auto mock_controller =
std::make_unique<MockContextualSearchContextController>();
auto metrics_recorder = std::make_unique<ContextualSearchMetricsRecorder>(
ContextualSearchSource::kUnknown);
MockContextualSearchContextController* mock_controller_ptr =
mock_controller.get();
auto session_handle = service_->CreateSessionForTesting(
std::move(mock_controller), std::move(metrics_recorder));
session_handle->CheckSearchContentSharingSettings(&pref_service_);
// Create tokens and FileInfo objects.
base::UnguessableToken token1 = base::UnguessableToken::Create();
FileInfo file_info1;
file_info1.file_token = token1;
file_info1.file_name = "file1.pdf";
base::UnguessableToken token2 = base::UnguessableToken::Create();
FileInfo file_info2;
file_info2.file_token = token2;
file_info2.file_name = "file2.jpg";
base::UnguessableToken tab_token1 = base::UnguessableToken::Create();
FileInfo tab_info1;
tab_info1.file_token = tab_token1;
tab_info1.tab_url = GURL("http://example.com/tab1");
tab_info1.tab_title = "Tab 1 Title";
tab_info1.tab_session_id = SessionID::FromSerializedValue(123);
base::UnguessableToken tab_token2 = base::UnguessableToken::Create();
FileInfo tab_info2;
tab_info2.file_token = tab_token2;
tab_info2.tab_url = GURL("http://example.com/tab2");
tab_info2.tab_title = "Tab 2 Title";
tab_info2.tab_session_id = SessionID::FromSerializedValue(456);
// Mock GetFileInfo.
EXPECT_CALL(*mock_controller_ptr, GetFileInfo(token1))
.WillRepeatedly(testing::Return(&file_info1));
EXPECT_CALL(*mock_controller_ptr, GetFileInfo(token2))
.WillRepeatedly(testing::Return(&file_info2));
EXPECT_CALL(*mock_controller_ptr, GetFileInfo(tab_token1))
.WillRepeatedly(testing::Return(&tab_info1));
EXPECT_CALL(*mock_controller_ptr, GetFileInfo(tab_token2))
.WillRepeatedly(testing::Return(&tab_info2));
// Test GetUploadedContextFileInfos.
session_handle->GetUploadedContextTokensForTesting().push_back(token1);
session_handle->GetUploadedContextTokensForTesting().push_back(tab_token1);
std::vector<FileInfo> uploaded_infos =
session_handle->GetUploadedContextFileInfos();
ASSERT_EQ(uploaded_infos.size(), 2u);
EXPECT_THAT(uploaded_infos,
testing::UnorderedElementsAre(
testing::Field(&FileInfo::file_token, token1),
testing::Field(&FileInfo::file_token, tab_token1)));
session_handle->GetUploadedContextTokensForTesting().push_back(token2);
session_handle->GetUploadedContextTokensForTesting().push_back(tab_token2);
uploaded_infos = session_handle->GetUploadedContextFileInfos();
EXPECT_THAT(uploaded_infos,
testing::UnorderedElementsAre(
testing::Field(&FileInfo::file_token, token1),
testing::Field(&FileInfo::file_token, token2),
testing::Field(&FileInfo::file_token, tab_token1),
testing::Field(&FileInfo::file_token, tab_token2)));
// Test GetSubmittedContextFileInfos (after moving tokens).
EXPECT_CALL(*mock_controller_ptr, CreateClientToAimRequest(_))
.WillOnce(testing::Invoke(
this, &ContextualSearchServiceTest::CaptureClientToAimRequest));
auto create_client_to_aim_request_info = std::make_unique<
ContextualSearchContextController::CreateClientToAimRequestInfo>();
session_handle->CreateClientToAimRequest(
std::move(create_client_to_aim_request_info));
std::vector<FileInfo> submitted_infos =
session_handle->GetSubmittedContextFileInfos();
EXPECT_THAT(submitted_infos,
testing::UnorderedElementsAre(
testing::Field(&FileInfo::file_token, token1),
testing::Field(&FileInfo::file_token, token2),
testing::Field(&FileInfo::file_token, tab_token1),
testing::Field(&FileInfo::file_token, tab_token2)));
// Also check that uploaded files is now empty.
uploaded_infos = session_handle->GetUploadedContextFileInfos();
EXPECT_TRUE(uploaded_infos.empty());
// Add a new file to uploaded, and make sure submitted remains the same.
base::UnguessableToken token3 = base::UnguessableToken::Create();
FileInfo file_info3;
file_info3.file_token = token3;
file_info3.file_name = "file3.png";
EXPECT_CALL(*mock_controller_ptr, GetFileInfo(token3))
.WillRepeatedly(testing::Return(&file_info3));
session_handle->GetUploadedContextTokensForTesting().push_back(token3);
uploaded_infos = session_handle->GetUploadedContextFileInfos();
ASSERT_EQ(uploaded_infos.size(), 1u);
EXPECT_EQ(uploaded_infos[0].file_token, token3);
submitted_infos = session_handle->GetSubmittedContextFileInfos();
EXPECT_THAT(submitted_infos,
testing::UnorderedElementsAre(
testing::Field(&FileInfo::file_token, token1),
testing::Field(&FileInfo::file_token, token2),
testing::Field(&FileInfo::file_token, tab_token1),
testing::Field(&FileInfo::file_token, tab_token2)));
// Test that a token with no FileInfo is ignored.
base::UnguessableToken token4 = base::UnguessableToken::Create();
EXPECT_CALL(*mock_controller_ptr, GetFileInfo(token4))
.WillRepeatedly(testing::Return(nullptr));
session_handle->GetUploadedContextTokensForTesting().push_back(token4);
uploaded_infos = session_handle->GetUploadedContextFileInfos();
ASSERT_EQ(uploaded_infos.size(), 1u);
EXPECT_EQ(uploaded_infos[0].file_token, token3);
}
TEST_F(ContextualSearchServiceTest, NullController) {
// Create a session.
auto config_params =
std::make_unique<ContextualSearchContextController::ConfigParams>();
auto session_handle = service_->CreateSession(
std::move(config_params), ContextualSearchSource::kUnknown,
/*invocation_source=*/std::nullopt);
ASSERT_THAT(session_handle, NotNull());
session_handle->CheckSearchContentSharingSettings(&pref_service_);
// Add some dummy tokens.
session_handle->GetUploadedContextTokensForTesting().push_back(
base::UnguessableToken::Create());
auto create_client_to_aim_request_info = std::make_unique<
ContextualSearchContextController::CreateClientToAimRequestInfo>();
session_handle->CreateClientToAimRequest(
std::move(create_client_to_aim_request_info));
// Destroy the service. This will cause GetController() to return nullptr.
service_.reset();
// These calls should not crash.
EXPECT_TRUE(session_handle->GetUploadedContextFileInfos().empty());
EXPECT_TRUE(session_handle->GetSubmittedContextFileInfos().empty());
}
// Regression test for crbug.com/466564968
TEST_F(ContextualSearchServiceTest, CreateSessionAfterShutdown) {
// Call Shutdown to clear the IdentityManager pointer.
service_->Shutdown();
// Destroy the IdentityManager to ensure the service is not holding a dangling
// pointer.
identity_test_env_.reset();
// Create a new session. This should not crash even if IdentityManager is gone
// (simulated by it being null after Shutdown).
auto config_params =
std::make_unique<ContextualSearchContextController::ConfigParams>();
auto session_handle = service_->CreateSession(
std::move(config_params), ContextualSearchSource::kUnknown,
/*invocation_source=*/std::nullopt);
ASSERT_THAT(session_handle, NotNull());
ASSERT_THAT(session_handle->GetController(), NotNull());
}
} // namespace contextual_search