blob: 67343c03c2e58a116ee51e85c61d5be7d5fe0886 [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 "ash/scanner/scanner_feedback.h"
#include <cstddef>
#include <optional>
#include <string>
#include <utility>
#include "base/containers/span.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "components/manta/proto/scanner.pb.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace ash {
namespace {
using ::base::test::IsJson;
TEST(ScannerFeedbackTest, UnsetActionToDict) {
base::Value::Dict dict = ScannerActionToDict(manta::proto::ScannerAction());
EXPECT_THAT(dict, IsJson("{}"));
}
TEST(ScannerFeedbackTest, NewEventActionToDict) {
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");
base::Value::Dict dict = ScannerActionToDict(std::move(action));
EXPECT_THAT(dict, IsJson(R"json({
"new_event": {
"title": "🌏",
"description": "formerly \"Geo Sync\"",
"dates": "20241014T160000/20241014T161500",
"location": "Wonderland",
}
})json"));
}
TEST(ScannerFeedbackTest, NewContactActionToDict) {
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");
new_contact.set_email("afrancois@example.com");
new_contact.set_phone("+61400000000");
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");
base::Value::Dict dict = ScannerActionToDict(std::move(action));
EXPECT_THAT(dict, IsJson(R"json({
"new_contact": {
"given_name": "André",
"family_name": "François",
"email": "afrancois@example.com",
"phone": "+61400000000",
"email_addresses": [
{
"value": "afrancois@example.com",
"type": "home",
},
{
"value": "afrancois@work.example.com",
"type": "work",
},
],
"phone_numbers": [
{
"value": "+61400000000",
"type": "mobile",
},
{
"value": "+61390000000",
"type": "home",
},
],
}
})json"));
}
TEST(ScannerFeedbackTest, NewGoogleDocActionToDict) {
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>");
base::Value::Dict dict = ScannerActionToDict(std::move(action));
EXPECT_THAT(dict, IsJson(R"json({
"new_google_doc": {
"title": "Doc Title",
"html_contents": "<span>Contents</span>",
}
})json"));
}
TEST(ScannerFeedbackTest, NewGoogleSheetActionToDict) {
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");
base::Value::Dict dict = ScannerActionToDict(std::move(action));
EXPECT_THAT(dict, IsJson(R"json({
"new_google_sheet": {
"title": "Sheet Title",
"csv_contents": "a,b\n1,2",
}
})json"));
}
TEST(ScannerFeedbackTest, CopyToClipboardActionToDict) {
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>");
base::Value::Dict dict = ScannerActionToDict(std::move(action));
EXPECT_THAT(dict, IsJson(R"json({
"copy_to_clipboard": {
"plain_text": "Hello",
"html_text": "<b>Hello</b>",
}
})json"));
}
struct ValueToUserFacingStringTestCase {
std::string name;
std::string json_string;
std::optional<std::string> user_string;
size_t depth_limit = 20;
size_t output_limit = 1000;
};
class ScannerValueToUserFacingStringTest
: public ::testing::TestWithParam<ValueToUserFacingStringTestCase> {};
// These tests assume that `Dict`s are iterated through in sorted order. If this
// ever changes, these tests should be updated to use gMock's
// `UnorderedElementsAre` matcher over the lines of the user-facing string.
INSTANTIATE_TEST_SUITE_P(
,
ScannerValueToUserFacingStringTest,
::testing::ValuesIn({
ValueToUserFacingStringTestCase{
.name = "Basic",
.json_string =
R"json({
"new_event": {
"title": "🌏",
"description": "formerly \"Geo Sync\"",
"dates": "20241014T160000/20241014T161500",
"location": "Wonderland",
}
})json",
.user_string =
R"user(new_event.dates: 20241014T160000/20241014T161500
new_event.description: formerly "Geo Sync"
new_event.location: Wonderland
new_event.title: 🌏
)user",
},
ValueToUserFacingStringTestCase{
.name = "Complex",
.json_string =
R"json({
"new_contact": {
"given_name": "André",
"family_name": "François",
"email": "afrancois@example.com",
"phone": "+61400000000",
"email_addresses": [
{
"value": "afrancois@example.com",
"type": "home",
},
{
"value": "afrancois@work.example.com",
"type": "work",
},
],
"phone_numbers": [
{
"value": "+61400000000",
"type": "mobile",
},
{
"value": "+61390000000",
"type": "home",
},
],
}
})json",
.user_string =
R"user(new_contact.email: afrancois@example.com
new_contact.email_addresses.0.type: home
new_contact.email_addresses.0.value: afrancois@example.com
new_contact.email_addresses.1.type: work
new_contact.email_addresses.1.value: afrancois@work.example.com
new_contact.family_name: François
new_contact.given_name: André
new_contact.phone: +61400000000
new_contact.phone_numbers.0.type: mobile
new_contact.phone_numbers.0.value: +61400000000
new_contact.phone_numbers.1.type: home
new_contact.phone_numbers.1.value: +61390000000
)user",
},
ValueToUserFacingStringTestCase{
.name = "DoesNotHtmlEscapeStrings",
.json_string =
R"json({
"copy_to_clipboard": {
"plain_text": "Hello",
"html_text": "\u003Cb>Hello\u003C/b>",
}
})json",
.user_string =
R"user(copy_to_clipboard.html_text: <b>Hello</b>
copy_to_clipboard.plain_text: Hello
)user",
},
ValueToUserFacingStringTestCase{
.name = "PlainFalse",
.json_string = "false",
.user_string = "false\n",
},
ValueToUserFacingStringTestCase{
.name = "PlainTrue",
.json_string = "true",
.user_string = "true\n",
},
ValueToUserFacingStringTestCase{
.name = "PlainNull",
.json_string = "null",
.user_string = "null\n",
},
ValueToUserFacingStringTestCase{
.name = "PlainNumber",
.json_string = "123",
.user_string = "123\n",
},
ValueToUserFacingStringTestCase{
.name = "PlainString",
.json_string = "\"string\"",
.user_string = "string\n",
},
ValueToUserFacingStringTestCase{
.name = "NestedDict",
.json_string =
R"json({
"a": {
"b": {
"c": 1,
},
"d": 2,
},
"e": 3,
})json",
.user_string = "a.b.c: 1\na.d: 2\ne: 3\n",
},
ValueToUserFacingStringTestCase{
.name = "NestedList",
.json_string =
R"json([
[
[1],
2,
],
3,
])json",
.user_string = "0.0.0: 1\n0.1: 2\n1: 3\n",
},
ValueToUserFacingStringTestCase{
.name = "DictInList",
.json_string =
R"json([
{
"a": 1,
},
{
"b": 2,
},
])json",
.user_string = "0.a: 1\n1.b: 2\n",
},
ValueToUserFacingStringTestCase{
.name = "ListInDict",
.json_string =
R"json({
"a": [1, 2],
"b": [3, 4],
})json",
.user_string = "a.0: 1\na.1: 2\nb.0: 3\nb.1: 4\n",
},
ValueToUserFacingStringTestCase{
.name = "HitsDepthLimitWithDictionary",
.json_string = R"json({"a":{"b":{"c": 1}}})json",
.user_string = "a.b.c: 1\n",
.depth_limit = 3,
},
ValueToUserFacingStringTestCase{
.name = "ExceedsDepthLimitWithDictionary",
.json_string = R"json({"a":{"b":{"c": 1}}})json",
.user_string = std::nullopt,
.depth_limit = 2,
},
ValueToUserFacingStringTestCase{
.name = "HitsDepthLimitWithList",
.json_string = R"json([[[1]]])json",
.user_string = "0.0.0: 1\n",
.depth_limit = 3,
},
ValueToUserFacingStringTestCase{
.name = "ExceedsDepthLimitWithList",
.json_string = R"json([[[1]]])json",
.user_string = std::nullopt,
.depth_limit = 2,
},
ValueToUserFacingStringTestCase{
.name = "HitsDepthLimitWithDictionaryAndList",
.json_string = R"json([[{"a": 1}]])json",
.user_string = "0.0.a: 1\n",
.depth_limit = 3,
},
ValueToUserFacingStringTestCase{
.name = "ExceedsDepthLimitWithDictionaryAndList",
.json_string = R"json([[{"a": 1}]])json",
.user_string = std::nullopt,
.depth_limit = 2,
},
ValueToUserFacingStringTestCase{
.name = "HitsOutputLimit",
.json_string = "123456789",
.user_string = "123456789\n",
.output_limit = 10,
},
ValueToUserFacingStringTestCase{
.name = "ExceedsOutputLimit",
.json_string = "123456789",
.user_string = std::nullopt,
.output_limit = 9,
},
}),
[](const ::testing::TestParamInfo<ValueToUserFacingStringTestCase>& info) {
return info.param.name;
});
TEST_P(ScannerValueToUserFacingStringTest,
ValueToUserFacingStringMatchesExpected) {
base::Value value = base::test::ParseJson(GetParam().json_string);
std::optional<std::string> user_string = ValueToUserFacingString(
value, GetParam().depth_limit, GetParam().output_limit);
EXPECT_EQ(GetParam().user_string, user_string);
}
TEST(ScannerFeedbackTest, ValueToUserFacingStringReturnsNulloptWithBinary) {
base::Value binary_value(base::as_byte_span("binaryvalue"));
std::optional<std::string> user_string = ValueToUserFacingString(
binary_value, /*depth_limit=*/20, /*output_limit=*/1000);
EXPECT_EQ(user_string, std::nullopt);
}
TEST(ScannerFeedbackTest,
ValueToUserFacingStringReturnsNulloptWithNestedBinary) {
base::Value::Dict nested_value;
nested_value.EnsureDict("a")->EnsureList("b")->Append(
base::Value(base::as_byte_span("binaryvalue")));
std::optional<std::string> user_string = ValueToUserFacingString(
nested_value, /*depth_limit=*/20, /*output_limit=*/1000);
EXPECT_EQ(user_string, std::nullopt);
}
} // namespace
} // namespace ash