blob: 09c62bbfe7aa317c5f57cfa5224d56b0dd378dc3 [file] [log] [blame]
// 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/omnibox/composebox/composebox_query_controller.h"
#include <memory>
#include <optional>
#include <string>
#include "base/base64url.h"
#include "base/test/bind.h"
#include "base/test/repeating_test_future.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "base/version_info/channel.h"
#include "components/omnibox/composebox/composebox_query.mojom.h"
#include "components/omnibox/composebox/test_composebox_query_controller.h"
#include "components/search_engines/search_engines_test_environment.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "components/signin/public/identity_manager/identity_test_utils.h"
#include "components/signin/public/identity_manager/primary_account_mutator.h"
#include "components/variations/variations_client.h"
#include "net/base/url_util.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.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"
#include "third_party/icu/source/common/unicode/locid.h"
#include "third_party/icu/source/common/unicode/unistr.h"
#include "third_party/icu/source/i18n/unicode/timezone.h"
#include "third_party/lens_server_proto/lens_overlay_platform.pb.h"
#include "third_party/lens_server_proto/lens_overlay_server.pb.h"
#include "third_party/lens_server_proto/lens_overlay_service_deps.pb.h"
#include "third_party/lens_server_proto/lens_overlay_surface.pb.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/image/image_skia_operations.h"
#if !BUILDFLAG(IS_IOS)
#include "ui/gfx/codec/jpeg_codec.h"
#endif // !BUILDFLAG(IS_IOS)
constexpr char kQuerySubmissionTimeQueryParameter[] = "qsubts";
constexpr char kClientUploadDurationQueryParameter[] = "cud";
constexpr char kSessionIdQueryParameterKey[] = "gsessionid";
constexpr char kVariationsHeaderKey[] = "X-Client-Data";
constexpr char kTestUser[] = "test_user@gmail.com";
constexpr char kTestSearchSessionId[] = "test_search_session_id";
constexpr char kTestServerSessionId[] = "test_server_session_id";
constexpr char kLocale[] = "en-US";
constexpr char kRegion[] = "US";
constexpr char kTimeZone[] = "America/Los_Angeles";
constexpr char kRequestIdParameterKey[] = "vsrid";
constexpr char kVisualInputTypeParameterKey[] = "vit";
constexpr char kLnsSurfaceParameterKey[] = "lns_surface";
constexpr char kTestCellAddress[] = "test_cell_address";
constexpr char kTestServerAddress[] = "test_server_address";
base::Time kTestQueryStartTime =
base::Time::FromMillisecondsSinceUnixEpoch(1000);
using FileUploadStatusTuple = std::tuple<base::UnguessableToken,
lens::MimeType,
FileUploadStatus,
std::optional<FileUploadErrorType>>;
class ComposeboxQueryControllerTest
: public testing::Test,
public ComposeboxQueryController::FileUploadStatusObserver {
public:
ComposeboxQueryControllerTest() = default;
~ComposeboxQueryControllerTest() override = default;
void CreateController(bool send_lns_surface) {
controller_ = std::make_unique<TestComposeboxQueryController>(
identity_manager(), shared_url_loader_factory_,
version_info::Channel::UNKNOWN, kLocale, template_url_service(),
fake_variations_client_.get(), send_lns_surface);
controller_->AddObserver(this);
lens::LensOverlayServerClusterInfoResponse cluster_info_response;
cluster_info_response.set_search_session_id(kTestSearchSessionId);
cluster_info_response.set_server_session_id(kTestServerSessionId);
cluster_info_response.mutable_routing_info()->set_cell_address(
kTestCellAddress);
cluster_info_response.mutable_routing_info()->set_server_address(
kTestServerAddress);
controller_->set_fake_cluster_info_response(cluster_info_response);
controller().set_on_query_controller_state_changed_callback(
base::BindRepeating(
&ComposeboxQueryControllerTest::OnQueryControllerStateChanged,
base::Unretained(this)));
}
void SetUp() override {
shared_url_loader_factory_ =
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_factory_);
in_process_data_decoder_ =
std::make_unique<data_decoder::test::InProcessDataDecoder>();
icu::TimeZone::adoptDefault(
icu::TimeZone::createTimeZone(icu::UnicodeString(kTimeZone)));
UErrorCode error_code = U_ZERO_ERROR;
icu::Locale::setDefault(icu::Locale(kLocale), error_code);
ASSERT_TRUE(U_SUCCESS(error_code));
fake_variations_client_ = std::make_unique<FakeVariationsClient>();
CreateController(/*send_lns_surface=*/false);
}
void TearDown() override {
controller_->RemoveObserver(this);
while (!controller_state_future_.IsEmpty()) {
controller_state_future_.Take();
}
while (!file_upload_status_future_.IsEmpty()) {
file_upload_status_future_.Take();
}
}
void WaitForClusterInfo(QueryControllerState expected_state =
QueryControllerState::kClusterInfoReceived) {
EXPECT_EQ(QueryControllerState::kAwaitingClusterInfoResponse,
controller_state_future_.Take());
EXPECT_EQ(QueryControllerState::kAwaitingClusterInfoResponse,
controller().query_controller_state());
EXPECT_EQ(expected_state, controller_state_future_.Take());
EXPECT_EQ(expected_state, controller().query_controller_state());
EXPECT_EQ(controller().num_cluster_info_fetch_requests_sent(), 1);
// The cluster info request should have the cors variations header.
EXPECT_THAT(controller().last_sent_cors_exempt_headers(),
testing::Contains(kVariationsHeaderKey));
}
void StartPdfFileUploadFlow(const base::UnguessableToken& file_token,
scoped_refptr<base::RefCountedBytes> file_data) {
std::unique_ptr<ComposeboxQueryController::FileInfo> file_info =
std::make_unique<ComposeboxQueryController::FileInfo>();
file_info->file_token_ = file_token;
file_info->mime_type_ = lens::MimeType::kPdf;
controller().StartFileUploadFlow(std::move(file_info), std::move(file_data),
/*image_options=*/std::nullopt);
}
void StartImageFileUploadFlow(const base::UnguessableToken& file_token,
scoped_refptr<base::RefCountedBytes> file_data,
std::optional<composebox::ImageEncodingOptions>
image_options = std::nullopt) {
std::unique_ptr<ComposeboxQueryController::FileInfo> file_info =
std::make_unique<ComposeboxQueryController::FileInfo>();
file_info->file_token_ = file_token;
file_info->mime_type_ = lens::MimeType::kImage;
controller().StartFileUploadFlow(std::move(file_info), std::move(file_data),
image_options);
}
void WaitForFileUpload(
const base::UnguessableToken& file_token,
lens::MimeType mime_type,
FileUploadStatus expected_status = FileUploadStatus::kUploadSuccessful,
std::optional<FileUploadErrorType> expected_error_type = std::nullopt) {
FileUploadStatusTuple processing_file_upload_status =
file_upload_status_future_.Take();
EXPECT_EQ(file_token, std::get<0>(processing_file_upload_status));
EXPECT_EQ(mime_type, std::get<1>(processing_file_upload_status));
EXPECT_EQ(FileUploadStatus::kProcessing,
std::get<2>(processing_file_upload_status));
EXPECT_EQ(std::nullopt, std::get<3>(processing_file_upload_status));
if (expected_status != FileUploadStatus::kValidationFailed) {
// For client-side validation failures, the state will never change to
// kUploadStarted.
FileUploadStatusTuple upload_started_file_upload_status =
file_upload_status_future_.Take();
EXPECT_EQ(file_token, std::get<0>(upload_started_file_upload_status));
EXPECT_EQ(mime_type, std::get<1>(upload_started_file_upload_status));
EXPECT_EQ(FileUploadStatus::kUploadStarted,
std::get<2>(upload_started_file_upload_status));
EXPECT_EQ(std::nullopt, std::get<3>(upload_started_file_upload_status));
}
FileUploadStatusTuple final_file_upload_status =
file_upload_status_future_.Take();
EXPECT_EQ(file_token, std::get<0>(final_file_upload_status));
EXPECT_EQ(mime_type, std::get<1>(final_file_upload_status));
EXPECT_EQ(expected_status, std::get<2>(final_file_upload_status));
EXPECT_EQ(expected_error_type, std::get<3>(final_file_upload_status));
if (expected_status == FileUploadStatus::kValidationFailed) {
// For client-side validation failures, the file upload request will not
// be sent.
EXPECT_EQ(controller().num_file_upload_requests_sent(), 0);
} else {
EXPECT_EQ(controller().num_file_upload_requests_sent(), 1);
EXPECT_THAT(GetGsessionIdFromUrl(controller().last_sent_fetch_url()),
testing::Optional(std::string(kTestServerSessionId)));
// The file upload request should have the cors variations header.
EXPECT_THAT(controller().last_sent_cors_exempt_headers(),
testing::Contains(kVariationsHeaderKey));
}
}
TestComposeboxQueryController& controller() { return *controller_; }
void OnQueryControllerStateChanged(QueryControllerState new_state) {
controller_state_future_.AddValue(new_state);
}
// ComposeboxQueryController::FileUploadStatusObserver:
void OnFileUploadStatusChanged(
const base::UnguessableToken& file_token,
lens::MimeType mime_type,
FileUploadStatus file_upload_status,
const std::optional<FileUploadErrorType>& error_type) override {
file_upload_status_future_.AddValue(file_token, mime_type,
file_upload_status, error_type);
}
#if !BUILDFLAG(IS_IOS)
std::vector<uint8_t> CreateJPGBytes(int width, int height) {
SkBitmap bitmap;
bitmap.allocN32Pixels(width, height);
bitmap.eraseColor(SK_ColorRED); // Fill with a solid color
auto image_bytes = gfx::JPEGCodec::Encode(bitmap, 100);
return image_bytes.value();
}
#endif // !BUILDFLAG(IS_IOS)
lens::LensOverlayRequestId DecodeRequestIdFromVsrid(std::string vsrid_param) {
std::string serialized_proto;
EXPECT_TRUE(base::Base64UrlDecode(
vsrid_param, base::Base64UrlDecodePolicy::DISALLOW_PADDING,
&serialized_proto));
lens::LensOverlayRequestId proto;
EXPECT_TRUE(proto.ParseFromString(serialized_proto));
return proto;
}
lens::LensOverlayRequestId GetRequestIdFromUrl(std::string url_string) {
GURL url = GURL(url_string);
std::string vsrid_param;
EXPECT_TRUE(
net::GetValueForKeyInQuery(url, kRequestIdParameterKey, &vsrid_param));
return DecodeRequestIdFromVsrid(vsrid_param);
}
protected:
signin::IdentityTestEnvironment* identity_test_env() {
return &identity_test_env_;
}
signin::IdentityManager* identity_manager() {
return identity_test_env_.identity_manager();
}
TemplateURLService* template_url_service() {
return search_engines_test_environment_.template_url_service();
}
// Returns an AccessTokenInfo with valid information that can be used for
// completing access token requests.
const signin::AccessTokenInfo& access_token_info() const {
return access_token_info_;
}
// Returns the gsessionid parameter from the given url.
std::optional<std::string> GetGsessionIdFromUrl(GURL url) {
std::string gsessionid_param;
bool has_gsessionid_param = net::GetValueForKeyInQuery(
url, kSessionIdQueryParameterKey, &gsessionid_param);
if (has_gsessionid_param) {
return gsessionid_param;
}
return std::nullopt;
}
// Returns the task environment.
base::test::TaskEnvironment& task_environment() { return task_environment_; }
base::test::RepeatingTestFuture<QueryControllerState>
controller_state_future_;
base::test::RepeatingTestFuture<base::UnguessableToken,
lens::MimeType,
FileUploadStatus,
std::optional<FileUploadErrorType>>
file_upload_status_future_;
private:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
search_engines::SearchEnginesTestEnvironment search_engines_test_environment_;
network::TestURLLoaderFactory test_factory_;
signin::IdentityTestEnvironment identity_test_env_;
scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory_;
std::unique_ptr<FakeVariationsClient> fake_variations_client_;
std::unique_ptr<TestComposeboxQueryController> controller_;
std::unique_ptr<data_decoder::test::InProcessDataDecoder>
in_process_data_decoder_;
signin::AccessTokenInfo access_token_info_{"access_token", base::Time::Max(),
"id_token"};
};
TEST_F(ComposeboxQueryControllerTest,
NotifySessionStartedIssuesClusterInfoRequest) {
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
}
TEST_F(ComposeboxQueryControllerTest,
NotifySessionStartedIssuesClusterInfoRequestWithOAuth) {
// Arrange: Make primary account available.
identity_test_env()->MakePrimaryAccountAvailable(
kTestUser, signin::ConsentLevel::kSignin);
// Act: Start the session.
controller().NotifySessionStarted();
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
access_token_info().token, access_token_info().expiration_time,
access_token_info().id_token);
// Assert: Validate cluster info request and state changes
WaitForClusterInfo();
}
TEST_F(ComposeboxQueryControllerTest,
NotifySessionStartedIssuesClusterInfoRequestFailure) {
// Arrange: Simulate an error in the cluster info request.
controller().set_next_cluster_info_request_should_return_error(true);
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo(
/*expected_state=*/QueryControllerState::kClusterInfoInvalid);
}
TEST_F(ComposeboxQueryControllerTest, NotifySessionAbandoned) {
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
StartPdfFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>());
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, lens::MimeType::kPdf);
// Check that file is in cache.
EXPECT_TRUE(controller().GetFileInfo(file_token));
// Act: End the session.
controller().NotifySessionAbandoned();
// Check that file is no longer in cache.
EXPECT_FALSE(controller().GetFileInfo(file_token));
EXPECT_EQ(QueryControllerState::kOff, controller().query_controller_state());
}
TEST_F(ComposeboxQueryControllerTest, UploadFileRequestFailure) {
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Arrange: Simulate a failure in the file upload request.
controller().set_next_file_upload_request_should_return_error(true);
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
StartPdfFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>());
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, lens::MimeType::kPdf,
/*expected_status=*/FileUploadStatus::kUploadFailed,
/*expected_error_type=*/FileUploadErrorType::kServerError);
}
#if !BUILDFLAG(IS_IOS)
TEST_F(ComposeboxQueryControllerTest, UploadImageFileRequestSuccess) {
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
std::vector<uint8_t> image_bytes = CreateJPGBytes(100, 100);
composebox::ImageEncodingOptions image_options{.max_size = 1000000,
.max_height = 1000,
.max_width = 1000,
.compression_quality = 30};
StartImageFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>(image_bytes),
image_options);
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, lens::MimeType::kImage);
// Validate the file upload request payload.
EXPECT_EQ(controller()
.last_sent_file_upload_request()
->objects_request()
.image_data()
.image_metadata()
.width(),
100);
EXPECT_EQ(controller()
.last_sent_file_upload_request()
->objects_request()
.image_data()
.image_metadata()
.height(),
100);
EXPECT_EQ(controller()
.last_sent_file_upload_request()
->client_logs()
.phase_latencies_metadata()
.phase_size(),
1);
EXPECT_EQ(controller()
.last_sent_file_upload_request()
->client_logs()
.phase_latencies_metadata()
.phase(0)
.image_encode_data()
.encoded_image_size_bytes(),
360);
EXPECT_EQ(controller()
.last_sent_file_upload_request()
->objects_request()
.request_context()
.request_id()
.media_type(),
lens::LensOverlayRequestId::MEDIA_TYPE_DEFAULT_IMAGE);
// Check that the vsrid matches that for an image upload.
EXPECT_EQ(controller()
.GetFileInfo(file_token)
->GetRequestIdForTesting()
->sequence_id(),
1);
EXPECT_EQ(controller()
.GetFileInfo(file_token)
->GetRequestIdForTesting()
->image_sequence_id(),
1);
EXPECT_EQ(controller()
.GetFileInfo(file_token)
->GetRequestIdForTesting()
->long_context_id(),
0);
// Check that the routing info is in the vsrid.
EXPECT_EQ(controller()
.GetFileInfo(file_token)
->GetRequestIdForTesting()
->routing_info()
.cell_address(),
kTestCellAddress);
EXPECT_EQ(controller()
.GetFileInfo(file_token)
->GetRequestIdForTesting()
->routing_info()
.server_address(),
kTestServerAddress);
}
TEST_F(ComposeboxQueryControllerTest, UploadEmptyImageFileRequestFailure) {
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
std::vector<uint8_t> image_bytes = std::vector<uint8_t>();
composebox::ImageEncodingOptions image_options{.max_size = 1000000,
.max_height = 1000,
.max_width = 1000,
.compression_quality = 30};
StartImageFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>(image_bytes),
image_options);
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, lens::MimeType::kImage,
FileUploadStatus::kValidationFailed,
FileUploadErrorType::kImageProcessingError);
}
#endif // !BUILDFLAG(IS_IOS)
TEST_F(ComposeboxQueryControllerTest, UploadPdfFileRequestSuccess) {
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
StartPdfFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>());
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, lens::MimeType::kPdf);
// Validate the file upload request payload.
EXPECT_EQ(controller()
.last_sent_file_upload_request()
->objects_request()
.payload()
.content()
.content_data(0)
.content_type(),
lens::ContentData::CONTENT_TYPE_PDF);
EXPECT_EQ(controller()
.last_sent_file_upload_request()
->objects_request()
.payload()
.content()
.content_data(0)
.data(),
"");
// Check that the vsrid matches that for a pdf upload.
EXPECT_EQ(controller()
.GetFileInfo(file_token)
->GetRequestIdForTesting()
->sequence_id(),
1);
EXPECT_EQ(controller()
.GetFileInfo(file_token)
->GetRequestIdForTesting()
->image_sequence_id(),
0);
EXPECT_EQ(controller()
.GetFileInfo(file_token)
->GetRequestIdForTesting()
->long_context_id(),
1);
EXPECT_EQ(controller()
.last_sent_file_upload_request()
->objects_request()
.request_context()
.request_id()
.sequence_id(),
1);
EXPECT_EQ(controller()
.last_sent_file_upload_request()
->objects_request()
.request_context()
.request_id()
.image_sequence_id(),
0);
EXPECT_EQ(controller()
.last_sent_file_upload_request()
->objects_request()
.request_context()
.request_id()
.long_context_id(),
1);
EXPECT_EQ(controller()
.last_sent_file_upload_request()
->objects_request()
.request_context()
.request_id()
.media_type(),
lens::LensOverlayRequestId::MEDIA_TYPE_PDF);
// Check that the routing info is in the vsrid.
EXPECT_EQ(controller()
.GetFileInfo(file_token)
->GetRequestIdForTesting()
->routing_info()
.cell_address(),
kTestCellAddress);
EXPECT_EQ(controller()
.GetFileInfo(file_token)
->GetRequestIdForTesting()
->routing_info()
.server_address(),
kTestServerAddress);
}
TEST_F(ComposeboxQueryControllerTest, UploadInvalidMimeTypeFileRequestFailure) {
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
std::unique_ptr<ComposeboxQueryController::FileInfo> file_info =
std::make_unique<ComposeboxQueryController::FileInfo>();
file_info->file_token_ = file_token;
lens::MimeType mime_type = lens::MimeType::kUnknown;
file_info->mime_type_ = mime_type;
controller().StartFileUploadFlow(
std::move(file_info), base::MakeRefCounted<base::RefCountedBytes>(),
/*image_options=*/std::nullopt);
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, mime_type, FileUploadStatus::kValidationFailed,
FileUploadErrorType::kBrowserProcessingError);
}
TEST_F(ComposeboxQueryControllerTest, UploadFileRequestSuccessWithOAuth) {
// Arrange: Make primary account available.
identity_test_env()->MakePrimaryAccountAvailable(
kTestUser, signin::ConsentLevel::kSignin);
// Act: Start the session.
controller().NotifySessionStarted();
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
access_token_info().token, access_token_info().expiration_time,
access_token_info().id_token);
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
StartPdfFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>());
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
access_token_info().token, access_token_info().expiration_time,
access_token_info().id_token);
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, lens::MimeType::kPdf);
}
TEST_F(ComposeboxQueryControllerTest, UploadFileAndWaitForClusterInfoExpire) {
// Enable cluster info TTL.
controller().set_enable_cluster_info_ttl(true);
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
StartPdfFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>());
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, lens::MimeType::kPdf);
// Wait 1 hour.
task_environment().FastForwardBy(base::Hours(1));
// Assert: Validate file upload request and status changes.
FileUploadStatusTuple expired_file_upload_status =
file_upload_status_future_.Take();
EXPECT_EQ(file_token, std::get<0>(expired_file_upload_status));
EXPECT_EQ(lens::MimeType::kPdf, std::get<1>(expired_file_upload_status));
EXPECT_EQ(FileUploadStatus::kUploadExpired,
std::get<2>(expired_file_upload_status));
EXPECT_EQ(std::nullopt, std::get<3>(expired_file_upload_status));
}
TEST_F(ComposeboxQueryControllerTest,
UploadFileRequestWithOAuthAndDelayedClusterInfo) {
// Arrange: Make primary account available.
identity_test_env()->MakePrimaryAccountAvailable(
kTestUser, signin::ConsentLevel::kSignin);
// Arrange: Listen for the controller state changes.
base::test::TestFuture<QueryControllerState> controller_state_future;
controller().set_on_query_controller_state_changed_callback(
controller_state_future.GetRepeatingCallback());
// Act: Start the session.
controller().NotifySessionStarted();
// Act: Start the file upload flow without waiting for the cluster info
// request to complete.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
StartPdfFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>());
// Assert: Validate file upload status change.
FileUploadStatusTuple processing_file_upload_status =
file_upload_status_future_.Take();
EXPECT_EQ(file_token, std::get<0>(processing_file_upload_status));
EXPECT_EQ(lens::MimeType::kPdf, std::get<1>(processing_file_upload_status));
EXPECT_EQ(FileUploadStatus::kProcessing,
std::get<2>(processing_file_upload_status));
EXPECT_EQ(std::nullopt, std::get<3>(processing_file_upload_status));
// Act: Send the oauth token for the cluster info or file upload request.
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
access_token_info().token, access_token_info().expiration_time,
access_token_info().id_token);
// Act: Send the oauth token for the other request.
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
access_token_info().token, access_token_info().expiration_time,
access_token_info().id_token);
// Assert: Validate cluster info request and state changes.
EXPECT_EQ(QueryControllerState::kAwaitingClusterInfoResponse,
controller_state_future.Take());
EXPECT_EQ(QueryControllerState::kAwaitingClusterInfoResponse,
controller().query_controller_state());
EXPECT_EQ(QueryControllerState::kClusterInfoReceived,
controller_state_future.Take());
EXPECT_EQ(QueryControllerState::kClusterInfoReceived,
controller().query_controller_state());
EXPECT_EQ(controller().num_cluster_info_fetch_requests_sent(), 1);
// Assert: Validate file upload request and status changes.
FileUploadStatusTuple upload_started_file_upload_status =
file_upload_status_future_.Take();
EXPECT_EQ(file_token, std::get<0>(upload_started_file_upload_status));
EXPECT_EQ(lens::MimeType::kPdf,
std::get<1>(upload_started_file_upload_status));
EXPECT_EQ(FileUploadStatus::kUploadStarted,
std::get<2>(upload_started_file_upload_status));
EXPECT_EQ(std::nullopt, std::get<3>(upload_started_file_upload_status));
FileUploadStatusTuple upload_successful_file_upload_status =
file_upload_status_future_.Take();
EXPECT_EQ(file_token, std::get<0>(upload_successful_file_upload_status));
EXPECT_EQ(lens::MimeType::kPdf,
std::get<1>(upload_successful_file_upload_status));
EXPECT_EQ(FileUploadStatus::kUploadSuccessful,
std::get<2>(upload_successful_file_upload_status));
EXPECT_EQ(std::nullopt, std::get<3>(upload_successful_file_upload_status));
EXPECT_EQ(controller().num_file_upload_requests_sent(), 1);
EXPECT_THAT(GetGsessionIdFromUrl(controller().last_sent_fetch_url()),
testing::Optional(std::string(kTestServerSessionId)));
}
TEST_F(ComposeboxQueryControllerTest, CreateClientContextHasCorrectValues) {
// Act: Get the client context.
lens::LensOverlayClientContext client_context = controller().client_context();
// Assert: Validate the client context values.
EXPECT_EQ(client_context.surface(), lens::SURFACE_CHROME_NTP);
EXPECT_EQ(client_context.platform(), lens::PLATFORM_LENS_OVERLAY);
EXPECT_EQ(client_context.locale_context().language(), kLocale);
EXPECT_EQ(client_context.locale_context().region(), kRegion);
EXPECT_EQ(client_context.locale_context().time_zone(), kTimeZone);
}
TEST_F(ComposeboxQueryControllerTest, AbandonSessionClearsFiles) {
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
StartPdfFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>());
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, lens::MimeType::kPdf);
// Act: Abandon the session.
controller().NotifySessionAbandoned();
// Assert: Validate the state change.
EXPECT_EQ(QueryControllerState::kOff, controller_state_future_.Take());
// Act: Start the session again.
controller().NotifySessionStarted();
// Assert: Validate the state change.
EXPECT_EQ(QueryControllerState::kAwaitingClusterInfoResponse,
controller_state_future_.Take());
// Assert: Validate the state change.
EXPECT_EQ(QueryControllerState::kClusterInfoReceived,
controller_state_future_.Take());
// Act: Generate the destination URL for the query.
GURL aim_url = controller().CreateAimUrl("test", kTestQueryStartTime);
// Assert: Lens request id is NOT added to unimodal text queries.
std::string vsrid_value;
EXPECT_FALSE(net::GetValueForKeyInQuery(aim_url, kRequestIdParameterKey,
&vsrid_value));
// Assert: Visual input type is NOT added to unimodal text queries.
std::string vit_value;
EXPECT_FALSE(net::GetValueForKeyInQuery(aim_url, kVisualInputTypeParameterKey,
&vit_value));
// Assert: Gsession id is NOT added to unimodal text queries.
std::string gsession_id_value;
EXPECT_FALSE(net::GetValueForKeyInQuery(aim_url, kSessionIdQueryParameterKey,
&gsession_id_value));
// Check that the timestamps are attached to the url.
std::string qsubts_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(
aim_url, kQuerySubmissionTimeQueryParameter, &qsubts_value));
std::string cud_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(
aim_url, kClientUploadDurationQueryParameter, &cud_value));
}
TEST_F(ComposeboxQueryControllerTest,
AbandonSessionPreventsMultipleClusterInfoFetch) {
// Enable cluster info TTL.
controller().set_enable_cluster_info_ttl(true);
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Abandon the session.
controller().NotifySessionAbandoned();
// Assert: Validate the state change.
EXPECT_EQ(QueryControllerState::kOff, controller_state_future_.Take());
// Act: Start the session again.
controller().NotifySessionStarted();
// Assert: Validate the state change.
EXPECT_EQ(QueryControllerState::kAwaitingClusterInfoResponse,
controller_state_future_.Take());
// Assert: Validate the state change.
EXPECT_EQ(QueryControllerState::kClusterInfoReceived,
controller_state_future_.Take());
// Wait 45 minutes, long enough for the cluster info to expire once.
task_environment().FastForwardBy(base::Minutes(45));
// Assert: Validate the state change sequence.
EXPECT_EQ(QueryControllerState::kClusterInfoInvalid,
controller_state_future_.Take());
EXPECT_EQ(QueryControllerState::kAwaitingClusterInfoResponse,
controller_state_future_.Take());
EXPECT_EQ(QueryControllerState::kClusterInfoReceived,
controller_state_future_.Take());
// Assert: The cluster info fetch request was only sent 3 times.
EXPECT_EQ(controller().num_cluster_info_fetch_requests_sent(), 3);
}
TEST_F(ComposeboxQueryControllerTest,
UnimodalTextQuerySubmittedWithInvalidClusterInfoSuccess) {
controller().set_next_cluster_info_request_should_return_error(true);
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo(QueryControllerState::kClusterInfoInvalid);
// Act: Generate the destination URL for the query.
GURL aim_url = controller().CreateAimUrl("test", kTestQueryStartTime);
// Assert: Lens request id is NOT added to unimodal text queries.
std::string vsrid_value;
EXPECT_FALSE(net::GetValueForKeyInQuery(aim_url, kRequestIdParameterKey,
&vsrid_value));
// Assert: Visual input type is NOT added to unimodal text queries.
std::string vit_value;
EXPECT_FALSE(net::GetValueForKeyInQuery(aim_url, kVisualInputTypeParameterKey,
&vit_value));
// Assert: Gsession id is NOT added to unimodal text queries.
std::string gsession_id_value;
EXPECT_FALSE(net::GetValueForKeyInQuery(aim_url, kSessionIdQueryParameterKey,
&gsession_id_value));
// Check that the timestamps are attached to the url.
std::string qsubts_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(
aim_url, kQuerySubmissionTimeQueryParameter, &qsubts_value));
std::string cud_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(
aim_url, kClientUploadDurationQueryParameter, &cud_value));
}
TEST_F(ComposeboxQueryControllerTest, QuerySubmitted) {
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Generate the destination URL for the query.
GURL aim_url = controller().CreateAimUrl("test", kTestQueryStartTime);
// Assert: Lens request id is NOT added to unimodal text queries.
std::string vsrid_value;
EXPECT_FALSE(net::GetValueForKeyInQuery(aim_url, kRequestIdParameterKey,
&vsrid_value));
// Assert: Visual input type is NOT added to unimodal text queries.
std::string vit_value;
EXPECT_FALSE(net::GetValueForKeyInQuery(aim_url, kVisualInputTypeParameterKey,
&vit_value));
// Assert: Gsession id is NOT added to unimodal text queries.
std::string gsession_id_value;
EXPECT_FALSE(net::GetValueForKeyInQuery(aim_url, kSessionIdQueryParameterKey,
&gsession_id_value));
// Check that the timestamps are attached to the url.
std::string qsubts_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(
aim_url, kQuerySubmissionTimeQueryParameter, &qsubts_value));
std::string cud_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(
aim_url, kClientUploadDurationQueryParameter, &cud_value));
}
TEST_F(ComposeboxQueryControllerTest, QuerySubmittedWithUploadedPdf) {
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
StartPdfFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>());
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, lens::MimeType::kPdf);
// Act: Create the destination URL for the query. The destination URL can
// only be created after the cluster info is received.
GURL aim_url = controller().CreateAimUrl("hello", kTestQueryStartTime);
// Assert: Lens request id is NOT added to multimodal pdf queries.
std::string vsrid_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(aim_url, kRequestIdParameterKey,
&vsrid_value));
EXPECT_FALSE(vsrid_value.empty());
EXPECT_EQ(lens::LensOverlayRequestId::MEDIA_TYPE_PDF,
DecodeRequestIdFromVsrid(vsrid_value).media_type());
// Assert: Visual input type is set to pdf for multimodal pdf queries.
std::string vit_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(aim_url, kVisualInputTypeParameterKey,
&vit_value));
EXPECT_EQ(vit_value, "pdf");
// Assert: Gsession id is added to multimodal pdf queries.
std::string gsession_id_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(aim_url, kSessionIdQueryParameterKey,
&gsession_id_value));
EXPECT_EQ(kTestSearchSessionId, gsession_id_value);
// Check that the timestamps are attached to the url.
std::string qsubts_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(
aim_url, kQuerySubmissionTimeQueryParameter, &qsubts_value));
std::string cud_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(
aim_url, kClientUploadDurationQueryParameter, &cud_value));
}
#if !BUILDFLAG(IS_IOS)
TEST_F(ComposeboxQueryControllerTest, QuerySubmittedWithUploadedImage) {
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
std::vector<uint8_t> image_bytes = CreateJPGBytes(100, 100);
composebox::ImageEncodingOptions image_options{.max_size = 1000000,
.max_height = 1000,
.max_width = 1000,
.compression_quality = 30};
StartImageFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>(image_bytes),
image_options);
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, lens::MimeType::kImage);
// Act: Create the destination URL for the query. The destination URL can
// only be created after the cluster info is received.
GURL aim_url = controller().CreateAimUrl("hello", kTestQueryStartTime);
// Assert: Lens request id is NOT added to multimodal pdf queries.
std::string vsrid_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(aim_url, kRequestIdParameterKey,
&vsrid_value));
EXPECT_FALSE(vsrid_value.empty());
EXPECT_EQ(lens::LensOverlayRequestId::MEDIA_TYPE_DEFAULT_IMAGE,
DecodeRequestIdFromVsrid(vsrid_value).media_type());
// Assert: Visual input type is set to img for multimodal image queries.
std::string vit_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(aim_url, kVisualInputTypeParameterKey,
&vit_value));
EXPECT_EQ(vit_value, "img");
// Assert: Gsession id is added to multimodal pdf queries.
std::string gsession_id_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(aim_url, kSessionIdQueryParameterKey,
&gsession_id_value));
EXPECT_EQ(kTestSearchSessionId, gsession_id_value);
// Check that the timestamps are attached to the url.
std::string qsubts_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(
aim_url, kQuerySubmissionTimeQueryParameter, &qsubts_value));
std::string cud_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(
aim_url, kClientUploadDurationQueryParameter,
&cud_value));
}
#endif // !BUILDFLAG(IS_IOS)
TEST_F(ComposeboxQueryControllerTest,
QuerySubmittedWithUploadedPdfButInvalidClusterInfoIsUnimodal) {
// Enable cluster info TTL.
controller().set_enable_cluster_info_ttl(true);
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Ensure that future cluster info requests fail.
controller().set_next_cluster_info_request_should_return_error(true);
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
StartPdfFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>());
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, lens::MimeType::kPdf);
// Wait 1 hour.
task_environment().FastForwardBy(base::Hours(1));
// Assert: Validate cluster info request and state changes.
EXPECT_EQ(QueryControllerState::kClusterInfoInvalid,
controller().query_controller_state());
// Act: Create the destination URL for the query.
GURL aim_url = controller().CreateAimUrl("hello", kTestQueryStartTime);
// Assert: Lens request id is NOT added to unimodal text queries.
std::string vsrid_value;
EXPECT_FALSE(net::GetValueForKeyInQuery(aim_url, kRequestIdParameterKey,
&vsrid_value));
// Assert: Visual input type is NOT added to unimodal text queries.
std::string vit_value;
EXPECT_FALSE(net::GetValueForKeyInQuery(aim_url, kVisualInputTypeParameterKey,
&vit_value));
// Assert: Gsession id is NOT added to unimodal text queries.
std::string gsession_id_value;
EXPECT_FALSE(net::GetValueForKeyInQuery(aim_url, kSessionIdQueryParameterKey,
&gsession_id_value));
}
TEST_F(ComposeboxQueryControllerTest, DeleteFile_Success) {
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
StartPdfFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>());
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, lens::MimeType::kPdf);
// Check that file is in cache.
EXPECT_TRUE(controller().GetFileInfo(file_token));
// Delete file.
const bool deleted = controller().DeleteFile(file_token);
// Check that file is no longer in cache.
EXPECT_TRUE(deleted);
EXPECT_FALSE(controller().GetFileInfo(file_token));
}
TEST_F(ComposeboxQueryControllerTest, DeleteFile_Failed) {
identity_test_env()->MakePrimaryAccountAvailable(
kTestUser, signin::ConsentLevel::kSignin);
// Wait until the state changes to kClusterInfoReceived.
base::RunLoop cluster_info_run_loop;
controller().set_on_query_controller_state_changed_callback(
base::BindLambdaForTesting([&](QueryControllerState state) {
if (state == QueryControllerState::kClusterInfoReceived) {
cluster_info_run_loop.Quit();
}
}));
// Start the session.
controller().NotifySessionStarted();
// Delete file.
const bool deleted =
controller().DeleteFile(base::UnguessableToken::Create());
EXPECT_FALSE(deleted);
}
TEST_F(ComposeboxQueryControllerTest, ClearFiles) {
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
StartPdfFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>());
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, lens::MimeType::kPdf);
// Check that file is in cache.
EXPECT_TRUE(controller().GetFileInfo(file_token));
// Clear files.
controller().ClearFiles();
// Check that file is no longer in cache.
EXPECT_FALSE(controller().GetFileInfo(file_token));
}
TEST_F(ComposeboxQueryControllerTest, QuerySubmittedWithLnsSurface) {
CreateController(/*send_lns_surface=*/true);
// Act: Start the session.
controller().NotifySessionStarted();
// Assert: Validate cluster info request and state changes.
WaitForClusterInfo();
// Act: Start the file upload flow.
const base::UnguessableToken file_token = base::UnguessableToken::Create();
StartPdfFileUploadFlow(
file_token,
/*file_data=*/base::MakeRefCounted<base::RefCountedBytes>());
// Assert: Validate file upload request and status changes.
WaitForFileUpload(file_token, lens::MimeType::kPdf);
// Act: Create the destination URL for the query. The destination URL can
// only be created after the cluster info is received.
GURL aim_url = controller().CreateAimUrl("hello", kTestQueryStartTime);
// Assert: Lns surface is added to the url.
std::string lns_surface_value;
EXPECT_TRUE(net::GetValueForKeyInQuery(aim_url, kLnsSurfaceParameterKey,
&lns_surface_value));
EXPECT_EQ(lns_surface_value, "47");
}