blob: c96599c97037aabf0757019ddc2464c658fc80d4 [file] [log] [blame] [edit]
// 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.
#include "ash/scanner/scanner_action_handler.h"
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include "ash/scanner/scanner_command.h"
#include "ash/scanner/scanner_command_delegate.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "components/drive/drive_api_util.h"
#include "components/drive/service/drive_service_interface.h"
#include "components/drive/service/fake_drive_service.h"
#include "components/manta/proto/scanner.pb.h"
#include "google_apis/common/api_error_codes.h"
#include "google_apis/common/dummy_auth_service.h"
#include "google_apis/common/request_sender.h"
#include "google_apis/drive/drive_api_parser.h"
#include "google_apis/gaia/gaia_urls.h"
#include "google_apis/gaia/gaia_urls_overrider_for_testing.h"
#include "google_apis/people/people_api_request_types.h"
#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/test/test_shared_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/clipboard/clipboard_data.h"
#include "url/gurl.h"
namespace ash {
namespace {
using ::base::test::IsJson;
using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::Field;
using ::testing::FieldsAre;
using ::testing::HasSubstr;
using ::testing::Pointee;
using ::testing::Property;
using ::testing::ResultOf;
using ::testing::Return;
using ::testing::VariantWith;
constexpr std::string_view kJsonMimeType = "application/json";
class TestScannerCommandDelegate : public ScannerCommandDelegate {
public:
MOCK_METHOD(void, OpenUrl, (const GURL& url), (override));
MOCK_METHOD(drive::DriveServiceInterface*, GetDriveService, (), (override));
MOCK_METHOD(google_apis::RequestSender*,
GetGoogleApisRequestSender,
(),
(override));
MOCK_METHOD(void,
SetClipboard,
(std::unique_ptr<ui::ClipboardData> data),
(override));
base::WeakPtr<ScannerCommandDelegate> GetWeakPtr() override {
return weak_factory_.GetWeakPtr();
}
private:
base::WeakPtrFactory<TestScannerCommandDelegate> weak_factory_{this};
};
constexpr std::string_view kGoogleCalendarHost = "calendar.google.com";
constexpr std::string_view kGoogleCalendarRenderPath = "/calendar/render";
// gMock matchers must match on const refs. Turning a `Contact` into a
// `base::Value::Dict` requires a rvalue reference, so explicitly create a copy
// to turn it into a dict.
base::Value::Dict ContactToDict(const google_apis::people::Contact& contact) {
return google_apis::people::Contact(contact).ToDict();
}
TEST(ScannerActionToCommandTest, NewEvent) {
manta::proto::ScannerAction action;
action.mutable_new_event();
ScannerCommand command = ScannerActionToCommand(std::move(action));
EXPECT_THAT(
command,
VariantWith<OpenUrlCommand>(FieldsAre(AllOf(
Property("host_piece", &GURL::host_piece, kGoogleCalendarHost),
Property("path_piece", &GURL::path_piece, kGoogleCalendarRenderPath),
Property("query_piece", &GURL::query_piece, "action=TEMPLATE")))));
}
TEST(ScannerActionToCommandTest, NewEventWithTitle) {
manta::proto::ScannerAction action;
manta::proto::NewEventAction& new_event = *action.mutable_new_event();
new_event.set_title("Test title?");
ScannerCommand command = ScannerActionToCommand(std::move(action));
EXPECT_THAT(
command,
VariantWith<OpenUrlCommand>(FieldsAre(AllOf(
Property("host_piece", &GURL::host_piece, kGoogleCalendarHost),
Property("path_piece", &GURL::path_piece, kGoogleCalendarRenderPath),
Property("query_piece", &GURL::query_piece,
"action=TEMPLATE&text=Test+title%3F")))));
}
TEST(ScannerActionToCommandTest, NewEventWithDescription) {
manta::proto::ScannerAction action;
manta::proto::NewEventAction& new_event = *action.mutable_new_event();
new_event.set_description("Test desc?");
ScannerCommand command = ScannerActionToCommand(std::move(action));
EXPECT_THAT(
command,
VariantWith<OpenUrlCommand>(FieldsAre(AllOf(
Property("host_piece", &GURL::host_piece, kGoogleCalendarHost),
Property("path_piece", &GURL::path_piece, kGoogleCalendarRenderPath),
Property("query_piece", &GURL::query_piece,
"action=TEMPLATE&details=Test+desc%3F")))));
}
TEST(ScannerActionToCommandTest, NewEventWithDates) {
manta::proto::ScannerAction action;
manta::proto::NewEventAction& new_event = *action.mutable_new_event();
new_event.set_dates("20241014T160000/20241014T161500");
ScannerCommand command = ScannerActionToCommand(std::move(action));
EXPECT_THAT(
command,
VariantWith<OpenUrlCommand>(FieldsAre(AllOf(
Property("host_piece", &GURL::host_piece, kGoogleCalendarHost),
Property("path_piece", &GURL::path_piece, kGoogleCalendarRenderPath),
Property(
"query_piece", &GURL::query_piece,
"action=TEMPLATE&dates=20241014T160000%2F20241014T161500")))));
}
TEST(ScannerActionToCommandTest, NewEventWithLocation) {
manta::proto::ScannerAction action;
manta::proto::NewEventAction& new_event = *action.mutable_new_event();
new_event.set_location("401 - Unauthorized");
ScannerCommand command = ScannerActionToCommand(std::move(action));
EXPECT_THAT(
command,
VariantWith<OpenUrlCommand>(FieldsAre(AllOf(
Property("host_piece", &GURL::host_piece, kGoogleCalendarHost),
Property("path_piece", &GURL::path_piece, kGoogleCalendarRenderPath),
Property("query_piece", &GURL::query_piece,
"action=TEMPLATE&location=401+-+Unauthorized")))));
}
TEST(ScannerActionToCommandTest, NewEventWithMultipleFields) {
manta::proto::ScannerAction action;
manta::proto::NewEventAction& new_event = *action.mutable_new_event();
new_event.set_title("🌏");
new_event.set_description("formerly \"Geo Sync\"");
new_event.set_dates("20241014T160000/20241014T161500");
new_event.set_location("Wonderland");
ScannerCommand command = ScannerActionToCommand(std::move(action));
EXPECT_THAT(
command,
VariantWith<OpenUrlCommand>(FieldsAre(AllOf(
Property("host_piece", &GURL::host_piece, kGoogleCalendarHost),
Property("path_piece", &GURL::path_piece, kGoogleCalendarRenderPath),
Property("query_piece", &GURL::query_piece,
"action=TEMPLATE"
"&text=%F0%9F%8C%8F"
"&details=formerly+%22Geo+Sync%22"
"&dates=20241014T160000%2F20241014T161500"
"&location=Wonderland")))));
}
TEST(ScannerActionToCommandTest, NewContact) {
manta::proto::ScannerAction action;
action.mutable_new_contact();
ScannerCommand command = ScannerActionToCommand(std::move(action));
EXPECT_THAT(std::move(command), VariantWith<CreateContactCommand>(FieldsAre(
ResultOf(&ContactToDict, IsJson("{}")))));
}
TEST(ScannerActionToCommandTest, NewContactWithGivenName) {
manta::proto::ScannerAction action;
manta::proto::NewContactAction& new_contact = *action.mutable_new_contact();
new_contact.set_given_name("Léa");
ScannerCommand command = ScannerActionToCommand(std::move(action));
constexpr std::string_view kExpectedJson = R"json({
"names": [
{
"givenName": "Léa",
},
],
})json";
EXPECT_THAT(std::move(command),
VariantWith<CreateContactCommand>(
FieldsAre(ResultOf(&ContactToDict, IsJson(kExpectedJson)))));
}
TEST(ScannerActionToCommandTest, NewContactWithFamilyName) {
manta::proto::ScannerAction action;
manta::proto::NewContactAction& new_contact = *action.mutable_new_contact();
new_contact.set_family_name("François");
ScannerCommand command = ScannerActionToCommand(std::move(action));
constexpr std::string_view kExpectedJson = R"json({
"names": [
{
"familyName": "François",
},
],
})json";
EXPECT_THAT(std::move(command),
VariantWith<CreateContactCommand>(
FieldsAre(ResultOf(&ContactToDict, IsJson(kExpectedJson)))));
}
TEST(ScannerActionToCommandTest, NewContactWithDeprecatedEmail) {
manta::proto::ScannerAction action;
manta::proto::NewContactAction& new_contact = *action.mutable_new_contact();
new_contact.set_email("afrancois@example.com");
ScannerCommand command = ScannerActionToCommand(std::move(action));
constexpr std::string_view kExpectedJson = R"json({
"emailAddresses": [
{
"value": "afrancois@example.com",
},
],
})json";
EXPECT_THAT(std::move(command),
VariantWith<CreateContactCommand>(
FieldsAre(ResultOf(&ContactToDict, IsJson(kExpectedJson)))));
}
TEST(ScannerActionToCommandTest, NewContactWithEmailAddresses) {
manta::proto::ScannerAction action;
manta::proto::NewContactAction& new_contact = *action.mutable_new_contact();
manta::proto::NewContactAction::EmailAddress& home_email =
*new_contact.add_email_addresses();
home_email.set_value("afrancois@example.com");
home_email.set_type("home");
manta::proto::NewContactAction::EmailAddress& work_email =
*new_contact.add_email_addresses();
work_email.set_value("afrancois@work.example.com");
work_email.set_type("work");
ScannerCommand command = ScannerActionToCommand(std::move(action));
constexpr std::string_view kExpectedJson = R"json({
"emailAddresses": [
{
"value": "afrancois@example.com",
"type": "home",
},
{
"value": "afrancois@work.example.com",
"type": "work",
},
],
})json";
EXPECT_THAT(std::move(command),
VariantWith<CreateContactCommand>(
FieldsAre(ResultOf(&ContactToDict, IsJson(kExpectedJson)))));
}
TEST(ScannerActionToCommandTest,
NewContactWithEmailAddressesAndDeprecatedEmail) {
manta::proto::ScannerAction action;
manta::proto::NewContactAction& new_contact = *action.mutable_new_contact();
new_contact.set_email("afrancois@example.com");
manta::proto::NewContactAction::EmailAddress& home_email =
*new_contact.add_email_addresses();
home_email.set_value("afrancois@example.com");
home_email.set_type("home");
manta::proto::NewContactAction::EmailAddress& work_email =
*new_contact.add_email_addresses();
work_email.set_value("afrancois@work.example.com");
work_email.set_type("work");
ScannerCommand command = ScannerActionToCommand(std::move(action));
constexpr std::string_view kExpectedJson = R"json({
"emailAddresses": [
{
"value": "afrancois@example.com",
"type": "home",
},
{
"value": "afrancois@work.example.com",
"type": "work",
},
],
})json";
EXPECT_THAT(std::move(command),
VariantWith<CreateContactCommand>(
FieldsAre(ResultOf(&ContactToDict, IsJson(kExpectedJson)))));
}
TEST(ScannerActionToCommandTest, NewContactWithDeprecatedPhone) {
manta::proto::ScannerAction action;
manta::proto::NewContactAction& new_contact = *action.mutable_new_contact();
new_contact.set_phone("+61400000000");
ScannerCommand command = ScannerActionToCommand(std::move(action));
constexpr std::string_view kExpectedJson = R"json({
"phoneNumbers": [
{
"value": "+61400000000",
},
],
})json";
EXPECT_THAT(std::move(command),
VariantWith<CreateContactCommand>(
FieldsAre(ResultOf(&ContactToDict, IsJson(kExpectedJson)))));
}
TEST(ScannerActionToCommandTest, NewContactWithPhoneNumbers) {
manta::proto::ScannerAction action;
manta::proto::NewContactAction& new_contact = *action.mutable_new_contact();
manta::proto::NewContactAction::PhoneNumber& mobile_number =
*new_contact.add_phone_numbers();
mobile_number.set_value("+61400000000");
mobile_number.set_type("mobile");
manta::proto::NewContactAction::PhoneNumber& home_number =
*new_contact.add_phone_numbers();
home_number.set_value("+61390000000");
home_number.set_type("home");
ScannerCommand command = ScannerActionToCommand(std::move(action));
constexpr std::string_view kExpectedJson = R"json({
"phoneNumbers": [
{
"value": "+61400000000",
"type": "mobile",
},
{
"value": "+61390000000",
"type": "home",
},
],
})json";
EXPECT_THAT(std::move(command),
VariantWith<CreateContactCommand>(
FieldsAre(ResultOf(&ContactToDict, IsJson(kExpectedJson)))));
}
TEST(ScannerActionToCommandTest, NewContactWithPhoneNumbersAndDeprecatedPhone) {
manta::proto::ScannerAction action;
manta::proto::NewContactAction& new_contact = *action.mutable_new_contact();
new_contact.set_phone("+61400000000");
manta::proto::NewContactAction::PhoneNumber& mobile_number =
*new_contact.add_phone_numbers();
mobile_number.set_value("+61400000000");
mobile_number.set_type("mobile");
manta::proto::NewContactAction::PhoneNumber& home_number =
*new_contact.add_phone_numbers();
home_number.set_value("+61390000000");
home_number.set_type("home");
ScannerCommand command = ScannerActionToCommand(std::move(action));
constexpr std::string_view kExpectedJson = R"json({
"phoneNumbers": [
{
"value": "+61400000000",
"type": "mobile",
},
{
"value": "+61390000000",
"type": "home",
},
],
})json";
EXPECT_THAT(std::move(command),
VariantWith<CreateContactCommand>(
FieldsAre(ResultOf(&ContactToDict, IsJson(kExpectedJson)))));
}
TEST(ScannerActionToCommandTest, NewContactWithMultipleFields) {
manta::proto::ScannerAction action;
manta::proto::NewContactAction& new_contact = *action.mutable_new_contact();
new_contact.set_given_name("André");
new_contact.set_family_name("François");
manta::proto::NewContactAction::EmailAddress& home_email =
*new_contact.add_email_addresses();
home_email.set_value("afrancois@example.com");
home_email.set_type("home");
manta::proto::NewContactAction::EmailAddress& work_email =
*new_contact.add_email_addresses();
work_email.set_value("afrancois@work.example.com");
work_email.set_type("work");
manta::proto::NewContactAction::PhoneNumber& mobile_number =
*new_contact.add_phone_numbers();
mobile_number.set_value("+61400000000");
mobile_number.set_type("mobile");
manta::proto::NewContactAction::PhoneNumber& home_number =
*new_contact.add_phone_numbers();
home_number.set_value("+61390000000");
home_number.set_type("home");
ScannerCommand command = ScannerActionToCommand(std::move(action));
constexpr std::string_view kExpectedJson = R"json({
"names": [
{
"givenName": "André",
"familyName": "François",
},
],
"emailAddresses": [
{
"value": "afrancois@example.com",
"type": "home",
},
{
"value": "afrancois@work.example.com",
"type": "work",
},
],
"phoneNumbers": [
{
"value": "+61400000000",
"type": "mobile",
},
{
"value": "+61390000000",
"type": "home",
},
],
})json";
EXPECT_THAT(std::move(command),
VariantWith<CreateContactCommand>(
FieldsAre(ResultOf(&ContactToDict, IsJson(kExpectedJson)))));
}
TEST(ScannerActionToCommandTest, NewGoogleDoc) {
manta::proto::ScannerAction action;
manta::proto::NewGoogleDocAction& new_google_doc =
*action.mutable_new_google_doc();
new_google_doc.set_title("Doc Title");
new_google_doc.set_html_contents("<span>Contents</span>");
ScannerCommand command = ScannerActionToCommand(std::move(action));
EXPECT_THAT(
command,
VariantWith<DriveUploadCommand>(FieldsAre(
"Doc Title", "<span>Contents</span>",
/*contents_mime_type=*/"text/html",
/*converted_mime_type=*/drive::util::kGoogleDocumentMimeType)));
}
TEST(ScannerActionToCommandTest, NewGoogleSheet) {
manta::proto::ScannerAction action;
manta::proto::NewGoogleSheetAction& new_google_sheet =
*action.mutable_new_google_sheet();
new_google_sheet.set_title("Sheet Title");
new_google_sheet.set_csv_contents("a,b\n1,2");
ScannerCommand command = ScannerActionToCommand(std::move(action));
EXPECT_THAT(
command,
VariantWith<DriveUploadCommand>(FieldsAre(
"Sheet Title", "a,b\n1,2",
/*contents_mime_type=*/"text/csv",
/*converted_mime_type=*/drive::util::kGoogleSpreadsheetMimeType)));
}
TEST(ScannerActionHandlerTest, CopyToClipboardWithPlainText) {
manta::proto::ScannerAction action;
manta::proto::CopyToClipboardAction& copy_to_clipboard =
*action.mutable_copy_to_clipboard();
copy_to_clipboard.set_plain_text("Hello");
ScannerCommand command = ScannerActionToCommand(std::move(action));
EXPECT_THAT(
command,
VariantWith<CopyToClipboardCommand>(FieldsAre(Pointee(
AllOf(Property("format", &ui::ClipboardData::format,
static_cast<int>(ui::ClipboardInternalFormat::kText)),
Property("text", &ui::ClipboardData::text, "Hello"))))));
}
TEST(ScannerActionHandlerTest, CopyToClipboardWithHtmlText) {
manta::proto::ScannerAction action;
manta::proto::CopyToClipboardAction& copy_to_clipboard =
*action.mutable_copy_to_clipboard();
copy_to_clipboard.set_html_text("<img />");
ScannerCommand command = ScannerActionToCommand(std::move(action));
EXPECT_THAT(
command,
VariantWith<CopyToClipboardCommand>(FieldsAre(Pointee(
AllOf(Property("format", &ui::ClipboardData::format,
static_cast<int>(ui::ClipboardInternalFormat::kHtml)),
Property("markup_data", &ui::ClipboardData::markup_data,
"<img />"))))));
}
TEST(ScannerActionHandlerTest, CopyToClipboardWithMultipleFields) {
manta::proto::ScannerAction action;
manta::proto::CopyToClipboardAction& copy_to_clipboard =
*action.mutable_copy_to_clipboard();
copy_to_clipboard.set_plain_text("Hello");
copy_to_clipboard.set_html_text("<b>Hello</b>");
ScannerCommand command = ScannerActionToCommand(std::move(action));
EXPECT_THAT(
command,
VariantWith<CopyToClipboardCommand>(FieldsAre(Pointee(AllOf(
Property("format", &ui::ClipboardData::format,
static_cast<int>(ui::ClipboardInternalFormat::kText) |
static_cast<int>(ui::ClipboardInternalFormat::kHtml)),
Property("text", &ui::ClipboardData::text, "Hello"),
Property("markup_data", &ui::ClipboardData::markup_data,
"<b>Hello</b>"))))));
}
TEST(ScannerActionHandlerTest, HandlesOpenUrlCommandWithoutDelegate) {
base::test::SingleThreadTaskEnvironment task_environment;
base::test::TestFuture<bool> done_future;
HandleScannerCommand(nullptr, OpenUrlCommand(GURL("https://example.com")),
done_future.GetCallback());
EXPECT_FALSE(done_future.Get());
}
TEST(ScannerActionHandlerTest, OpenUrlCommandOpensUrl) {
base::test::SingleThreadTaskEnvironment task_environment;
TestScannerCommandDelegate delegate;
EXPECT_CALL(delegate, OpenUrl(GURL("https://example.com"))).Times(1);
base::test::TestFuture<bool> done_future;
HandleScannerCommand(delegate.GetWeakPtr(),
OpenUrlCommand(GURL("https://example.com")),
done_future.GetCallback());
EXPECT_TRUE(done_future.Get());
}
TEST(ScannerActionHandlerTest, HandlesDriveUploadCommandWithoutDelegate) {
base::test::TaskEnvironment task_environment;
base::test::TestFuture<bool> done_future;
HandleScannerCommand(
nullptr,
DriveUploadCommand(
"Doc Title", "<span>Contents</span>",
/*contents_mime_type=*/"text/html",
/*converted_mime_type=*/drive::util::kGoogleDocumentMimeType),
done_future.GetCallback());
EXPECT_FALSE(done_future.Get());
}
TEST(ScannerActionHandlerTest,
HandlesDriveUploadCommandWithDelayedDelegateDeletion) {
base::test::TaskEnvironment task_environment;
base::test::TestFuture<bool> done_future;
{
TestScannerCommandDelegate delegate;
EXPECT_CALL(delegate, GetDriveService).Times(0);
HandleScannerCommand(
delegate.GetWeakPtr(),
DriveUploadCommand(
"Doc Title", "<span>Contents</span>",
/*contents_mime_type=*/"text/html",
/*converted_mime_type=*/drive::util::kGoogleDocumentMimeType),
done_future.GetCallback());
// `delegate` is deleted here, invalidating weak pointers.
}
ASSERT_FALSE(done_future.IsReady());
EXPECT_FALSE(done_future.Get());
}
TEST(ScannerActionHandlerTest, DriveUploadCommandOpensAlternateLink) {
base::test::TaskEnvironment task_environment;
drive::FakeDriveService drive_service;
TestScannerCommandDelegate delegate;
EXPECT_CALL(delegate, GetDriveService).WillRepeatedly(Return(&drive_service));
EXPECT_CALL(delegate,
OpenUrl(GURL("https://document_alternate_link/Doc%20Title")))
.Times(1);
base::test::TestFuture<bool> done_future;
HandleScannerCommand(
delegate.GetWeakPtr(),
DriveUploadCommand(
"Doc Title", "<span>Contents</span>",
/*contents_mime_type=*/"text/html",
/*converted_mime_type=*/drive::util::kGoogleDocumentMimeType),
done_future.GetCallback());
EXPECT_TRUE(done_future.Get());
}
TEST(ScannerActionHandlerTest,
DriveUploadCommandCreatesFileWithTitleAndMimeTypeInRoot) {
base::test::TaskEnvironment task_environment;
drive::FakeDriveService drive_service;
{
TestScannerCommandDelegate delegate;
EXPECT_CALL(delegate, GetDriveService)
.WillRepeatedly(Return(&drive_service));
EXPECT_CALL(delegate, OpenUrl).Times(1);
base::test::TestFuture<bool> done_future;
HandleScannerCommand(
delegate.GetWeakPtr(),
DriveUploadCommand(
"Doc Title", "<span>Contents</span>",
/*contents_mime_type=*/"text/html",
/*converted_mime_type=*/drive::util::kGoogleDocumentMimeType),
done_future.GetCallback());
ASSERT_TRUE(done_future.Get());
}
base::test::TestFuture<google_apis::ApiErrorCode,
std::unique_ptr<google_apis::FileList>>
file_list_future;
drive_service.SearchByTitle("Doc Title", drive_service.GetRootResourceId(),
file_list_future.GetCallback());
auto [error, file_list] = file_list_future.Take();
EXPECT_EQ(error, google_apis::ApiErrorCode::HTTP_SUCCESS);
EXPECT_THAT(
file_list,
Pointee(Property(
"items", &google_apis::FileList::items,
ElementsAre(Pointee(AllOf(
Property("title", &google_apis::FileResource::title, "Doc Title"),
Property("mime_type", &google_apis::FileResource::mime_type,
drive::util::kGoogleDocumentMimeType),
Property("parents", &google_apis::FileResource::parents,
ElementsAre(Property(
"file_id", &google_apis::ParentReference::file_id,
drive_service.GetRootResourceId())))))))));
}
TEST(ScannerActionHandlerTest, HandlesCopyToClipboardActionWithoutDelegate) {
base::test::SingleThreadTaskEnvironment task_environment;
base::test::TestFuture<bool> done_future;
HandleScannerCommand(
nullptr, CopyToClipboardCommand{std::make_unique<ui::ClipboardData>()},
done_future.GetCallback());
EXPECT_FALSE(done_future.Get());
}
TEST(ScannerActionHandlerTest, HandlesCopyToClipboardAction) {
base::test::SingleThreadTaskEnvironment task_environment;
TestScannerCommandDelegate delegate;
ui::ClipboardData clipboard_data;
clipboard_data.set_text("Hello");
clipboard_data.set_markup_data("<b>Hello</b>");
EXPECT_CALL(delegate, SetClipboard(Pointee(clipboard_data))).Times(1);
base::test::TestFuture<bool> done_future;
HandleScannerCommand(delegate.GetWeakPtr(),
CopyToClipboardCommand(
std::make_unique<ui::ClipboardData>(clipboard_data)),
done_future.GetCallback());
EXPECT_TRUE(done_future.Get());
}
// Wrapper around an `EmbeddedTestServer` which starts the server in the
// constructor, so a `base_url()` can be obtained immediately after
// construction.
// This is required so `ScannerCreateContactCommandHandlerTest` can initialise
// `gaia_urls_overrider_` in the constructor - which requires `base_url()`.
//
// TODO: b/374624760 - Consider deduplicating this with
// google_apis/people/people_api_requests_unittest.cc.
class MockServer {
public:
MockServer() {
test_server_.RegisterRequestHandler(base::BindRepeating(
&MockServer::HandleRequest, base::Unretained(this)));
CHECK(test_server_.Start());
}
MOCK_METHOD(std::unique_ptr<net::test_server::HttpResponse>,
HandleRequest,
(const net::test_server::HttpRequest& request));
const GURL& base_url() const { return test_server_.base_url(); }
private:
net::test_server::EmbeddedTestServer test_server_;
};
// TODO: b/374624760 - Consider deduplicating this with
// google_apis/people/people_api_requests_unittest.cc.
class ScannerCreateContactCommandHandlerTest : public testing::Test {
public:
ScannerCreateContactCommandHandlerTest()
: request_sender_(
std::make_unique<google_apis::DummyAuthService>(),
base::MakeRefCounted<network::TestSharedURLLoaderFactory>(
/*network_service=*/nullptr,
/*is_trusted=*/true),
task_environment_.GetMainThreadTaskRunner(),
"test-user-agent",
TRAFFIC_ANNOTATION_FOR_TESTS),
gaia_urls_overrider_(base::CommandLine::ForCurrentProcess(),
"people_api_origin_url",
mock_server_.base_url().spec()) {
CHECK_EQ(mock_server_.base_url(),
GaiaUrls::GetInstance()->people_api_origin_url());
}
google_apis::RequestSender& request_sender() { return request_sender_; }
MockServer& mock_server() { return mock_server_; }
private:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::IO};
google_apis::RequestSender request_sender_;
MockServer mock_server_;
GaiaUrlsOverriderForTesting gaia_urls_overrider_;
};
TEST_F(ScannerCreateContactCommandHandlerTest, WithoutDelegate) {
base::test::TestFuture<bool> done_future;
HandleScannerCommand(nullptr,
CreateContactCommand(google_apis::people::Contact()),
done_future.GetCallback());
EXPECT_FALSE(done_future.Get());
}
TEST_F(ScannerCreateContactCommandHandlerTest, WithoutRequestSender) {
TestScannerCommandDelegate delegate;
EXPECT_CALL(delegate, GetGoogleApisRequestSender).WillOnce(Return(nullptr));
base::test::TestFuture<bool> done_future;
HandleScannerCommand(delegate.GetWeakPtr(),
CreateContactCommand(google_apis::people::Contact()),
done_future.GetCallback());
EXPECT_FALSE(done_future.Get());
}
TEST_F(ScannerCreateContactCommandHandlerTest, WithDelayedDelegateDeletion) {
// The response must be valid and successful to attempt to open a URL.
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HttpStatusCode::HTTP_OK);
response->set_content(R"json({"resourceName": "people/c1"})json");
response->set_content_type(kJsonMimeType);
EXPECT_CALL(mock_server(), HandleRequest)
.WillOnce(Return(std::move(response)));
base::test::TestFuture<bool> done_future;
{
testing::StrictMock<TestScannerCommandDelegate> delegate;
EXPECT_CALL(delegate, GetGoogleApisRequestSender)
.WillOnce(Return(&request_sender()));
HandleScannerCommand(delegate.GetWeakPtr(),
CreateContactCommand(google_apis::people::Contact()),
done_future.GetCallback());
// `delegate` is deleted here, invalidating weak pointers.
}
EXPECT_FALSE(done_future.Get());
}
TEST_F(ScannerCreateContactCommandHandlerTest, SendsRequestToServer) {
constexpr std::string_view kContactJson = R"json({
"emailAddresses": [
{
"value": "afrancois@example.com",
"type": "home",
},
{
"value": "afrancois@work.example.com",
"type": "work",
},
],
"names": [
{
"familyName": "Francois",
"givenName": "Andre",
},
],
"phoneNumbers": [
{
"value": "+61400000000",
"type": "mobile",
},
{
"value": "+61390000000",
"type": "home",
},
],
})json";
// We are not interested in the server response in this test - just the server
// request - so return any arbitrary response.
EXPECT_CALL(
mock_server(),
HandleRequest(AllOf(
Field("relative_url", &net::test_server::HttpRequest::relative_url,
HasSubstr("createContact")),
Field("content", &net::test_server::HttpRequest::content,
IsJson(kContactJson)))))
.WillOnce(
Return(std::make_unique<net::test_server::BasicHttpResponse>()));
testing::StrictMock<TestScannerCommandDelegate> delegate;
EXPECT_CALL(delegate, GetGoogleApisRequestSender)
.WillOnce(Return(&request_sender()));
google_apis::people::Contact contact;
google_apis::people::EmailAddress home_email;
home_email.value = "afrancois@example.com";
home_email.type = "home";
contact.email_addresses.push_back(std::move(home_email));
google_apis::people::EmailAddress work_email;
work_email.value = "afrancois@work.example.com";
work_email.type = "work";
contact.email_addresses.push_back(std::move(work_email));
google_apis::people::Name name;
name.family_name = "Francois";
name.given_name = "Andre";
contact.name = std::move(name);
google_apis::people::PhoneNumber mobile_number;
mobile_number.value = "+61400000000";
mobile_number.type = "mobile";
contact.phone_numbers.push_back(std::move(mobile_number));
google_apis::people::PhoneNumber home_number;
home_number.value = "+61390000000";
home_number.type = "home";
contact.phone_numbers.push_back(std::move(home_number));
base::test::TestFuture<bool> done_future;
HandleScannerCommand(delegate.GetWeakPtr(),
CreateContactCommand(std::move(contact)),
done_future.GetCallback());
// We are not interested in the result of the command in this test, just
// whether the command sends the right JSON to the server.
// However, we should still wait for the future to be resolved.
ASSERT_TRUE(done_future.Wait());
}
TEST_F(ScannerCreateContactCommandHandlerTest, OpensEditContactInBrowser) {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HttpStatusCode::HTTP_OK);
response->set_content(R"json({"resourceName": "people/c1"})json");
response->set_content_type(kJsonMimeType);
EXPECT_CALL(mock_server(), HandleRequest)
.WillOnce(Return(std::move(response)));
testing::StrictMock<TestScannerCommandDelegate> delegate;
EXPECT_CALL(delegate, GetGoogleApisRequestSender)
.WillOnce(Return(&request_sender()));
EXPECT_CALL(delegate,
OpenUrl(GURL("https://contacts.google.com/person/c1?edit=1")))
.Times(1);
base::test::TestFuture<bool> done_future;
HandleScannerCommand(delegate.GetWeakPtr(),
CreateContactCommand(google_apis::people::Contact()),
done_future.GetCallback());
EXPECT_TRUE(done_future.Get());
}
TEST_F(ScannerCreateContactCommandHandlerTest, HandlesServerErrors) {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HttpStatusCode::HTTP_INTERNAL_SERVER_ERROR);
EXPECT_CALL(mock_server(), HandleRequest)
.WillOnce(Return(std::move(response)));
testing::StrictMock<TestScannerCommandDelegate> delegate;
EXPECT_CALL(delegate, GetGoogleApisRequestSender)
.WillOnce(Return(&request_sender()));
base::test::TestFuture<bool> done_future;
HandleScannerCommand(delegate.GetWeakPtr(),
CreateContactCommand(google_apis::people::Contact()),
done_future.GetCallback());
EXPECT_FALSE(done_future.Get());
}
TEST_F(ScannerCreateContactCommandHandlerTest, HandlesInvalidResourceNames) {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HttpStatusCode::HTTP_OK);
response->set_content(R"json({"resourceName": "c1"})json");
response->set_content_type(kJsonMimeType);
EXPECT_CALL(mock_server(), HandleRequest)
.WillOnce(Return(std::move(response)));
testing::StrictMock<TestScannerCommandDelegate> delegate;
EXPECT_CALL(delegate, GetGoogleApisRequestSender)
.WillOnce(Return(&request_sender()));
base::test::TestFuture<bool> done_future;
HandleScannerCommand(delegate.GetWeakPtr(),
CreateContactCommand(google_apis::people::Contact()),
done_future.GetCallback());
EXPECT_FALSE(done_future.Get());
}
TEST_F(ScannerCreateContactCommandHandlerTest,
HandlesResourceNamesWithPathTraversal) {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HttpStatusCode::HTTP_OK);
response->set_content(
R"json({"resourceName": "people/../deleteAccount"})json");
response->set_content_type(kJsonMimeType);
EXPECT_CALL(mock_server(), HandleRequest)
.WillOnce(Return(std::move(response)));
testing::StrictMock<TestScannerCommandDelegate> delegate;
EXPECT_CALL(delegate, GetGoogleApisRequestSender)
.WillOnce(Return(&request_sender()));
base::test::TestFuture<bool> done_future;
HandleScannerCommand(delegate.GetWeakPtr(),
CreateContactCommand(google_apis::people::Contact()),
done_future.GetCallback());
EXPECT_FALSE(done_future.Get());
}
} // namespace
} // namespace ash