// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/file_suggest/item_suggest_cache.h"
#include <vector>
#include "base/functional/callback_helpers.h"
#include "base/json/json_reader.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "chrome/browser/signin/chrome_signin_client_factory.h"
#include "chrome/browser/signin/chrome_signin_client_test_util.h"
#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/drive/drive_pref_names.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "content/public/test/browser_task_environment.h"
#include "net/http/http_util.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.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/public/mojom/url_response_head.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace ash::test {
namespace {
using base::test::ScopedFeatureList;
constexpr char kEmail[] = "[email protected]";
constexpr char16_t kEmail16[] = u"[email protected]";
constexpr char kRequestUrl[] =
"https://appsitemsuggest-pa.googleapis.com/v1/items";
constexpr char kValidJsonResponse[] = R"(
{
"item": [
{
"itemId": "item id 1",
"displayText": "display text 1"
},
{
"itemId": "item id 2",
"displayText": "display text 2",
"justification": {
"unstructuredJustificationDescription": {
"textSegment": [
{
"text": "prediction reason 2"
}
]
}
}
},
{
"itemId": "item id 3",
"displayText": "display text 3",
"justification": {
"unstructuredJustificationDescription": {
"textSegment": [
{
"text": "prediction reason 3"
}
]
}
}
}
],
"suggestionSessionId": "suggestion id 1"
})";
constexpr char kStatusHistogramName[] = "Apps.AppList.ItemSuggestCache.Status";
constexpr char kResponseSizeHistogramName[] =
"Apps.AppList.ItemSuggestCache.ResponseSize";
} // namespace
class ItemSuggestCacheTest : public testing::Test {
protected:
ItemSuggestCacheTest() = default;
~ItemSuggestCacheTest() override = default;
base::Value Parse(const std::string& json) {
return base::JSONReader::Read(json).value();
}
void ResultMatches(const ItemSuggestCache::Result& actual,
const std::string& id,
const std::string& title,
const std::optional<std::string>& prediction_reason) {
EXPECT_EQ(actual.id, id);
EXPECT_EQ(actual.title, title);
EXPECT_EQ(actual.prediction_reason, prediction_reason);
}
void ResultsMatch(
const std::optional<ItemSuggestCache::Results>& actual,
const std::string& suggestion_id,
const std::vector<
std::tuple<std::string, std::string, std::optional<std::string>>>&
results) {
EXPECT_TRUE(actual.has_value());
EXPECT_EQ(actual->suggestion_id, suggestion_id);
ASSERT_EQ(actual->results.size(), results.size());
for (size_t i = 0; i < results.size(); ++i) {
ResultMatches(actual->results[i], std::get<0>(results[i]),
std::get<1>(results[i]), std::get<2>(results[i]));
}
}
void SetUp() override {
profile_manager_ = std::make_unique<TestingProfileManager>(
TestingBrowserProcess::GetGlobal());
ASSERT_TRUE(profile_manager_->SetUp());
profile_ = profile_manager_->CreateTestingProfile(
kEmail, /*prefs=*/{}, kEmail16,
/*avatar_id=*/0,
IdentityTestEnvironmentProfileAdaptor::
GetIdentityTestEnvironmentFactoriesWithAppendedFactories(
{TestingProfile::TestingFactory{
ChromeSigninClientFactory::GetInstance(),
base::BindRepeating(&BuildChromeSigninClientWithURLLoader,
&url_loader_factory_)}}));
identity_test_env_adaptor_ =
std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile_);
identity_test_env_ = identity_test_env_adaptor_->identity_test_env();
identity_test_env_->SetTestURLLoaderFactory(&url_loader_factory_);
shared_url_loader_factory_ =
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&url_loader_factory_);
}
content::BrowserTaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
raw_ptr<signin::IdentityTestEnvironment, DanglingUntriaged>
identity_test_env_;
std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
identity_test_env_adaptor_;
std::unique_ptr<TestingProfileManager> profile_manager_;
raw_ptr<TestingProfile> profile_;
network::TestURLLoaderFactory url_loader_factory_;
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory_;
ScopedFeatureList scoped_feature_list_;
const raw_ref<const base::Feature> feature_{kLauncherItemSuggest};
const base::HistogramTester histogram_tester_;
};
TEST_F(ItemSuggestCacheTest, ConvertJsonSuccess) {
const base::Value full = Parse(kValidJsonResponse);
ResultsMatch(ItemSuggestCache::ConvertJsonForTest(&full), "suggestion id 1",
{{"item id 1", "display text 1", std::nullopt},
{"item id 2", "display text 2", "prediction reason 2"},
{"item id 3", "display text 3", "prediction reason 3"}});
const base::Value empty_items = Parse(R"(
{
"item": [],
"suggestionSessionId": "the suggestion id"
})");
ResultsMatch(ItemSuggestCache::ConvertJsonForTest(&empty_items),
"the suggestion id", {});
const base::Value no_items = Parse(R"(
{
"suggestionSessionId": "the suggestion id"
})");
ResultsMatch(ItemSuggestCache::ConvertJsonForTest(&no_items),
"the suggestion id", {});
}
TEST_F(ItemSuggestCacheTest, ConvertJsonFailure) {
const base::Value no_display_text = Parse(R"(
{
"item": [
{
"itemId": "item id 1"
}
],
"suggestionSessionId": "the suggestion id"
})");
EXPECT_FALSE(
ItemSuggestCache::ConvertJsonForTest(&no_display_text).has_value());
const base::Value no_item_id = Parse(R"(
{
"item": [
{
"displayText": "display text 1"
}
],
"suggestionSessionId": "the suggestion id"
})");
EXPECT_FALSE(ItemSuggestCache::ConvertJsonForTest(&no_item_id).has_value());
const base::Value no_session_id = Parse(R"(
{
"item": [
{
"itemId": "item id 1",
"displayText": "display text 2"
}
]
})");
EXPECT_FALSE(
ItemSuggestCache::ConvertJsonForTest(&no_session_id).has_value());
}
TEST_F(ItemSuggestCacheTest, UpdateCacheDisabledByExperiment) {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
*feature_, {{"enabled", "false"}});
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(
kStatusHistogramName, ItemSuggestCache::Status::kDisabledByExperiment, 1);
}
TEST_F(ItemSuggestCacheTest, UpdateCacheDisabledByPolicy) {
profile_->GetPrefs()->SetBoolean(drive::prefs::kDisableDrive, true);
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(
kStatusHistogramName, ItemSuggestCache::Status::kDisabledByPolicy, 1);
}
TEST_F(ItemSuggestCacheTest, UpdateCacheServerUrlIsNotHttps) {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
*feature_,
{{"server_url", "http://appsitemsuggest-pa.googleapis.com/v1/items"}});
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(
kStatusHistogramName, ItemSuggestCache::Status::kInvalidServerUrl, 1);
}
TEST_F(ItemSuggestCacheTest, UpdateCacheServerUrlIsNotGoogleDomain) {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
*feature_, {{"server_url", "https://foo.com"}});
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(
kStatusHistogramName, ItemSuggestCache::Status::kInvalidServerUrl, 1);
}
TEST_F(ItemSuggestCacheTest, UpdateCacheServerNoAuthToken) {
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(
kStatusHistogramName, ItemSuggestCache::Status::kGoogleAuthError, 1);
}
TEST_F(ItemSuggestCacheTest, UpdateCacheInsufficientResourcesError) {
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
identity_test_env_->MakePrimaryAccountAvailable(kEmail,
signin::ConsentLevel::kSync);
identity_test_env_->SetAutomaticIssueOfAccessTokens(true);
auto head = network::CreateURLResponseHead(net::HTTP_OK);
network::URLLoaderCompletionStatus status(net::ERR_INSUFFICIENT_RESOURCES);
url_loader_factory_.AddResponse(GURL(kRequestUrl), std::move(head), "content",
status);
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(
kStatusHistogramName, ItemSuggestCache::Status::kResponseTooLarge, 1);
}
TEST_F(ItemSuggestCacheTest, UpdateCacheNetError) {
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
identity_test_env_->MakePrimaryAccountAvailable(kEmail,
signin::ConsentLevel::kSync);
identity_test_env_->SetAutomaticIssueOfAccessTokens(true);
auto head = network::CreateURLResponseHead(net::HTTP_OK);
network::URLLoaderCompletionStatus status(net::ERR_FAILED);
url_loader_factory_.AddResponse(GURL(kRequestUrl), std::move(head), "content",
status);
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(kStatusHistogramName,
ItemSuggestCache::Status::kNetError, 1);
}
TEST_F(ItemSuggestCacheTest, UpdateCache5kkError) {
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
identity_test_env_->MakePrimaryAccountAvailable(kEmail,
signin::ConsentLevel::kSync);
identity_test_env_->SetAutomaticIssueOfAccessTokens(true);
auto head = network::mojom::URLResponseHead::New();
std::string headers("HTTP/1.1 500 Owiee\nContent-type: application/json\n\n");
head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(headers));
url_loader_factory_.AddResponse(GURL(kRequestUrl), std::move(head),
/* content= */ "",
network::URLLoaderCompletionStatus(net::OK));
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(kStatusHistogramName,
ItemSuggestCache::Status::k5xxStatus, 1);
}
TEST_F(ItemSuggestCacheTest, UpdateCache4kkError) {
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
identity_test_env_->MakePrimaryAccountAvailable(kEmail,
signin::ConsentLevel::kSync);
identity_test_env_->SetAutomaticIssueOfAccessTokens(true);
auto head = network::mojom::URLResponseHead::New();
std::string headers("HTTP/1.1 400 Owiee\nContent-type: application/json\n\n");
head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(headers));
url_loader_factory_.AddResponse(GURL(kRequestUrl), std::move(head),
/* content= */ "",
network::URLLoaderCompletionStatus(net::OK));
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(kStatusHistogramName,
ItemSuggestCache::Status::k4xxStatus, 1);
}
TEST_F(ItemSuggestCacheTest, UpdateCache3kkError) {
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
identity_test_env_->MakePrimaryAccountAvailable(kEmail,
signin::ConsentLevel::kSync);
identity_test_env_->SetAutomaticIssueOfAccessTokens(true);
auto head = network::mojom::URLResponseHead::New();
std::string headers("HTTP/1.1 300 Owiee\nContent-type: application/json\n\n");
head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(headers));
url_loader_factory_.AddResponse(GURL(kRequestUrl), std::move(head),
/* content= */ "",
network::URLLoaderCompletionStatus(net::OK));
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(kStatusHistogramName,
ItemSuggestCache::Status::k3xxStatus, 1);
}
TEST_F(ItemSuggestCacheTest, UpdateCacheEmptyResponse) {
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
identity_test_env_->MakePrimaryAccountAvailable(kEmail,
signin::ConsentLevel::kSync);
identity_test_env_->SetAutomaticIssueOfAccessTokens(true);
url_loader_factory_.AddResponse(kRequestUrl,
/* content= */ "", net::HTTP_OK);
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(
kStatusHistogramName, ItemSuggestCache::Status::kEmptyResponse, 1);
}
TEST_F(ItemSuggestCacheTest, UpdateCacheInvalidResponse) {
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
identity_test_env_->MakePrimaryAccountAvailable(kEmail,
signin::ConsentLevel::kSync);
identity_test_env_->SetAutomaticIssueOfAccessTokens(true);
url_loader_factory_.AddResponse(kRequestUrl, "invalid = json", net::HTTP_OK);
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(kResponseSizeHistogramName,
/* sample= */ 14,
/* expected_bucket_count= */ 1);
histogram_tester_.ExpectUniqueSample(
kStatusHistogramName, ItemSuggestCache::Status::kJsonParseFailure, 1);
}
TEST_F(ItemSuggestCacheTest, UpdateCacheConversionFailure) {
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
identity_test_env_->MakePrimaryAccountAvailable(kEmail,
signin::ConsentLevel::kSync);
identity_test_env_->SetAutomaticIssueOfAccessTokens(true);
url_loader_factory_.AddResponse(kRequestUrl,
R"(
{
"a": ""
}
)",
net::HTTP_OK);
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(kResponseSizeHistogramName, 45, 1);
histogram_tester_.ExpectUniqueSample(
kStatusHistogramName, ItemSuggestCache::Status::kJsonConversionFailure,
1);
}
TEST_F(ItemSuggestCacheTest, UpdateCacheConversionEmptyResults) {
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
identity_test_env_->MakePrimaryAccountAvailable(kEmail,
signin::ConsentLevel::kSync);
identity_test_env_->SetAutomaticIssueOfAccessTokens(true);
url_loader_factory_.AddResponse(kRequestUrl,
R"(
{
"item": [],
"suggestionSessionId": "the suggestion id"
})",
net::HTTP_OK);
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(kResponseSizeHistogramName,
/* sample= */ 79,
/* expected_bucket_count= */ 1);
histogram_tester_.ExpectUniqueSample(
kStatusHistogramName, ItemSuggestCache::Status::kNoResultsInResponse, 1);
}
TEST_F(ItemSuggestCacheTest, UpdateCacheSavesResults) {
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
identity_test_env_->MakePrimaryAccountAvailable(kEmail,
signin::ConsentLevel::kSync);
identity_test_env_->SetAutomaticIssueOfAccessTokens(true);
url_loader_factory_.AddResponse(kRequestUrl, kValidJsonResponse,
net::HTTP_OK);
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
histogram_tester_.ExpectUniqueSample(kResponseSizeHistogramName,
/* sample= */ 716,
/* expected_bucket_count= */ 1);
ResultsMatch(item_suggest_cache->GetResults(), "suggestion id 1",
{{"item id 1", "display text 1", std::nullopt},
{"item id 2", "display text 2", "prediction reason 2"},
{"item id 3", "display text 3", "prediction reason 3"}});
histogram_tester_.ExpectUniqueSample(kStatusHistogramName,
ItemSuggestCache::Status::kOk, 1);
}
TEST_F(ItemSuggestCacheTest, UpdateCacheSmallTimeBetweenUpdates) {
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en", profile_,
shared_url_loader_factory_);
identity_test_env_->MakePrimaryAccountAvailable(kEmail,
signin::ConsentLevel::kSync);
identity_test_env_->SetAutomaticIssueOfAccessTokens(true);
url_loader_factory_.AddResponse(kRequestUrl,
R"(
{
"item": [
{
"itemId": "item id 1",
"displayText": "display text 1"
}
],
"suggestionSessionId": "suggestion id 1"
})",
net::HTTP_OK);
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
ResultsMatch(item_suggest_cache->GetResults(), "suggestion id 1",
{{"item id 1", "display text 1", std::nullopt}});
task_environment_.AdvanceClock(base::Minutes(2));
url_loader_factory_.AddResponse(kRequestUrl,
R"(
{
"item": [
{
"itemId": "item id 2",
"displayText": "display text 2"
}
],
"suggestionSessionId": "suggestion id 2"
})",
net::HTTP_OK);
item_suggest_cache->MaybeUpdateCache();
task_environment_.RunUntilIdle();
// The first set of results are in the cache since the second update occurred
// before the minimum time between updates.
ResultsMatch(item_suggest_cache->GetResults(), "suggestion id 1",
{{"item id 1", "display text 1", std::nullopt}});
}
TEST_F(ItemSuggestCacheTest, RequestIncludesLocale) {
std::unique_ptr<ItemSuggestCache> item_suggest_cache =
std::make_unique<ItemSuggestCache>("en-AU", profile_,
shared_url_loader_factory_);
identity_test_env_->MakePrimaryAccountAvailable(kEmail,
signin::ConsentLevel::kSync);
identity_test_env_->SetAutomaticIssueOfAccessTokens(true);
item_suggest_cache->MaybeUpdateCache();
ASSERT_EQ(1, url_loader_factory_.NumPending());
std::string request_body(url_loader_factory_.pending_requests()
->at(0)
.request.request_body->elements()
->at(0)
.As<network::DataElementBytes>()
.AsStringPiece());
auto body_value = Parse(request_body);
EXPECT_EQ("en-AU", *body_value.GetDict().FindStringByDottedPath(
"client_info.language_code"));
}
} // namespace ash::test