blob: b8f4e7161472b1adbccfbfe46d54e92b7a199f5e [file] [log] [blame]
// Copyright 2015 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_service.h"
#include <memory>
#include "base/json/json_reader.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "components/ntp_snippets/ntp_snippet.h"
#include "components/ntp_snippets/ntp_snippets_fetcher.h"
#include "components/ntp_snippets/ntp_snippets_scheduler.h"
#include "components/prefs/testing_pref_service.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using testing::_;
namespace ntp_snippets {
namespace {
const base::Time::Exploded kDefaultCreationTime = {2015, 11, 4, 25, 13, 46, 45};
base::Time GetDefaultCreationTime() {
return base::Time::FromUTCExploded(kDefaultCreationTime);
}
std::string GetTestJson(const std::string& content_creation_time_str,
const std::string& expiry_time_str) {
char json_str_format[] =
"{ \"recos\": [ "
"{ \"contentInfo\": {"
"\"url\" : \"http://localhost/foobar\","
"\"site_title\" : \"Site Title\","
"\"favicon_url\" : \"http://localhost/favicon\","
"\"title\" : \"Title\","
"\"snippet\" : \"Snippet\","
"\"thumbnailUrl\" : \"http://localhost/salient_image\","
"\"creationTimestampSec\" : \"%s\","
"\"expiryTimestampSec\" : \"%s\","
"\"sourceCorpusInfo\" : [ "
"{\"ampUrl\" : \"http://localhost/amp\"},"
"{\"corpusId\" : \"id\"}]"
"}}"
"]}";
return base::StringPrintf(json_str_format, content_creation_time_str.c_str(),
expiry_time_str.c_str());
}
std::string GetTestJson(const std::string& content_creation_time_str) {
base::Time expiry_time = base::Time::Now() + base::TimeDelta::FromHours(1);
return GetTestJson(content_creation_time_str,
NTPSnippet::TimeToJsonString(expiry_time));
}
std::string GetTestJson() {
return GetTestJson(NTPSnippet::TimeToJsonString(GetDefaultCreationTime()));
}
std::string GetTestExpiredJson() {
return GetTestJson(NTPSnippet::TimeToJsonString(GetDefaultCreationTime()),
NTPSnippet::TimeToJsonString(base::Time::Now()));
}
std::string GetInvalidJson() {
std::string json_str = GetTestJson();
// Make the json invalid by removing the final closing brace.
return json_str.substr(0, json_str.size() - 1);
}
std::string GetIncompleteJson() {
std::string json_str = GetTestJson();
// Rename the "url" entry. The result is syntactically valid json that will
// fail to parse as snippets.
size_t pos = json_str.find("\"url\"");
if (pos == std::string::npos) {
NOTREACHED();
return std::string();
}
json_str[pos + 1] = 'x';
return json_str;
}
void ParseJson(
bool expect_success,
const std::string& json,
const ntp_snippets::NTPSnippetsService::SuccessCallback& success_callback,
const ntp_snippets::NTPSnippetsService::ErrorCallback& error_callback) {
base::JSONReader json_reader;
std::unique_ptr<base::Value> value = json_reader.ReadToValue(json);
bool success = !!value;
EXPECT_EQ(expect_success, success);
if (value) {
success_callback.Run(std::move(value));
} else {
error_callback.Run(json_reader.GetErrorMessage());
}
}
class MockScheduler : public NTPSnippetsScheduler {
public:
MOCK_METHOD4(Schedule,
bool(base::TimeDelta period_wifi_charging,
base::TimeDelta period_wifi,
base::TimeDelta period_fallback,
base::Time reschedule_time));
MOCK_METHOD0(Unschedule, bool());
};
} // namespace
class NTPSnippetsServiceTest : public testing::Test {
public:
NTPSnippetsServiceTest()
: pref_service_(new TestingPrefServiceSimple()) {}
~NTPSnippetsServiceTest() override {}
void SetUp() override {
NTPSnippetsService::RegisterProfilePrefs(pref_service_->registry());
CreateSnippetsService();
}
virtual void CreateSnippetsService() {
CreateSnippetsServiceEnabled(true);
}
void CreateSnippetsServiceEnabled(bool enabled) {
scheduler_.reset(new MockScheduler);
scoped_refptr<base::SingleThreadTaskRunner> task_runner(
base::ThreadTaskRunnerHandle::Get());
scoped_refptr<net::TestURLRequestContextGetter> request_context_getter =
new net::TestURLRequestContextGetter(task_runner.get());
service_.reset(new NTPSnippetsService(
pref_service_.get(), nullptr, task_runner, std::string("fr"),
scheduler_.get(),
base::WrapUnique(new NTPSnippetsFetcher(
task_runner, std::move(request_context_getter), true)),
base::Bind(&ParseJson, true)));
if (enabled)
EXPECT_CALL(*scheduler_, Schedule(_, _, _, _));
else
EXPECT_CALL(*scheduler_, Unschedule());
service_->Init(enabled);
}
protected:
NTPSnippetsService* service() { return service_.get(); }
void LoadFromJSONString(const std::string& json) {
service_->OnSnippetsDownloaded(json, std::string());
}
void SetExpectJsonParseSuccess(bool expect_success) {
service_->parse_json_callback_ = base::Bind(&ParseJson, expect_success);
}
private:
base::MessageLoop message_loop_;
std::unique_ptr<TestingPrefServiceSimple> pref_service_;
std::unique_ptr<NTPSnippetsService> service_;
std::unique_ptr<MockScheduler> scheduler_;
DISALLOW_COPY_AND_ASSIGN(NTPSnippetsServiceTest);
};
class NTPSnippetsServiceDisabledTest : public NTPSnippetsServiceTest {
public:
void CreateSnippetsService() override {
CreateSnippetsServiceEnabled(false);
}
};
TEST_F(NTPSnippetsServiceTest, Schedule) {
// CreateSnippetsServiceEnabled checks that Schedule is called.
}
TEST_F(NTPSnippetsServiceDisabledTest, Unschedule) {
// CreateSnippetsServiceEnabled checks that Unschedule is called.
}
TEST_F(NTPSnippetsServiceTest, Loop) {
std::string json_str(
"{ \"recos\": [ "
" { \"contentInfo\": { \"url\" : \"http://localhost/foobar\" }}"
"]}");
LoadFromJSONString(json_str);
// The same for loop without the '&' should not compile.
for (auto& snippet : *service()) {
// Snippet here is a const.
EXPECT_EQ(snippet.url(), GURL("http://localhost/foobar"));
}
// Without the const, this should not compile.
for (const NTPSnippet& snippet : *service()) {
EXPECT_EQ(snippet.url(), GURL("http://localhost/foobar"));
}
}
TEST_F(NTPSnippetsServiceTest, Full) {
std::string json_str(GetTestJson());
LoadFromJSONString(json_str);
EXPECT_EQ(service()->size(), 1u);
// The same for loop without the '&' should not compile.
for (auto& snippet : *service()) {
// Snippet here is a const.
EXPECT_EQ(snippet.url(), GURL("http://localhost/foobar"));
EXPECT_EQ(snippet.site_title(), "Site Title");
EXPECT_EQ(snippet.favicon_url(), GURL("http://localhost/favicon"));
EXPECT_EQ(snippet.title(), "Title");
EXPECT_EQ(snippet.snippet(), "Snippet");
EXPECT_EQ(snippet.salient_image_url(),
GURL("http://localhost/salient_image"));
EXPECT_EQ(GetDefaultCreationTime(), snippet.publish_date());
EXPECT_EQ(snippet.amp_url(), GURL("http://localhost/amp"));
}
}
TEST_F(NTPSnippetsServiceTest, Clear) {
std::string json_str(GetTestJson());
LoadFromJSONString(json_str);
EXPECT_EQ(service()->size(), 1u);
service()->ClearSnippets();
EXPECT_EQ(service()->size(), 0u);
}
TEST_F(NTPSnippetsServiceTest, InsertAtFront) {
std::string json_str(
"{ \"recos\": [ "
" { \"contentInfo\": { \"url\" : \"http://first\" }}"
"]}");
LoadFromJSONString(json_str);
ASSERT_EQ(service()->size(), 1u);
std::string json_str2(
"{ \"recos\": [ "
" { \"contentInfo\": { \"url\" : \"http://second\" }}"
"]}");
LoadFromJSONString(json_str2);
ASSERT_EQ(service()->size(), 2u);
// The snippet loaded last should be at the first position in the list now.
const NTPSnippet& first_snippet = *service()->begin();
EXPECT_EQ(first_snippet.url(), GURL("http://second"));
}
TEST_F(NTPSnippetsServiceTest, LimitNumSnippets) {
int max_snippet_count = NTPSnippetsService::GetMaxSnippetCountForTesting();
int snippets_per_load = max_snippet_count / 2 + 1;
const char snippet_format[] =
"{ \"contentInfo\": { \"url\" : \"http://localhost/%i\" }}";
std::vector<std::string> snippets1;
std::vector<std::string> snippets2;
for (int i = 0; i < snippets_per_load; i++) {
snippets1.push_back(base::StringPrintf(snippet_format, i));
snippets2.push_back(base::StringPrintf(snippet_format,
snippets_per_load + i));
}
LoadFromJSONString(
"{ \"recos\": [ " + base::JoinString(snippets1, ", ") + "]}");
ASSERT_EQ(snippets1.size(), service()->size());
LoadFromJSONString(
"{ \"recos\": [ " + base::JoinString(snippets2, ", ") + "]}");
EXPECT_EQ(max_snippet_count, (int)service()->size());
}
TEST_F(NTPSnippetsServiceTest, LoadInvalidJson) {
SetExpectJsonParseSuccess(false);
LoadFromJSONString(GetInvalidJson());
EXPECT_EQ(service()->size(), 0u);
}
TEST_F(NTPSnippetsServiceTest, LoadInvalidJsonWithExistingSnippets) {
LoadFromJSONString(GetTestJson());
ASSERT_EQ(service()->size(), 1u);
SetExpectJsonParseSuccess(false);
LoadFromJSONString(GetInvalidJson());
// This should not have changed the existing snippets.
EXPECT_EQ(service()->size(), 1u);
}
TEST_F(NTPSnippetsServiceTest, LoadIncompleteJson) {
LoadFromJSONString(GetIncompleteJson());
EXPECT_EQ(service()->size(), 0u);
}
TEST_F(NTPSnippetsServiceTest, LoadIncompleteJsonWithExistingSnippets) {
LoadFromJSONString(GetTestJson());
ASSERT_EQ(service()->size(), 1u);
LoadFromJSONString(GetIncompleteJson());
// This should not have changed the existing snippets.
EXPECT_EQ(service()->size(), 1u);
}
TEST_F(NTPSnippetsServiceTest, Discard) {
std::string json_str(
"{ \"recos\": [ { \"contentInfo\": { \"url\" : \"http://site.com\" }}]}");
LoadFromJSONString(json_str);
ASSERT_EQ(1u, service()->size());
// Discarding a non-existent snippet shouldn't do anything.
EXPECT_FALSE(service()->DiscardSnippet(GURL("http://othersite.com")));
EXPECT_EQ(1u, service()->size());
// Discard the snippet.
EXPECT_TRUE(service()->DiscardSnippet(GURL("http://site.com")));
EXPECT_EQ(0u, service()->size());
// Make sure that fetching the same snippet again does not re-add it.
LoadFromJSONString(json_str);
EXPECT_EQ(0u, service()->size());
// The snippet should stay discarded even after re-creating the service.
CreateSnippetsService();
LoadFromJSONString(json_str);
EXPECT_EQ(0u, service()->size());
// The snippet can be added again after clearing discarded snippets.
service()->ClearDiscardedSnippets();
EXPECT_EQ(0u, service()->size());
LoadFromJSONString(json_str);
EXPECT_EQ(1u, service()->size());
}
TEST_F(NTPSnippetsServiceTest, GetDiscarded) {
std::string json_str(
"{ \"recos\": [ { \"contentInfo\": { \"url\" : \"http://site.com\" }}]}");
LoadFromJSONString(json_str);
// For the test, we need the snippet to get discarded.
ASSERT_TRUE(service()->DiscardSnippet(GURL("http://site.com")));
const NTPSnippetsService::NTPSnippetStorage& snippets =
service()->discarded_snippets();
EXPECT_EQ(1u, snippets.size());
for (auto& snippet : snippets) {
EXPECT_EQ(GURL("http://site.com"), snippet->url());
}
// There should be no discarded snippet after clearing the list.
service()->ClearDiscardedSnippets();
EXPECT_EQ(0u, service()->discarded_snippets().size());
}
TEST_F(NTPSnippetsServiceTest, CreationTimestampParseFail) {
std::string json_str(GetTestJson("aaa1448459205"));
LoadFromJSONString(json_str);
EXPECT_EQ(service()->size(), 1u);
// The same for loop without the '&' should not compile.
for (auto& snippet : *service()) {
// Snippet here is a const.
EXPECT_EQ(snippet.url(), GURL("http://localhost/foobar"));
EXPECT_EQ(snippet.title(), "Title");
EXPECT_EQ(snippet.snippet(), "Snippet");
EXPECT_EQ(base::Time::UnixEpoch(), snippet.publish_date());
}
}
TEST_F(NTPSnippetsServiceTest, RemoveExpiredContent) {
std::string json_str(GetTestExpiredJson());
LoadFromJSONString(json_str);
EXPECT_EQ(service()->size(), 0u);
}
} // namespace ntp_snippets