blob: 48380dee5bac59ab59044a2b8a1b1fec04da9b03 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/ntp_snippets/ntp_snippets_fetcher.h"
#include <map>
#include <utility>
#include "base/json/json_reader.h"
#include "base/memory/ptr_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/histogram_tester.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/ntp_snippets/category_factory.h"
#include "components/ntp_snippets/ntp_snippet.h"
#include "components/ntp_snippets/ntp_snippets_constants.h"
#include "components/prefs/testing_pref_service.h"
#include "components/signin/core/browser/account_tracker_service.h"
#include "components/signin/core/browser/fake_profile_oauth2_token_service.h"
#include "components/signin/core/browser/fake_signin_manager.h"
#include "components/signin/core/browser/test_signin_client.h"
#include "components/variations/entropy_provider.h"
#include "components/variations/variations_associated_data.h"
#include "google_apis/google_api_keys.h"
#include "net/url_request/test_url_fetcher_factory.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace ntp_snippets {
namespace {
using testing::_;
using testing::ElementsAre;
using testing::Eq;
using testing::IsEmpty;
using testing::IsNull;
using testing::Not;
using testing::NotNull;
using testing::PrintToString;
using testing::SizeIs;
using testing::StartsWith;
using testing::WithArg;
const char kTestChromeReaderUrlFormat[] =
"https://chromereader-pa.googleapis.com/v1/fetch?key=%s";
const char kTestChromeContentSuggestionsUrlFormat[] =
"https://chromecontentsuggestions-pa.googleapis.com/v1/suggestions/"
"fetch?key=%s";
// Artificial time delay for JSON parsing.
const int64_t kTestJsonParsingLatencyMs = 20;
ACTION_P(MovePointeeTo, ptr) {
*ptr = std::move(*arg0);
}
MATCHER(HasValue, "") {
return static_cast<bool>(*arg);
}
MATCHER(IsEmptyArticleList, "is an empty list of articles") {
NTPSnippetsFetcher::OptionalSnippets& snippets = *arg;
return snippets && snippets->size() == 1 &&
snippets->begin()->snippets.empty();
}
MATCHER_P(IsSingleArticle, url, "is a list with the single article %(url)s") {
NTPSnippetsFetcher::OptionalSnippets& snippets = *arg;
return snippets && snippets->size() == 1 &&
snippets->begin()->snippets.size() == 1 &&
snippets->begin()->snippets[0]->best_source().url.spec() == url;
}
MATCHER_P(EqualsJSON, json, "equals JSON") {
std::unique_ptr<base::Value> expected = base::JSONReader::Read(json);
if (!expected) {
*result_listener << "INTERNAL ERROR: couldn't parse expected JSON";
return false;
}
std::string err_msg;
int err_line, err_col;
std::unique_ptr<base::Value> actual = base::JSONReader::ReadAndReturnError(
arg, base::JSON_PARSE_RFC, nullptr, &err_msg, &err_line, &err_col);
if (!actual) {
*result_listener << "input:" << err_line << ":" << err_col << ": "
<< "parse error: " << err_msg;
return false;
}
return base::Value::Equals(actual.get(), expected.get());
}
class MockSnippetsAvailableCallback {
public:
// Workaround for gMock's lack of support for movable arguments.
void WrappedRun(NTPSnippetsFetcher::OptionalSnippets snippets) {
Run(&snippets);
}
MOCK_METHOD1(Run, void(NTPSnippetsFetcher::OptionalSnippets* snippets));
};
// Factory for FakeURLFetcher objects that always generate errors.
class FailingFakeURLFetcherFactory : public net::URLFetcherFactory {
public:
std::unique_ptr<net::URLFetcher> CreateURLFetcher(
int id, const GURL& url, net::URLFetcher::RequestType request_type,
net::URLFetcherDelegate* d) override {
return base::MakeUnique<net::FakeURLFetcher>(
url, d, /*response_data=*/std::string(), net::HTTP_NOT_FOUND,
net::URLRequestStatus::FAILED);
}
};
void ParseJson(
const std::string& json,
const ntp_snippets::NTPSnippetsFetcher::SuccessCallback& success_callback,
const ntp_snippets::NTPSnippetsFetcher::ErrorCallback& error_callback) {
base::JSONReader json_reader;
std::unique_ptr<base::Value> value = json_reader.ReadToValue(json);
if (value)
success_callback.Run(std::move(value));
else
error_callback.Run(json_reader.GetErrorMessage());
}
void ParseJsonDelayed(
const std::string& json,
const ntp_snippets::NTPSnippetsFetcher::SuccessCallback& success_callback,
const ntp_snippets::NTPSnippetsFetcher::ErrorCallback& error_callback) {
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, base::Bind(&ParseJson, json, success_callback, error_callback),
base::TimeDelta::FromMilliseconds(kTestJsonParsingLatencyMs));
}
GURL GetFetcherUrl(const char* url_format) {
return GURL(base::StringPrintf(url_format, google_apis::GetAPIKey().c_str()));
}
} // namespace
class NTPSnippetsFetcherTest : public testing::Test {
public:
NTPSnippetsFetcherTest()
: NTPSnippetsFetcherTest(GetFetcherUrl(kTestChromeReaderUrlFormat),
std::map<std::string, std::string>()) {}
NTPSnippetsFetcherTest(const GURL& gurl,
const std::map<std::string, std::string>& params)
: params_manager_(ntp_snippets::kStudyName, params),
mock_task_runner_(new base::TestMockTimeTaskRunner()),
mock_task_runner_handle_(mock_task_runner_),
signin_client_(new TestSigninClient(nullptr)),
account_tracker_(new AccountTrackerService()),
fake_signin_manager_(new FakeSigninManagerBase(signin_client_.get(),
account_tracker_.get())),
fake_token_service_(new FakeProfileOAuth2TokenService()),
pref_service_(new TestingPrefServiceSimple()),
test_lang_("en-US"),
test_url_(gurl) {
RequestThrottler::RegisterProfilePrefs(pref_service_->registry());
snippets_fetcher_ = base::MakeUnique<NTPSnippetsFetcher>(
fake_signin_manager_.get(), fake_token_service_.get(),
scoped_refptr<net::TestURLRequestContextGetter>(
new net::TestURLRequestContextGetter(mock_task_runner_.get())),
pref_service_.get(), &category_factory_, base::Bind(&ParseJsonDelayed),
/*is_stable_channel=*/true);
snippets_fetcher_->SetCallback(
base::Bind(&MockSnippetsAvailableCallback::WrappedRun,
base::Unretained(&mock_callback_)));
snippets_fetcher_->SetTickClockForTesting(
mock_task_runner_->GetMockTickClock());
test_hosts_.insert("www.somehost.com");
test_excluded_.insert("1234567890");
// Increase initial time such that ticks are non-zero.
mock_task_runner_->FastForwardBy(base::TimeDelta::FromMilliseconds(1234));
}
NTPSnippetsFetcher& snippets_fetcher() { return *snippets_fetcher_; }
MockSnippetsAvailableCallback& mock_callback() { return mock_callback_; }
void FastForwardUntilNoTasksRemain() {
mock_task_runner_->FastForwardUntilNoTasksRemain();
}
const std::string& test_lang() const { return test_lang_; }
const GURL& test_url() { return test_url_; }
const std::set<std::string>& test_hosts() const { return test_hosts_; }
const std::set<std::string>& test_excluded() const { return test_excluded_; }
base::HistogramTester& histogram_tester() { return histogram_tester_; }
void InitFakeURLFetcherFactory() {
if (fake_url_fetcher_factory_)
return;
// Instantiation of factory automatically sets itself as URLFetcher's
// factory.
fake_url_fetcher_factory_.reset(new net::FakeURLFetcherFactory(
/*default_factory=*/&failing_url_fetcher_factory_));
}
void SetFakeResponse(const std::string& response_data,
net::HttpStatusCode response_code,
net::URLRequestStatus::Status status) {
InitFakeURLFetcherFactory();
fake_url_fetcher_factory_->SetFakeResponse(test_url_, response_data,
response_code, status);
}
private:
variations::testing::VariationParamsManager params_manager_;
scoped_refptr<base::TestMockTimeTaskRunner> mock_task_runner_;
base::ThreadTaskRunnerHandle mock_task_runner_handle_;
FailingFakeURLFetcherFactory failing_url_fetcher_factory_;
// Initialized lazily in SetFakeResponse().
std::unique_ptr<net::FakeURLFetcherFactory> fake_url_fetcher_factory_;
std::unique_ptr<TestSigninClient> signin_client_;
std::unique_ptr<AccountTrackerService> account_tracker_;
std::unique_ptr<SigninManagerBase> fake_signin_manager_;
std::unique_ptr<OAuth2TokenService> fake_token_service_;
std::unique_ptr<NTPSnippetsFetcher> snippets_fetcher_;
std::unique_ptr<TestingPrefServiceSimple> pref_service_;
CategoryFactory category_factory_;
MockSnippetsAvailableCallback mock_callback_;
const std::string test_lang_;
const GURL test_url_;
std::set<std::string> test_hosts_;
std::set<std::string> test_excluded_;
base::HistogramTester histogram_tester_;
DISALLOW_COPY_AND_ASSIGN(NTPSnippetsFetcherTest);
};
class NTPSnippetsContentSuggestionsFetcherTest : public NTPSnippetsFetcherTest {
public:
NTPSnippetsContentSuggestionsFetcherTest()
: NTPSnippetsFetcherTest(
GetFetcherUrl(kTestChromeContentSuggestionsUrlFormat),
{{"content_suggestions_backend", kContentSuggestionsServer}}) {}
};
class NTPSnippetsFetcherHostRestrictedTest : public NTPSnippetsFetcherTest {
public:
NTPSnippetsFetcherHostRestrictedTest()
: NTPSnippetsFetcherTest(GetFetcherUrl(kTestChromeReaderUrlFormat),
{{"fetching_host_restrict", "on"}}) {}
};
TEST_F(NTPSnippetsFetcherTest, BuildRequestAuthenticated) {
NTPSnippetsFetcher::RequestParams params;
params.obfuscated_gaia_id = "0BFUSGAIA";
params.only_return_personalized_results = true;
params.user_locale = "en";
params.host_restricts = {"chromium.org"};
params.excluded_ids = {"1234567890"};
params.count_to_fetch = 25;
params.interactive_request = false;
params.fetch_api = NTPSnippetsFetcher::CHROME_READER_API;
EXPECT_THAT(params.BuildRequest(),
EqualsJSON("{"
" \"response_detail_level\": \"STANDARD\","
" \"obfuscated_gaia_id\": \"0BFUSGAIA\","
" \"user_locale\": \"en\","
" \"advanced_options\": {"
" \"local_scoring_params\": {"
" \"content_params\": {"
" \"only_return_personalized_results\": true"
" },"
" \"content_restricts\": ["
" {"
" \"type\": \"METADATA\","
" \"value\": \"TITLE\""
" },"
" {"
" \"type\": \"METADATA\","
" \"value\": \"SNIPPET\""
" },"
" {"
" \"type\": \"METADATA\","
" \"value\": \"THUMBNAIL\""
" }"
" ],"
" \"content_selectors\": ["
" {"
" \"type\": \"HOST_RESTRICT\","
" \"value\": \"chromium.org\""
" }"
" ]"
" },"
" \"global_scoring_params\": {"
" \"num_to_return\": 25,"
" \"sort_type\": 1"
" }"
" }"
"}"));
params.fetch_api = NTPSnippetsFetcher::CHROME_CONTENT_SUGGESTIONS_API;
EXPECT_THAT(params.BuildRequest(),
EqualsJSON("{"
" \"uiLanguage\": \"en\","
" \"priority\": \"BACKGROUND_PREFETCH\","
" \"regularlyVisitedHostNames\": ["
" \"chromium.org\""
" ],"
" \"excludedSuggestionIds\": ["
" \"1234567890\""
" ]"
"}"));
}
TEST_F(NTPSnippetsFetcherTest, BuildRequestUnauthenticated) {
NTPSnippetsFetcher::RequestParams params;
params.only_return_personalized_results = false;
params.host_restricts = {};
params.count_to_fetch = 10;
params.excluded_ids = {};
params.interactive_request = true;
params.fetch_api = NTPSnippetsFetcher::CHROME_READER_API;
EXPECT_THAT(params.BuildRequest(),
EqualsJSON("{"
" \"response_detail_level\": \"STANDARD\","
" \"advanced_options\": {"
" \"local_scoring_params\": {"
" \"content_params\": {"
" \"only_return_personalized_results\": false"
" },"
" \"content_restricts\": ["
" {"
" \"type\": \"METADATA\","
" \"value\": \"TITLE\""
" },"
" {"
" \"type\": \"METADATA\","
" \"value\": \"SNIPPET\""
" },"
" {"
" \"type\": \"METADATA\","
" \"value\": \"THUMBNAIL\""
" }"
" ],"
" \"content_selectors\": []"
" },"
" \"global_scoring_params\": {"
" \"num_to_return\": 10,"
" \"sort_type\": 1"
" }"
" }"
"}"));
params.fetch_api = NTPSnippetsFetcher::CHROME_CONTENT_SUGGESTIONS_API;
EXPECT_THAT(params.BuildRequest(),
EqualsJSON("{"
" \"regularlyVisitedHostNames\": [],"
" \"priority\": \"USER_ACTION\","
" \"excludedSuggestionIds\": []"
"}"));
}
TEST_F(NTPSnippetsFetcherTest, BuildRequestExcludedIds) {
NTPSnippetsFetcher::RequestParams params;
params.only_return_personalized_results = false;
params.host_restricts = {};
params.count_to_fetch = 10;
params.interactive_request = false;
for (int i = 0; i < 200; ++i) {
params.excluded_ids.insert(base::StringPrintf("%03d", i));
}
params.fetch_api = NTPSnippetsFetcher::CHROME_CONTENT_SUGGESTIONS_API;
EXPECT_THAT(params.BuildRequest(),
EqualsJSON("{"
" \"regularlyVisitedHostNames\": [],"
" \"priority\": \"BACKGROUND_PREFETCH\","
" \"excludedSuggestionIds\": ["
" \"000\", \"001\", \"002\", \"003\", \"004\","
" \"005\", \"006\", \"007\", \"008\", \"009\","
" \"010\", \"011\", \"012\", \"013\", \"014\","
" \"015\", \"016\", \"017\", \"018\", \"019\","
" \"020\", \"021\", \"022\", \"023\", \"024\","
" \"025\", \"026\", \"027\", \"028\", \"029\","
" \"030\", \"031\", \"032\", \"033\", \"034\","
" \"035\", \"036\", \"037\", \"038\", \"039\","
" \"040\", \"041\", \"042\", \"043\", \"044\","
" \"045\", \"046\", \"047\", \"048\", \"049\","
" \"050\", \"051\", \"052\", \"053\", \"054\","
" \"055\", \"056\", \"057\", \"058\", \"059\","
" \"060\", \"061\", \"062\", \"063\", \"064\","
" \"065\", \"066\", \"067\", \"068\", \"069\","
" \"070\", \"071\", \"072\", \"073\", \"074\","
" \"075\", \"076\", \"077\", \"078\", \"079\","
" \"080\", \"081\", \"082\", \"083\", \"084\","
" \"085\", \"086\", \"087\", \"088\", \"089\","
" \"090\", \"091\", \"092\", \"093\", \"094\","
" \"095\", \"096\", \"097\", \"098\", \"099\""
// Truncated to 100 entries. Currently, they happen to
// be those lexically first.
" ]"
"}"));
}
TEST_F(NTPSnippetsFetcherTest, ShouldNotFetchOnCreation) {
// The lack of registered baked in responses would cause any fetch to fail.
FastForwardUntilNoTasksRemain();
EXPECT_THAT(histogram_tester().GetAllSamples(
"NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
IsEmpty());
EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
IsEmpty());
EXPECT_THAT(snippets_fetcher().last_status(), IsEmpty());
}
TEST_F(NTPSnippetsFetcherTest, ShouldFetchSuccessfully) {
const std::string kJsonStr =
"{\"recos\": [{"
" \"contentInfo\": {"
" \"url\" : \"http://localhost/foobar\","
" \"sourceCorpusInfo\" : [{"
" \"ampUrl\" : \"http://localhost/amp\","
" \"corpusId\" : \"http://localhost/foobar\","
" \"publisherData\": { \"sourceName\" : \"Foo News\" }"
" }]"
" }"
"}]}";
SetFakeResponse(/*data=*/kJsonStr, net::HTTP_OK,
net::URLRequestStatus::SUCCESS);
EXPECT_CALL(mock_callback(), Run(IsSingleArticle("http://localhost/foobar")));
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
test_excluded(),
/*count=*/1,
/*interactive_request=*/true);
FastForwardUntilNoTasksRemain();
EXPECT_THAT(snippets_fetcher().last_status(), Eq("OK"));
EXPECT_THAT(snippets_fetcher().last_json(), Eq(kJsonStr));
EXPECT_THAT(histogram_tester().GetAllSamples(
"NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
ElementsAre(base::Bucket(/*min=*/kTestJsonParsingLatencyMs,
/*count=*/1)));
}
TEST_F(NTPSnippetsContentSuggestionsFetcherTest, ShouldFetchSuccessfully) {
const std::string kJsonStr =
"{\"categories\" : [{"
" \"id\": 1,"
" \"localizedTitle\": \"Articles for You\","
" \"suggestions\" : [{"
" \"ids\" : [\"http://localhost/foobar\"],"
" \"title\" : \"Foo Barred from Baz\","
" \"snippet\" : \"...\","
" \"fullPageUrl\" : \"http://localhost/foobar\","
" \"creationTime\" : \"2016-06-30T11:01:37.000Z\","
" \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
" \"attribution\" : \"Foo News\","
" \"imageUrl\" : \"http://localhost/foobar.jpg\","
" \"ampUrl\" : \"http://localhost/amp\","
" \"faviconUrl\" : \"http://localhost/favicon.ico\" "
" }]"
"}]}";
SetFakeResponse(/*data=*/kJsonStr, net::HTTP_OK,
net::URLRequestStatus::SUCCESS);
EXPECT_CALL(mock_callback(), Run(IsSingleArticle("http://localhost/foobar")));
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
test_excluded(),
/*count=*/1,
/*interactive_request=*/true);
FastForwardUntilNoTasksRemain();
EXPECT_THAT(snippets_fetcher().last_status(), Eq("OK"));
EXPECT_THAT(snippets_fetcher().last_json(), Eq(kJsonStr));
EXPECT_THAT(histogram_tester().GetAllSamples(
"NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
ElementsAre(base::Bucket(/*min=*/kTestJsonParsingLatencyMs,
/*count=*/1)));
}
TEST_F(NTPSnippetsContentSuggestionsFetcherTest, EmptyCategoryIsOK) {
const std::string kJsonStr =
"{\"categories\" : [{"
" \"id\": 1,"
" \"localizedTitle\": \"Articles for You\""
"}]}";
SetFakeResponse(/*data=*/kJsonStr, net::HTTP_OK,
net::URLRequestStatus::SUCCESS);
EXPECT_CALL(mock_callback(), Run(IsEmptyArticleList()));
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
test_excluded(),
/*count=*/1,
/*interactive_request=*/true);
FastForwardUntilNoTasksRemain();
EXPECT_THAT(snippets_fetcher().last_status(), Eq("OK"));
EXPECT_THAT(snippets_fetcher().last_json(), Eq(kJsonStr));
EXPECT_THAT(histogram_tester().GetAllSamples(
"NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
ElementsAre(base::Bucket(/*min=*/kTestJsonParsingLatencyMs,
/*count=*/1)));
}
TEST_F(NTPSnippetsContentSuggestionsFetcherTest, ServerCategories) {
const std::string kJsonStr =
"{\"categories\" : [{"
" \"id\": 1,"
" \"localizedTitle\": \"Articles for You\","
" \"suggestions\" : [{"
" \"ids\" : [\"http://localhost/foobar\"],"
" \"title\" : \"Foo Barred from Baz\","
" \"snippet\" : \"...\","
" \"fullPageUrl\" : \"http://localhost/foobar\","
" \"creationTime\" : \"2016-06-30T11:01:37.000Z\","
" \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
" \"attribution\" : \"Foo News\","
" \"imageUrl\" : \"http://localhost/foobar.jpg\","
" \"ampUrl\" : \"http://localhost/amp\","
" \"faviconUrl\" : \"http://localhost/favicon.ico\" "
" }]"
"}, {"
" \"id\": 2,"
" \"localizedTitle\": \"Articles for Me\","
" \"suggestions\" : [{"
" \"ids\" : [\"http://localhost/foo2\"],"
" \"title\" : \"Foo Barred from Baz\","
" \"snippet\" : \"...\","
" \"fullPageUrl\" : \"http://localhost/foo2\","
" \"creationTime\" : \"2016-06-30T11:01:37.000Z\","
" \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
" \"attribution\" : \"Foo News\","
" \"imageUrl\" : \"http://localhost/foo2.jpg\","
" \"ampUrl\" : \"http://localhost/amp\","
" \"faviconUrl\" : \"http://localhost/favicon.ico\" "
" }]"
"}]}";
SetFakeResponse(/*data=*/kJsonStr, net::HTTP_OK,
net::URLRequestStatus::SUCCESS);
NTPSnippetsFetcher::OptionalSnippets snippets;
EXPECT_CALL(mock_callback(), Run(_))
.WillOnce(WithArg<0>(MovePointeeTo(&snippets)));
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
test_excluded(),
/*count=*/1,
/*force_request=*/true);
FastForwardUntilNoTasksRemain();
ASSERT_TRUE(snippets);
ASSERT_THAT(snippets->size(), Eq(2u));
for (const auto& category : *snippets) {
const auto& articles = category.snippets;
switch (category.category.id()) {
case static_cast<int>(KnownCategories::ARTICLES):
ASSERT_THAT(articles.size(), Eq(1u));
EXPECT_THAT(articles[0]->best_source().url.spec(),
Eq("http://localhost/foobar"));
break;
case static_cast<int>(KnownCategories::ARTICLES) + 1:
ASSERT_THAT(articles.size(), Eq(1u));
EXPECT_THAT(articles[0]->best_source().url.spec(),
Eq("http://localhost/foo2"));
break;
default:
FAIL() << "unknown category ID " << category.category.id();
}
}
EXPECT_THAT(snippets_fetcher().last_status(), Eq("OK"));
EXPECT_THAT(snippets_fetcher().last_json(), Eq(kJsonStr));
EXPECT_THAT(histogram_tester().GetAllSamples(
"NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
ElementsAre(base::Bucket(/*min=*/kTestJsonParsingLatencyMs,
/*count=*/1)));
}
TEST_F(NTPSnippetsFetcherTest, ShouldFetchSuccessfullyEmptyList) {
const std::string kJsonStr = "{\"recos\": []}";
SetFakeResponse(/*data=*/kJsonStr, net::HTTP_OK,
net::URLRequestStatus::SUCCESS);
EXPECT_CALL(mock_callback(), Run(IsEmptyArticleList()));
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
test_excluded(),
/*count=*/1,
/*interactive_request=*/true);
FastForwardUntilNoTasksRemain();
EXPECT_THAT(snippets_fetcher().last_status(), Eq("OK"));
EXPECT_THAT(snippets_fetcher().last_json(), Eq(kJsonStr));
EXPECT_THAT(
histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
ElementsAre(base::Bucket(/*min=*/0, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples(
"NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
}
TEST_F(NTPSnippetsFetcherHostRestrictedTest, ShouldReportEmptyHostsError) {
EXPECT_CALL(mock_callback(), Run(/*snippets=*/Not(HasValue()))).Times(1);
snippets_fetcher().FetchSnippetsFromHosts(/*hosts=*/std::set<std::string>(),
/*language_code=*/"en-US",
test_excluded(),
/*count=*/1,
/*interactive_request=*/true);
FastForwardUntilNoTasksRemain();
EXPECT_THAT(snippets_fetcher().last_status(),
Eq("Cannot fetch for empty hosts list."));
EXPECT_THAT(snippets_fetcher().last_json(), IsEmpty());
EXPECT_THAT(
histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
ElementsAre(base::Bucket(/*min=*/1, /*count=*/1)));
// This particular error gets triggered prior to fetching, so no fetch time
// or response should get recorded.
EXPECT_THAT(histogram_tester().GetAllSamples(
"NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
IsEmpty());
EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
IsEmpty());
}
TEST_F(NTPSnippetsFetcherHostRestrictedTest, ShouldRestrictToHosts) {
net::TestURLFetcherFactory test_url_fetcher_factory;
snippets_fetcher().FetchSnippetsFromHosts(
{"www.somehost1.com", "www.somehost2.com"}, test_lang(), test_excluded(),
/*count=*/17,
/*interactive_request=*/true);
net::TestURLFetcher* fetcher = test_url_fetcher_factory.GetFetcherByID(0);
ASSERT_THAT(fetcher, NotNull());
std::unique_ptr<base::Value> value =
base::JSONReader::Read(fetcher->upload_data());
ASSERT_TRUE(value) << " failed to parse JSON: "
<< PrintToString(fetcher->upload_data());
const base::DictionaryValue* dict = nullptr;
ASSERT_TRUE(value->GetAsDictionary(&dict));
const base::DictionaryValue* local_scoring_params = nullptr;
ASSERT_TRUE(dict->GetDictionary("advanced_options.local_scoring_params",
&local_scoring_params));
const base::ListValue* content_selectors = nullptr;
ASSERT_TRUE(
local_scoring_params->GetList("content_selectors", &content_selectors));
ASSERT_THAT(content_selectors->GetSize(), Eq(static_cast<size_t>(2)));
const base::DictionaryValue* content_selector = nullptr;
ASSERT_TRUE(content_selectors->GetDictionary(0, &content_selector));
std::string content_selector_value;
EXPECT_TRUE(content_selector->GetString("value", &content_selector_value));
EXPECT_THAT(content_selector_value, Eq("www.somehost1.com"));
ASSERT_TRUE(content_selectors->GetDictionary(1, &content_selector));
EXPECT_TRUE(content_selector->GetString("value", &content_selector_value));
EXPECT_THAT(content_selector_value, Eq("www.somehost2.com"));
}
TEST_F(NTPSnippetsFetcherTest, ShouldReportUrlStatusError) {
SetFakeResponse(/*data=*/std::string(), net::HTTP_NOT_FOUND,
net::URLRequestStatus::FAILED);
EXPECT_CALL(mock_callback(), Run(/*snippets=*/Not(HasValue()))).Times(1);
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
test_excluded(),
/*count=*/1,
/*interactive_request=*/true);
FastForwardUntilNoTasksRemain();
EXPECT_THAT(snippets_fetcher().last_status(),
Eq("URLRequestStatus error -2"));
EXPECT_THAT(snippets_fetcher().last_json(), IsEmpty());
EXPECT_THAT(
histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
ElementsAre(base::Bucket(/*min=*/2, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples(
"NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
ElementsAre(base::Bucket(/*min=*/-2, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
Not(IsEmpty()));
}
TEST_F(NTPSnippetsFetcherTest, ShouldReportHttpError) {
SetFakeResponse(/*data=*/std::string(), net::HTTP_NOT_FOUND,
net::URLRequestStatus::SUCCESS);
EXPECT_CALL(mock_callback(), Run(/*snippets=*/Not(HasValue()))).Times(1);
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
test_excluded(),
/*count=*/1,
/*interactive_request=*/true);
FastForwardUntilNoTasksRemain();
EXPECT_THAT(snippets_fetcher().last_json(), IsEmpty());
EXPECT_THAT(
histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
ElementsAre(base::Bucket(/*min=*/3, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples(
"NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
ElementsAre(base::Bucket(/*min=*/404, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
Not(IsEmpty()));
}
TEST_F(NTPSnippetsFetcherTest, ShouldReportJsonError) {
const std::string kInvalidJsonStr = "{ \"recos\": []";
SetFakeResponse(/*data=*/kInvalidJsonStr, net::HTTP_OK,
net::URLRequestStatus::SUCCESS);
EXPECT_CALL(mock_callback(), Run(/*snippets=*/Not(HasValue()))).Times(1);
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
test_excluded(),
/*count=*/1,
/*interactive_request=*/true);
FastForwardUntilNoTasksRemain();
EXPECT_THAT(snippets_fetcher().last_status(),
StartsWith("Received invalid JSON (error "));
EXPECT_THAT(snippets_fetcher().last_json(), Eq(kInvalidJsonStr));
EXPECT_THAT(
histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
ElementsAre(base::Bucket(/*min=*/4, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples(
"NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
ElementsAre(base::Bucket(/*min=*/kTestJsonParsingLatencyMs,
/*count=*/1)));
}
TEST_F(NTPSnippetsFetcherTest, ShouldReportJsonErrorForEmptyResponse) {
SetFakeResponse(/*data=*/std::string(), net::HTTP_OK,
net::URLRequestStatus::SUCCESS);
EXPECT_CALL(mock_callback(), Run(/*snippets=*/Not(HasValue()))).Times(1);
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
test_excluded(),
/*count=*/1,
/*interactive_request=*/true);
FastForwardUntilNoTasksRemain();
EXPECT_THAT(snippets_fetcher().last_json(), std::string());
EXPECT_THAT(
histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
ElementsAre(base::Bucket(/*min=*/4, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples(
"NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
}
TEST_F(NTPSnippetsFetcherTest, ShouldReportInvalidListError) {
const std::string kJsonStr =
"{\"recos\": [{ \"contentInfo\": { \"foo\" : \"bar\" }}]}";
SetFakeResponse(/*data=*/kJsonStr, net::HTTP_OK,
net::URLRequestStatus::SUCCESS);
EXPECT_CALL(mock_callback(), Run(/*snippets=*/Not(HasValue()))).Times(1);
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
test_excluded(),
/*count=*/1,
/*interactive_request=*/true);
FastForwardUntilNoTasksRemain();
EXPECT_THAT(snippets_fetcher().last_json(), Eq(kJsonStr));
EXPECT_THAT(
histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
ElementsAre(base::Bucket(/*min=*/5, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples(
"NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
Not(IsEmpty()));
}
// This test actually verifies that the test setup itself is sane, to prevent
// hard-to-reproduce test failures.
TEST_F(NTPSnippetsFetcherTest, ShouldReportHttpErrorForMissingBakedResponse) {
InitFakeURLFetcherFactory();
EXPECT_CALL(mock_callback(), Run(/*snippets=*/Not(HasValue()))).Times(1);
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
test_excluded(),
/*count=*/1,
/*interactive_request=*/true);
FastForwardUntilNoTasksRemain();
}
TEST_F(NTPSnippetsFetcherTest, ShouldCancelOngoingFetch) {
const std::string kJsonStr = "{ \"recos\": [] }";
SetFakeResponse(/*data=*/kJsonStr, net::HTTP_OK,
net::URLRequestStatus::SUCCESS);
EXPECT_CALL(mock_callback(), Run(IsEmptyArticleList()));
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
test_excluded(),
/*count=*/1,
/*interactive_request=*/true);
// Second call to FetchSnippetsFromHosts() overrides/cancels the previous.
// Callback is expected to be called once.
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
test_excluded(),
/*count=*/1,
/*interactive_request=*/true);
FastForwardUntilNoTasksRemain();
EXPECT_THAT(
histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
ElementsAre(base::Bucket(/*min=*/0, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples(
"NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
ElementsAre(base::Bucket(/*min=*/kTestJsonParsingLatencyMs,
/*count=*/1)));
}
::std::ostream& operator<<(
::std::ostream& os,
const NTPSnippetsFetcher::OptionalSnippets& snippets) {
if (snippets) {
// Matchers above aren't any more precise than this, so this is sufficient
// for test-failure diagnostics.
return os << "list with " << snippets->size() << " elements";
} else {
return os << "null";
}
}
} // namespace ntp_snippets