blob: 4ac227e5c5c5e5e593f33dec8c45c0f74ad4f0db [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 "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/ntp_snippet.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 "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::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;
const char kTestContentSnippetsServerFormat[] =
"https://chromereader-pa.googleapis.com/v1/fetch?key=%s";
// Artificial time delay for JSON parsing.
const int64_t kTestJsonParsingLatencyMs = 20;
MATCHER(HasValue, "") {
return static_cast<bool>(arg);
}
MATCHER_P(PointeeSizeIs,
size,
std::string("contains a value with size ") + PrintToString(size)) {
return arg && static_cast<int>(arg->size()) == size;
}
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(const 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::WrapUnique(new 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));
}
} // namespace
class NTPSnippetsFetcherTest : public testing::Test {
public:
NTPSnippetsFetcherTest()
: 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()),
snippets_fetcher_(
fake_signin_manager_.get(),
fake_token_service_.get(),
scoped_refptr<net::TestURLRequestContextGetter>(
new net::TestURLRequestContextGetter(mock_task_runner_.get())),
base::Bind(&ParseJsonDelayed),
/*is_stable_channel=*/true),
test_lang_("en-US"),
test_url_(base::StringPrintf(kTestContentSnippetsServerFormat,
google_apis::GetAPIKey().c_str())) {
snippets_fetcher_.SetCallback(
base::Bind(&MockSnippetsAvailableCallback::WrappedRun,
base::Unretained(&mock_callback_)));
snippets_fetcher_.SetTickClockForTesting(
mock_task_runner_->GetMockTickClock());
test_hosts_.insert("www.somehost.com");
// 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_; }
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:
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_;
NTPSnippetsFetcher snippets_fetcher_;
MockSnippetsAvailableCallback mock_callback_;
const std::string test_lang_;
const GURL test_url_;
std::set<std::string> test_hosts_;
base::HistogramTester histogram_tester_;
DISALLOW_COPY_AND_ASSIGN(NTPSnippetsFetcherTest);
};
TEST_F(NTPSnippetsFetcherTest, BuildRequestAuthenticated) {
EXPECT_THAT(NTPSnippetsFetcher::BuildRequest("0BFUSGAIA", true, "en",
{"chromium.org"}, 25),
EqualsJSON("{"
" \"response_detail_level\": \"STANDARD\","
" \"obfuscated_gaia_id\": \"0BFUSGAIA\","
" \"advanced_options\": {"
" \"local_scoring_params\": {"
" \"content_params\": {"
" \"only_return_personalized_results\": true,"
" \"user_segment\": \"en\""
" },"
" \"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"
" }"
" }"
"}"));
}
TEST_F(NTPSnippetsFetcherTest, BuildRequestUnauthenticated) {
EXPECT_THAT(NTPSnippetsFetcher::BuildRequest("", false, "",
std::set<std::string>(), 10),
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"
" }"
" }"
"}"));
}
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(/*snippets=*/PointeeSizeIs(1))).Times(1);
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
/*count=*/1);
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(NTPSnippetsFetcherTest, ShouldFetchSuccessfullyEmptyList) {
const std::string kJsonStr = "{\"recos\": []}";
SetFakeResponse(/*data=*/kJsonStr, net::HTTP_OK,
net::URLRequestStatus::SUCCESS);
EXPECT_CALL(mock_callback(), Run(/*snippets=*/PointeeSizeIs(0))).Times(1);
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
/*count=*/1);
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)));
}
// TODO(jkrcal) Return the tests ShouldReportEmptyHostsError and
// ShouldRestrictToHosts once we have a way to change variation parameters from
// unittests. The tests were tailored to previous default value of the parameter
// fetching_host_restrict, which changed now.
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(),
/*count=*/1);
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(),
/*count=*/1);
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(),
/*count=*/1);
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(),
/*count=*/1);
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(),
/*count=*/1);
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(),
/*count=*/1);
FastForwardUntilNoTasksRemain();
}
TEST_F(NTPSnippetsFetcherTest, ShouldCancelOngoingFetch) {
const std::string kJsonStr = "{ \"recos\": [] }";
SetFakeResponse(/*data=*/kJsonStr, net::HTTP_OK,
net::URLRequestStatus::SUCCESS);
EXPECT_CALL(mock_callback(), Run(/*snippets=*/PointeeSizeIs(0))).Times(1);
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
/*count=*/1);
// Second call to FetchSnippetsFromHosts() overrides/cancels the previous.
// Callback is expected to be called once.
snippets_fetcher().FetchSnippetsFromHosts(test_hosts(), test_lang(),
/*count=*/1);
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)));
}
} // namespace ntp_snippets