blob: 5f879d3d1d94cd744ff7081f219cb751deec43a2 [file] [log] [blame]
// Copyright 2014 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/suggestions/suggestions_service_impl.h"
#include <stdint.h>
#include <memory>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/macros.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_task_environment.h"
#include "components/suggestions/blacklist_store.h"
#include "components/suggestions/features.h"
#include "components/suggestions/proto/suggestions.pb.h"
#include "components/suggestions/suggestions_store.h"
#include "components/sync/driver/sync_service.h"
#include "components/sync/driver/test_sync_service.h"
#include "net/base/url_util.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/url_request/test_url_fetcher_factory.h"
#include "net/url_request/url_request_status.h"
#include "net/url_request/url_request_test_util.h"
#include "services/identity/public/cpp/identity_manager.h"
#include "services/identity/public/cpp/identity_test_environment.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.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"
#include "ui/gfx/image/image.h"
using syncer::SyncServiceObserver;
using testing::_;
using testing::AnyNumber;
using testing::DoAll;
using testing::Mock;
using testing::Return;
using testing::SetArgPointee;
using testing::StrictMock;
namespace {
const char kEmail[] = "foo_email";
const char kSuggestionsUrlPath[] = "/chromesuggestions";
const char kBlacklistUrlPath[] = "/chromesuggestions/blacklist";
const char kBlacklistClearUrlPath[] = "/chromesuggestions/blacklist/clear";
const char kTestTitle[] = "a title";
const char kTestUrl[] = "http://go.com";
const char kTestFaviconUrl[] =
"https://s2.googleusercontent.com/s2/favicons?domain_url="
"http://go.com&alt=s&sz=32";
const char kBlacklistedUrl[] = "http://blacklist.com";
const int64_t kTestSetExpiry = 12121212; // This timestamp lies in the past.
// GMock matcher for protobuf equality.
MATCHER_P(EqualsProto, message, "") {
// This implementation assumes protobuf serialization is deterministic, which
// is true in practice but technically not something that code is supposed
// to rely on. However, it vastly simplifies the implementation.
std::string expected_serialized, actual_serialized;
message.SerializeToString(&expected_serialized);
arg.SerializeToString(&actual_serialized);
return expected_serialized == actual_serialized;
}
} // namespace
namespace suggestions {
SuggestionsProfile CreateSuggestionsProfile() {
SuggestionsProfile profile;
profile.set_timestamp(123);
ChromeSuggestion* suggestion = profile.add_suggestions();
suggestion->set_title(kTestTitle);
suggestion->set_url(kTestUrl);
return profile;
}
class TestSuggestionsStore : public suggestions::SuggestionsStore {
public:
TestSuggestionsStore() { cached_suggestions = CreateSuggestionsProfile(); }
bool LoadSuggestions(SuggestionsProfile* suggestions) override {
suggestions->CopyFrom(cached_suggestions);
return cached_suggestions.suggestions_size();
}
bool StoreSuggestions(const SuggestionsProfile& suggestions) override {
cached_suggestions.CopyFrom(suggestions);
return true;
}
void ClearSuggestions() override {
cached_suggestions = SuggestionsProfile();
}
SuggestionsProfile cached_suggestions;
};
class MockBlacklistStore : public suggestions::BlacklistStore {
public:
MOCK_METHOD1(BlacklistUrl, bool(const GURL&));
MOCK_METHOD0(ClearBlacklist, void());
MOCK_METHOD1(GetTimeUntilReadyForUpload, bool(base::TimeDelta*));
MOCK_METHOD2(GetTimeUntilURLReadyForUpload,
bool(const GURL&, base::TimeDelta*));
MOCK_METHOD1(GetCandidateForUpload, bool(GURL*));
MOCK_METHOD1(RemoveUrl, bool(const GURL&));
MOCK_METHOD1(FilterSuggestions, void(SuggestionsProfile*));
};
class SuggestionsServiceTest : public testing::Test {
protected:
SuggestionsServiceTest() {
identity_test_env_.MakePrimaryAccountAvailable(kEmail);
identity_test_env_.SetAutomaticIssueOfAccessTokens(true);
}
~SuggestionsServiceTest() override {}
void SetUp() override {
sync_service()->SetPreferredDataTypes({syncer::HISTORY_DELETE_DIRECTIVES});
sync_service()->SetActiveDataTypes({syncer::HISTORY_DELETE_DIRECTIVES});
// These objects are owned by the SuggestionsService, but we keep the
// pointers around for testing.
test_suggestions_store_ = new TestSuggestionsStore();
mock_blacklist_store_ = new StrictMock<MockBlacklistStore>();
suggestions_service_ = std::make_unique<SuggestionsServiceImpl>(
identity_test_env_.identity_manager(), sync_service(),
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
url_loader_factory()),
base::WrapUnique(test_suggestions_store_),
base::WrapUnique(mock_blacklist_store_),
scoped_task_environment_.GetMockTickClock());
}
GURL GetCurrentlyQueriedUrl() {
if (url_loader_factory()->NumPending() == 0)
return GURL();
return url_loader_factory()->pending_requests()->front().request.url;
}
bool RespondToSuggestionsFetch(const std::string& response_body,
net::HttpStatusCode response_code,
int net_error = net::OK) {
return RespondToFetch(SuggestionsServiceImpl::BuildSuggestionsURL(),
response_body, response_code, net_error);
}
bool RespondToBlacklistFetch(const std::string& response_body,
net::HttpStatusCode response_code,
int net_error = net::OK) {
return RespondToFetch(SuggestionsServiceImpl::BuildSuggestionsBlacklistURL(
GURL(kBlacklistedUrl)),
response_body, response_code, net_error);
}
bool RespondToFetchWithProfile(const SuggestionsProfile& suggestions) {
return RespondToFetch(SuggestionsServiceImpl::BuildSuggestionsURL(),
suggestions.SerializeAsString(), net::HTTP_OK);
}
bool RespondToFetch(const GURL& url,
const std::string& response_body,
net::HttpStatusCode response_code,
int net_error = net::OK) {
bool rv = url_loader_factory()->SimulateResponseForPendingRequest(
url, network::URLLoaderCompletionStatus(net_error),
network::CreateResourceResponseHead(response_code), response_body);
scoped_task_environment_.RunUntilIdle();
return rv;
}
syncer::TestSyncService* sync_service() { return &test_sync_service_; }
MockBlacklistStore* blacklist_store() { return mock_blacklist_store_; }
TestSuggestionsStore* suggestions_store() { return test_suggestions_store_; }
SuggestionsServiceImpl* suggestions_service() {
return suggestions_service_.get();
}
identity::IdentityTestEnvironment* identity_test_env() {
return &identity_test_env_;
}
network::TestURLLoaderFactory* url_loader_factory() {
return &url_loader_factory_;
}
base::test::ScopedTaskEnvironment scoped_task_environment_{
base::test::ScopedTaskEnvironment::MainThreadType::MOCK_TIME};
private:
identity::IdentityTestEnvironment identity_test_env_;
syncer::TestSyncService test_sync_service_;
network::TestURLLoaderFactory url_loader_factory_;
// Owned by the SuggestionsService.
MockBlacklistStore* mock_blacklist_store_ = nullptr;
TestSuggestionsStore* test_suggestions_store_ = nullptr;
std::unique_ptr<SuggestionsServiceImpl> suggestions_service_;
DISALLOW_COPY_AND_ASSIGN(SuggestionsServiceTest);
};
TEST_F(SuggestionsServiceTest, FetchSuggestionsData) {
base::MockCallback<SuggestionsService::ResponseCallback> callback;
auto subscription = suggestions_service()->AddCallback(callback.Get());
EXPECT_CALL(*blacklist_store(), FilterSuggestions(_));
EXPECT_CALL(*blacklist_store(), GetTimeUntilReadyForUpload(_))
.WillOnce(Return(false));
// Send the request. The data should be returned to the callback.
suggestions_service()->FetchSuggestionsData();
EXPECT_CALL(callback, Run(_));
// Wait for the eventual network request.
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(GetCurrentlyQueriedUrl().is_valid());
EXPECT_EQ(GetCurrentlyQueriedUrl().path(), kSuggestionsUrlPath);
ASSERT_TRUE(RespondToFetchWithProfile(CreateSuggestionsProfile()));
SuggestionsProfile suggestions;
suggestions_store()->LoadSuggestions(&suggestions);
ASSERT_EQ(1, suggestions.suggestions_size());
EXPECT_EQ(kTestTitle, suggestions.suggestions(0).title());
EXPECT_EQ(kTestUrl, suggestions.suggestions(0).url());
EXPECT_EQ(kTestFaviconUrl, suggestions.suggestions(0).favicon_url());
}
TEST_F(SuggestionsServiceTest, IgnoresNoopSyncChange) {
base::MockCallback<SuggestionsService::ResponseCallback> callback;
EXPECT_CALL(callback, Run(_)).Times(0);
auto subscription = suggestions_service()->AddCallback(callback.Get());
// An no-op change should not result in a suggestions refresh.
static_cast<SyncServiceObserver*>(suggestions_service())
->OnStateChanged(sync_service());
// Wait for eventual (but unexpected) network requests.
scoped_task_environment_.RunUntilIdle();
EXPECT_FALSE(suggestions_service()->HasPendingRequestForTesting());
}
TEST_F(SuggestionsServiceTest, PersistentAuthErrorState) {
// Put some suggestions in.
suggestions_store()->StoreSuggestions(CreateSuggestionsProfile());
GoogleServiceAuthError error =
GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_ERROR);
sync_service()->SetAuthError(std::move(error));
// An no-op change should not result in a suggestions refresh.
static_cast<SyncServiceObserver*>(suggestions_service())
->OnStateChanged(sync_service());
// Wait for eventual (but unexpected) network requests.
scoped_task_environment_.RunUntilIdle();
EXPECT_FALSE(suggestions_service()->HasPendingRequestForTesting());
SuggestionsProfile empty_suggestions;
EXPECT_FALSE(suggestions_store()->LoadSuggestions(&empty_suggestions));
}
TEST_F(SuggestionsServiceTest, IgnoresUninterestingSyncChange) {
base::MockCallback<SuggestionsService::ResponseCallback> callback;
EXPECT_CALL(callback, Run(_)).Times(0);
auto subscription = suggestions_service()->AddCallback(callback.Get());
// An uninteresting change should not result in a network request (the
// SyncState is INITIALIZED_ENABLED_HISTORY before and after).
sync_service()->SetActiveDataTypes(
{syncer::HISTORY_DELETE_DIRECTIVES, syncer::BOOKMARKS});
static_cast<SyncServiceObserver*>(suggestions_service())
->OnStateChanged(sync_service());
// Wait for eventual (but unexpected) network requests.
scoped_task_environment_.RunUntilIdle();
EXPECT_FALSE(suggestions_service()->HasPendingRequestForTesting());
}
// During startup, the state changes from NOT_INITIALIZED_ENABLED to
// INITIALIZED_ENABLED_HISTORY (for a signed-in user with history sync enabled).
// This should *not* result in an automatic fetch.
TEST_F(SuggestionsServiceTest, DoesNotFetchOnStartup) {
// The sync service starts out inactive.
sync_service()->SetTransportState(
syncer::SyncService::TransportState::INITIALIZING);
static_cast<SyncServiceObserver*>(suggestions_service())
->OnStateChanged(sync_service());
scoped_task_environment_.RunUntilIdle();
ASSERT_FALSE(suggestions_service()->HasPendingRequestForTesting());
// Sync getting enabled should not result in a fetch.
sync_service()->SetTransportState(
syncer::SyncService::TransportState::ACTIVE);
static_cast<SyncServiceObserver*>(suggestions_service())
->OnStateChanged(sync_service());
// Wait for eventual (but unexpected) network requests.
scoped_task_environment_.RunUntilIdle();
EXPECT_FALSE(suggestions_service()->HasPendingRequestForTesting());
}
TEST_F(SuggestionsServiceTest, BuildUrlWithDefaultMinZeroParamForFewFeature) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(kUseSuggestionsEvenIfFewFeature);
EXPECT_CALL(*blacklist_store(), FilterSuggestions(_));
EXPECT_CALL(*blacklist_store(), GetTimeUntilReadyForUpload(_))
.WillOnce(Return(false));
// Send the request. The data should be returned to the callback.
suggestions_service()->FetchSuggestionsData();
// Wait for the eventual network request.
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(GetCurrentlyQueriedUrl().is_valid());
EXPECT_EQ(GetCurrentlyQueriedUrl().path(), kSuggestionsUrlPath);
std::string min_suggestions;
EXPECT_TRUE(net::GetValueForKeyInQuery(GetCurrentlyQueriedUrl(), "num",
&min_suggestions));
EXPECT_EQ(min_suggestions, "0");
ASSERT_TRUE(RespondToFetchWithProfile(CreateSuggestionsProfile()));
}
TEST_F(SuggestionsServiceTest, FetchSuggestionsDataSyncNotInitializedEnabled) {
sync_service()->SetTransportState(
syncer::SyncService::TransportState::INITIALIZING);
static_cast<SyncServiceObserver*>(suggestions_service())
->OnStateChanged(sync_service());
base::MockCallback<SuggestionsService::ResponseCallback> callback;
EXPECT_CALL(callback, Run(_)).Times(0);
auto subscription = suggestions_service()->AddCallback(callback.Get());
// Try to fetch suggestions. Since sync is not active, no network request
// should be sent.
suggestions_service()->FetchSuggestionsData();
// Wait for eventual (but unexpected) network requests.
scoped_task_environment_.RunUntilIdle();
EXPECT_FALSE(suggestions_service()->HasPendingRequestForTesting());
// |suggestions_store()| should still contain the default values.
SuggestionsProfile suggestions;
suggestions_store()->LoadSuggestions(&suggestions);
EXPECT_THAT(suggestions, EqualsProto(CreateSuggestionsProfile()));
}
TEST_F(SuggestionsServiceTest, FetchSuggestionsDataSyncDisabled) {
sync_service()->SetDisableReasons(
syncer::SyncService::DISABLE_REASON_USER_CHOICE);
sync_service()->SetTransportState(
syncer::SyncService::TransportState::DISABLED);
base::MockCallback<SuggestionsService::ResponseCallback> callback;
auto subscription = suggestions_service()->AddCallback(callback.Get());
// Tell SuggestionsService that the sync state changed. The cache should be
// cleared and empty data returned to the callback.
EXPECT_CALL(callback, Run(EqualsProto(SuggestionsProfile())));
static_cast<SyncServiceObserver*>(suggestions_service())
->OnStateChanged(sync_service());
// Try to fetch suggestions. Since sync is not active, no network request
// should be sent.
suggestions_service()->FetchSuggestionsData();
// Wait for eventual (but unexpected) network requests.
scoped_task_environment_.RunUntilIdle();
EXPECT_FALSE(suggestions_service()->HasPendingRequestForTesting());
}
TEST_F(SuggestionsServiceTest, FetchSuggestionsDataNoAccessToken) {
identity_test_env()->SetAutomaticIssueOfAccessTokens(false);
base::MockCallback<SuggestionsService::ResponseCallback> callback;
EXPECT_CALL(callback, Run(_)).Times(0);
auto subscription = suggestions_service()->AddCallback(callback.Get());
EXPECT_CALL(*blacklist_store(), GetTimeUntilReadyForUpload(_))
.WillOnce(Return(false));
suggestions_service()->FetchSuggestionsData();
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
GoogleServiceAuthError(
GoogleServiceAuthError::State::INVALID_GAIA_CREDENTIALS));
// Wait for eventual (but unexpected) network requests.
scoped_task_environment_.RunUntilIdle();
EXPECT_FALSE(suggestions_service()->HasPendingRequestForTesting());
}
TEST_F(SuggestionsServiceTest, FetchingSuggestionsIgnoresRequestFailure) {
EXPECT_CALL(*blacklist_store(), GetTimeUntilReadyForUpload(_))
.WillOnce(Return(false));
suggestions_service()->FetchSuggestionsData();
// Wait for the eventual network request.
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(RespondToSuggestionsFetch("irrelevant", net::HTTP_OK,
net::ERR_INVALID_RESPONSE));
}
TEST_F(SuggestionsServiceTest, FetchingSuggestionsClearsStoreIfResponseNotOK) {
suggestions_store()->StoreSuggestions(CreateSuggestionsProfile());
// Expect that an upload to the blacklist is scheduled.
EXPECT_CALL(*blacklist_store(), GetTimeUntilReadyForUpload(_))
.WillOnce(Return(false));
// Send the request. Empty data will be returned to the callback.
suggestions_service()->FetchSuggestionsData();
// Wait for the eventual network request.
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(RespondToSuggestionsFetch("irrelevant", net::HTTP_BAD_REQUEST));
SuggestionsProfile empty_suggestions;
EXPECT_FALSE(suggestions_store()->LoadSuggestions(&empty_suggestions));
}
TEST_F(SuggestionsServiceTest, BlacklistURL) {
const base::TimeDelta no_delay = base::TimeDelta::FromSeconds(0);
base::MockCallback<SuggestionsService::ResponseCallback> callback;
auto subscription = suggestions_service()->AddCallback(callback.Get());
EXPECT_CALL(*blacklist_store(), BlacklistUrl(GURL(kBlacklistedUrl)))
.WillOnce(Return(true));
EXPECT_CALL(*blacklist_store(), FilterSuggestions(_)).Times(2);
EXPECT_CALL(*blacklist_store(), GetTimeUntilReadyForUpload(_))
.WillOnce(DoAll(SetArgPointee<0>(no_delay), Return(true)))
.WillOnce(Return(false));
EXPECT_CALL(*blacklist_store(), GetCandidateForUpload(_))
.WillOnce(DoAll(SetArgPointee<0>(GURL(kBlacklistedUrl)), Return(true)));
EXPECT_CALL(*blacklist_store(), RemoveUrl(GURL(kBlacklistedUrl)))
.WillOnce(Return(true));
EXPECT_CALL(callback, Run(_)).Times(2);
EXPECT_TRUE(suggestions_service()->BlacklistURL(GURL(kBlacklistedUrl)));
// Wait on the upload task, the blacklist request and the next blacklist
// scheduling task.
scoped_task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_EQ(GetCurrentlyQueriedUrl().path(), kBlacklistUrlPath);
// The blacklist fetch needs to contain a valid profile or the favicon will
// not be set.
ASSERT_TRUE(RespondToBlacklistFetch(
CreateSuggestionsProfile().SerializeAsString(), net::HTTP_OK));
scoped_task_environment_.RunUntilIdle();
SuggestionsProfile suggestions;
suggestions_store()->LoadSuggestions(&suggestions);
ASSERT_EQ(1, suggestions.suggestions_size());
EXPECT_EQ(kTestTitle, suggestions.suggestions(0).title());
EXPECT_EQ(kTestUrl, suggestions.suggestions(0).url());
EXPECT_EQ(kTestFaviconUrl, suggestions.suggestions(0).favicon_url());
}
TEST_F(SuggestionsServiceTest, BlacklistURLFails) {
base::MockCallback<SuggestionsService::ResponseCallback> callback;
EXPECT_CALL(callback, Run(_)).Times(0);
auto subscription = suggestions_service()->AddCallback(callback.Get());
EXPECT_CALL(*blacklist_store(), BlacklistUrl(GURL(kBlacklistedUrl)))
.WillOnce(Return(false));
EXPECT_FALSE(suggestions_service()->BlacklistURL(GURL(kBlacklistedUrl)));
}
TEST_F(SuggestionsServiceTest, RetryBlacklistURLRequestAfterFailure) {
const base::TimeDelta no_delay = base::TimeDelta::FromSeconds(0);
base::MockCallback<SuggestionsService::ResponseCallback> callback;
auto subscription = suggestions_service()->AddCallback(callback.Get());
// Set expectations for first, failing request.
EXPECT_CALL(*blacklist_store(), GetTimeUntilReadyForUpload(_))
.Times(AnyNumber())
.WillRepeatedly(DoAll(SetArgPointee<0>(no_delay), Return(true)));
EXPECT_CALL(*blacklist_store(), FilterSuggestions(_));
EXPECT_CALL(*blacklist_store(), BlacklistUrl(GURL(kBlacklistedUrl)))
.WillOnce(Return(true));
EXPECT_CALL(*blacklist_store(), GetCandidateForUpload(_))
.WillOnce(DoAll(SetArgPointee<0>(GURL(kBlacklistedUrl)), Return(true)));
EXPECT_CALL(callback, Run(_)).Times(2);
// Blacklist call, first request attempt.
EXPECT_TRUE(suggestions_service()->BlacklistURL(GURL(kBlacklistedUrl)));
// Wait for the first scheduling receiving a failing response.
scoped_task_environment_.FastForwardUntilNoTasksRemain();
ASSERT_TRUE(GetCurrentlyQueriedUrl().is_valid());
EXPECT_EQ(GetCurrentlyQueriedUrl().path(), kBlacklistUrlPath);
ASSERT_TRUE(RespondToBlacklistFetch("irrelevant", net::HTTP_OK,
net::ERR_INVALID_RESPONSE));
// Assert that the failure was processed as expected.
Mock::VerifyAndClearExpectations(blacklist_store());
// Now expect the retried request to succeed.
EXPECT_CALL(*blacklist_store(), GetTimeUntilReadyForUpload(_))
.Times(AnyNumber())
.WillRepeatedly(DoAll(SetArgPointee<0>(no_delay), Return(true)));
EXPECT_CALL(*blacklist_store(), FilterSuggestions(_));
EXPECT_CALL(*blacklist_store(), GetCandidateForUpload(_))
.WillOnce(DoAll(SetArgPointee<0>(GURL(kBlacklistedUrl)), Return(true)));
EXPECT_CALL(*blacklist_store(), RemoveUrl(GURL(kBlacklistedUrl)))
.WillOnce(Return(true));
// Wait for the second scheduling followed by a successful response.
scoped_task_environment_.FastForwardUntilNoTasksRemain();
ASSERT_TRUE(suggestions_service()->HasPendingRequestForTesting());
ASSERT_TRUE(GetCurrentlyQueriedUrl().is_valid());
EXPECT_EQ(GetCurrentlyQueriedUrl().path(), kBlacklistUrlPath);
ASSERT_TRUE(RespondToBlacklistFetch(
CreateSuggestionsProfile().SerializeAsString(), net::HTTP_OK));
SuggestionsProfile suggestions;
suggestions_store()->LoadSuggestions(&suggestions);
ASSERT_EQ(1, suggestions.suggestions_size());
EXPECT_EQ(kTestTitle, suggestions.suggestions(0).title());
EXPECT_EQ(kTestUrl, suggestions.suggestions(0).url());
EXPECT_EQ(kTestFaviconUrl, suggestions.suggestions(0).favicon_url());
}
TEST_F(SuggestionsServiceTest, UndoBlacklistURL) {
// Ensure scheduling the request doesn't happen before undo.
const base::TimeDelta delay = base::TimeDelta::FromHours(1);
base::MockCallback<SuggestionsService::ResponseCallback> callback;
auto subscription = suggestions_service()->AddCallback(callback.Get());
// Blacklist expectations.
EXPECT_CALL(*blacklist_store(), BlacklistUrl(GURL(kBlacklistedUrl)))
.WillOnce(Return(true));
EXPECT_CALL(*blacklist_store(), FilterSuggestions(_)).Times(AnyNumber());
EXPECT_CALL(*blacklist_store(), GetTimeUntilReadyForUpload(_))
.WillOnce(DoAll(SetArgPointee<0>(delay), Return(true)));
// Undo expectations.
EXPECT_CALL(*blacklist_store(),
GetTimeUntilURLReadyForUpload(GURL(kBlacklistedUrl), _))
.WillOnce(DoAll(SetArgPointee<1>(delay), Return(true)));
EXPECT_CALL(*blacklist_store(), RemoveUrl(GURL(kBlacklistedUrl)))
.WillOnce(Return(true));
EXPECT_CALL(callback, Run(_)).Times(2);
EXPECT_TRUE(suggestions_service()->BlacklistURL(GURL(kBlacklistedUrl)));
EXPECT_TRUE(suggestions_service()->UndoBlacklistURL(GURL(kBlacklistedUrl)));
}
TEST_F(SuggestionsServiceTest, ClearBlacklist) {
const base::TimeDelta delay = base::TimeDelta::FromHours(1);
base::MockCallback<SuggestionsService::ResponseCallback> callback;
auto subscription = suggestions_service()->AddCallback(callback.Get());
// Blacklist expectations.
EXPECT_CALL(*blacklist_store(), BlacklistUrl(GURL(kBlacklistedUrl)))
.WillOnce(Return(true));
EXPECT_CALL(*blacklist_store(), FilterSuggestions(_)).Times(AnyNumber());
EXPECT_CALL(*blacklist_store(), GetTimeUntilReadyForUpload(_))
.WillOnce(DoAll(SetArgPointee<0>(delay), Return(true)));
EXPECT_CALL(*blacklist_store(), ClearBlacklist());
EXPECT_CALL(callback, Run(_)).Times(2);
EXPECT_TRUE(suggestions_service()->BlacklistURL(GURL(kBlacklistedUrl)));
suggestions_service()->ClearBlacklist();
// Wait for the eventual network request.
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(suggestions_service()->HasPendingRequestForTesting());
EXPECT_EQ(GetCurrentlyQueriedUrl().path(), kBlacklistClearUrlPath);
}
TEST_F(SuggestionsServiceTest, UndoBlacklistURLFailsIfNotInBlacklist) {
// Ensure scheduling the request doesn't happen before undo.
const base::TimeDelta delay = base::TimeDelta::FromHours(1);
base::MockCallback<SuggestionsService::ResponseCallback> callback;
auto subscription = suggestions_service()->AddCallback(callback.Get());
// Blacklist expectations.
EXPECT_CALL(*blacklist_store(), BlacklistUrl(GURL(kBlacklistedUrl)))
.WillOnce(Return(true));
EXPECT_CALL(*blacklist_store(), FilterSuggestions(_));
EXPECT_CALL(*blacklist_store(), GetTimeUntilReadyForUpload(_))
.WillOnce(DoAll(SetArgPointee<0>(delay), Return(true)));
// Undo expectations.
// URL is not in local blacklist.
EXPECT_CALL(*blacklist_store(),
GetTimeUntilURLReadyForUpload(GURL(kBlacklistedUrl), _))
.WillOnce(Return(false));
EXPECT_CALL(callback, Run(_));
EXPECT_TRUE(suggestions_service()->BlacklistURL(GURL(kBlacklistedUrl)));
EXPECT_FALSE(suggestions_service()->UndoBlacklistURL(GURL(kBlacklistedUrl)));
}
TEST_F(SuggestionsServiceTest, UndoBlacklistURLFailsIfAlreadyCandidate) {
// Ensure scheduling the request doesn't happen before undo.
const base::TimeDelta delay = base::TimeDelta::FromHours(1);
base::MockCallback<SuggestionsService::ResponseCallback> callback;
auto subscription = suggestions_service()->AddCallback(callback.Get());
// Blacklist expectations.
EXPECT_CALL(*blacklist_store(), BlacklistUrl(GURL(kBlacklistedUrl)))
.WillOnce(Return(true));
EXPECT_CALL(*blacklist_store(), FilterSuggestions(_));
EXPECT_CALL(*blacklist_store(), GetTimeUntilReadyForUpload(_))
.WillOnce(DoAll(SetArgPointee<0>(delay), Return(true)));
// URL is not yet candidate for upload.
const base::TimeDelta negative_delay = base::TimeDelta::FromHours(-1);
EXPECT_CALL(*blacklist_store(),
GetTimeUntilURLReadyForUpload(GURL(kBlacklistedUrl), _))
.WillOnce(DoAll(SetArgPointee<1>(negative_delay), Return(true)));
EXPECT_CALL(callback, Run(_));
EXPECT_TRUE(suggestions_service()->BlacklistURL(GURL(kBlacklistedUrl)));
EXPECT_FALSE(suggestions_service()->UndoBlacklistURL(GURL(kBlacklistedUrl)));
}
TEST_F(SuggestionsServiceTest, TemporarilyIncreasesBlacklistDelayOnFailure) {
EXPECT_CALL(*blacklist_store(), FilterSuggestions(_)).Times(AnyNumber());
EXPECT_CALL(*blacklist_store(), GetTimeUntilReadyForUpload(_))
.Times(AnyNumber())
.WillRepeatedly(Return(false));
const base::TimeDelta initial_delay =
suggestions_service()->BlacklistDelayForTesting();
// Delay unchanged on success.
suggestions_service()->FetchSuggestionsData();
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(RespondToFetchWithProfile(CreateSuggestionsProfile()));
EXPECT_EQ(initial_delay, suggestions_service()->BlacklistDelayForTesting());
// Delay increases on failure.
suggestions_service()->FetchSuggestionsData();
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(RespondToSuggestionsFetch("irrelevant", net::HTTP_BAD_REQUEST));
base::TimeDelta delay_after_fail =
suggestions_service()->BlacklistDelayForTesting();
EXPECT_GT(delay_after_fail, initial_delay);
// Success resets future delays, but the current horizon remains. Since no
// time has passed, the actual current delay stays the same.
suggestions_service()->FetchSuggestionsData();
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(RespondToFetchWithProfile(CreateSuggestionsProfile()));
EXPECT_EQ(delay_after_fail,
suggestions_service()->BlacklistDelayForTesting());
// After the current horizon has passed, we're back at the initial delay.
scoped_task_environment_.FastForwardBy(delay_after_fail);
suggestions_service()->FetchSuggestionsData();
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(RespondToFetchWithProfile(CreateSuggestionsProfile()));
EXPECT_EQ(initial_delay, suggestions_service()->BlacklistDelayForTesting());
}
TEST_F(SuggestionsServiceTest, DoesNotOverrideDefaultExpiryTime) {
EXPECT_CALL(*blacklist_store(), FilterSuggestions(_));
EXPECT_CALL(*blacklist_store(), GetTimeUntilReadyForUpload(_))
.WillOnce(Return(false));
suggestions_service()->FetchSuggestionsData();
scoped_task_environment_.RunUntilIdle();
// Creates one suggestion without timestamp and adds a second with timestamp.
SuggestionsProfile profile = CreateSuggestionsProfile();
ChromeSuggestion* suggestion = profile.add_suggestions();
suggestion->set_title(kTestTitle);
suggestion->set_url(kTestUrl);
suggestion->set_expiry_ts(kTestSetExpiry);
ASSERT_TRUE(RespondToFetchWithProfile(profile));
SuggestionsProfile suggestions;
suggestions_store()->LoadSuggestions(&suggestions);
ASSERT_EQ(2, suggestions.suggestions_size());
// Suggestion[0] had no time stamp and should be ahead of the old suggestion.
EXPECT_LT(kTestSetExpiry, suggestions.suggestions(0).expiry_ts());
// Suggestion[1] had a very old time stamp but should not be updated.
EXPECT_EQ(kTestSetExpiry, suggestions.suggestions(1).expiry_ts());
}
} // namespace suggestions