blob: 7010dc1d9e51000a8777606edf518fa262ecaf09 [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 "chrome/browser/ntp_snippets/download_suggestions_provider.h"
#include <memory>
#include <utility>
#include "base/bind.h"
#include "base/observer_list.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/simple_test_clock.h"
#include "base/time/default_clock.h"
#include "components/download/public/common/mock_download_item.h"
#include "components/ntp_snippets/category.h"
#include "components/ntp_snippets/mock_content_suggestions_provider_observer.h"
#include "components/ntp_snippets/offline_pages/offline_pages_test_utils.h"
#include "components/offline_pages/core/client_namespace_constants.h"
#include "components/prefs/testing_pref_service.h"
#include "content/public/test/fake_download_item.h"
#include "content/public/test/mock_download_manager.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "testing/gtest/include/gtest/gtest.h"
using download::DownloadItem;
using content::FakeDownloadItem;
using content::MockDownloadManager;
using ntp_snippets::Category;
using ntp_snippets::CategoryStatus;
using ntp_snippets::ContentSuggestion;
using ntp_snippets::ContentSuggestionsProvider;
using ntp_snippets::MockContentSuggestionsProviderObserver;
using ntp_snippets::test::CaptureDismissedSuggestions;
using ntp_snippets::test::FakeOfflinePageModel;
using offline_pages::ClientId;
using offline_pages::OfflinePageItem;
using testing::_;
using testing::AllOf;
using testing::AnyNumber;
using testing::ElementsAre;
using testing::IsEmpty;
using testing::Lt;
using testing::Mock;
using testing::Return;
using testing::SizeIs;
using testing::StrictMock;
using testing::UnorderedElementsAre;
namespace ntp_snippets {
// These functions are implicitly used to print out values during the tests.
std::ostream& operator<<(std::ostream& os, const ContentSuggestion& value) {
os << "{ url: " << value.url() << ", publish_date: " << value.publish_date()
<< "}";
return os;
}
std::ostream& operator<<(std::ostream& os, const CategoryStatus& value) {
os << "CategoryStatus::";
switch (value) {
case CategoryStatus::INITIALIZING:
os << "INITIALIZING";
break;
case CategoryStatus::AVAILABLE:
os << "AVAILABLE";
break;
case CategoryStatus::AVAILABLE_LOADING:
os << "AVAILABLE_LOADING";
break;
case CategoryStatus::NOT_PROVIDED:
os << "NOT_PROVIDED";
break;
case CategoryStatus::ALL_SUGGESTIONS_EXPLICITLY_DISABLED:
os << "ALL_SUGGESTIONS_EXPLICITLY_DISABLED";
break;
case CategoryStatus::CATEGORY_EXPLICITLY_DISABLED:
os << "CATEGORY_EXPLICITLY_DISABLED";
break;
case CategoryStatus::LOADING_ERROR:
os << "LOADING_ERROR";
break;
}
return os;
}
} // namespace ntp_snippets
namespace {
const int kDefaultMaxDownloadAgeHours = 24;
base::Time CalculateDummyNowTime() {
base::Time now;
CHECK(base::Time::FromUTCString("2017-01-31 13:00:00", &now));
return now;
}
const base::Time now = CalculateDummyNowTime();
const base::Time kNotOutdatedTime =
now - base::TimeDelta::FromHours(kDefaultMaxDownloadAgeHours) +
base::TimeDelta::FromSeconds(1);
const base::Time kOutdatedTime =
now - base::TimeDelta::FromHours(kDefaultMaxDownloadAgeHours) -
base::TimeDelta::FromSeconds(1);
// TODO(vitaliii): Move this and outputting functions above to common file and
// replace remaining |Property(&ContentSuggestion::url, GURL("some_url"))|.
// See crbug.com/655513.
MATCHER_P(HasUrl, url, "") {
*result_listener << "expected URL: " << url
<< "has URL: " << arg.url().spec();
return arg.url().spec() == url;
}
MATCHER_P3(HasDownloadSuggestionExtra,
is_download_asset,
target_file_path,
mime_type,
"") {
if (arg.download_suggestion_extra() == nullptr) {
*result_listener << "has no download_suggestion_extra";
return false;
}
auto extra = *arg.download_suggestion_extra();
*result_listener << "expected download asset?: " << is_download_asset
<< "\n actual is download asset?: "
<< extra.is_download_asset;
if (extra.is_download_asset != is_download_asset) {
return false;
}
*result_listener << "expected target_file_path: " << target_file_path
<< "\nactual target_file_path: "
<< extra.target_file_path.value();
if (extra.target_file_path.value() !=
base::FilePath::StringType(target_file_path)) {
return false;
}
*result_listener << "expected mime_type: " << mime_type
<< "\nactual mime_type: " << extra.mime_type;
return extra.mime_type == mime_type;
}
OfflinePageItem CreateDummyOfflinePage(int id) {
return ntp_snippets::test::CreateDummyOfflinePageItem(
id, offline_pages::kAsyncNamespace);
}
std::vector<OfflinePageItem> CreateDummyOfflinePages(
const std::vector<int>& ids) {
std::vector<OfflinePageItem> result;
for (int id : ids) {
result.push_back(CreateDummyOfflinePage(id));
}
return result;
}
std::unique_ptr<FakeDownloadItem> CreateDummyAssetDownload(int id) {
std::unique_ptr<FakeDownloadItem> item = std::make_unique<FakeDownloadItem>();
item->SetId(id);
std::string id_string = base::IntToString(id);
item->SetGuid("XYZ-100032-EFZBDF-13323-PXZ" + id_string);
item->SetTargetFilePath(
base::FilePath::FromUTF8Unsafe("folder/file" + id_string + ".mhtml"));
item->SetURL(GURL("http://download.com/redirected" + id_string));
item->SetOriginalUrl(GURL("http://download.com/" + id_string));
item->SetStartTime(base::Time::Now());
item->SetFileExternallyRemoved(false);
item->SetState(DownloadItem::DownloadState::COMPLETE);
item->SetMimeType("application/pdf");
return item;
}
std::unique_ptr<FakeDownloadItem> CreateDummyAssetDownload(
int id,
const base::Time& start_time) {
std::unique_ptr<FakeDownloadItem> item = CreateDummyAssetDownload(id);
item->SetStartTime(start_time);
return item;
}
std::vector<std::unique_ptr<FakeDownloadItem>> CreateDummyAssetDownloads(
const std::vector<int>& ids) {
std::vector<std::unique_ptr<FakeDownloadItem>> result;
// The time is set to enforce the provider to cache the first items in the
// list first.
base::Time current_time = base::Time::Now();
for (int id : ids) {
result.push_back(CreateDummyAssetDownload(id, current_time));
current_time -= base::TimeDelta::FromMinutes(1);
}
return result;
}
class ObservedMockDownloadManager : public MockDownloadManager {
public:
ObservedMockDownloadManager() {}
~ObservedMockDownloadManager() override {
for (auto& observer : observers_) {
observer.ManagerGoingDown(this);
}
}
// Observer accessors.
void AddObserver(Observer* observer) override {
observers_.AddObserver(observer);
}
void RemoveObserver(Observer* observer) override {
observers_.RemoveObserver(observer);
}
void NotifyDownloadCreated(DownloadItem* item) {
for (auto& observer : observers_) {
observer.OnDownloadCreated(this, item);
}
}
std::vector<std::unique_ptr<FakeDownloadItem>>* mutable_items() {
return &items_;
}
const std::vector<std::unique_ptr<FakeDownloadItem>>& items() const {
return items_;
}
void GetAllDownloads(std::vector<DownloadItem*>* all_downloads) override {
all_downloads->clear();
for (const auto& item : items_) {
all_downloads->push_back(item.get());
}
}
private:
base::ObserverList<Observer>::Unchecked observers_;
std::vector<std::unique_ptr<FakeDownloadItem>> items_;
};
class DummyHistoryAdapter : public DownloadHistory::HistoryAdapter {
public:
DummyHistoryAdapter() : HistoryAdapter(nullptr) {}
void QueryDownloads(
const history::HistoryService::DownloadQueryCallback& callback) override {
}
void CreateDownload(const history::DownloadRow& info,
const history::HistoryService::DownloadCreateCallback&
callback) override {}
void UpdateDownload(const history::DownloadRow& data,
bool should_commit_immediately) override {}
void RemoveDownloads(const std::set<uint32_t>& ids) override {}
};
} // namespace
class DownloadSuggestionsProviderTest : public testing::Test {
public:
DownloadSuggestionsProviderTest()
: download_history_(&downloads_manager_for_history_,
std::make_unique<DummyHistoryAdapter>()),
pref_service_(new TestingPrefServiceSimple()) {
DownloadSuggestionsProvider::RegisterProfilePrefs(
pref_service()->registry());
}
void IgnoreOnCategoryStatusChangedToAvailable() {
EXPECT_CALL(observer_, OnCategoryStatusChanged(_, downloads_category(),
CategoryStatus::AVAILABLE))
.Times(AnyNumber());
EXPECT_CALL(observer_,
OnCategoryStatusChanged(_, downloads_category(),
CategoryStatus::AVAILABLE_LOADING))
.Times(AnyNumber());
}
void IgnoreOnSuggestionInvalidated() {
EXPECT_CALL(observer_, OnSuggestionInvalidated(_, _)).Times(AnyNumber());
}
DownloadSuggestionsProvider* CreateLoadedProvider(bool show_assets,
bool show_offline_pages,
base::Clock* clock) {
CreateProvider(show_assets, show_offline_pages, clock);
FireHistoryQueryComplete();
return provider_.get();
}
DownloadSuggestionsProvider* CreateProvider(bool show_assets,
bool show_offline_pages,
base::Clock* clock) {
DCHECK(!provider_);
DCHECK(show_assets || show_offline_pages);
// TODO(crbug.com/681766): Extract DownloadHistory interface and move
// implementation into DownloadHistoryImpl. Then mock it.
provider_ = std::make_unique<DownloadSuggestionsProvider>(
&observer_, show_offline_pages ? &offline_pages_model_ : nullptr,
show_assets ? &downloads_manager_ : nullptr, &download_history_,
pref_service(), clock);
return provider_.get();
}
void DestroyProvider() { provider_.reset(); }
Category downloads_category() {
return Category::FromKnownCategory(
ntp_snippets::KnownCategories::DOWNLOADS);
}
void FireOfflinePageModelLoaded() {
DCHECK(provider_);
provider_->OfflinePageModelLoaded(&offline_pages_model_);
}
void AddOfflinePage(const offline_pages::OfflinePageItem& added_page) {
DCHECK(provider_);
offline_pages_model_.mutable_items()->push_back(added_page);
provider_->OfflinePageAdded(&offline_pages_model_, added_page);
}
void FireOfflinePageDeleted(const OfflinePageItem& item) {
DCHECK(provider_);
offline_pages::OfflinePageModel::DeletedPageInfo info(
item.offline_id, item.system_download_id, item.client_id,
item.request_origin, item.url);
provider_->OfflinePageDeleted(info);
}
void FireDownloadCreated(DownloadItem* item) {
DCHECK(provider_);
downloads_manager_.NotifyDownloadCreated(item);
}
void FireDownloadsCreated(
const std::vector<std::unique_ptr<FakeDownloadItem>>& items) {
for (const auto& item : items) {
FireDownloadCreated(item.get());
}
}
void FireHistoryQueryComplete() { provider_->OnHistoryQueryComplete(); }
ContentSuggestion::ID GetDummySuggestionId(int id, bool is_offline_page) {
return ContentSuggestion::ID(
downloads_category(),
(is_offline_page ? "O" : "D") + base::IntToString(id));
}
std::vector<ContentSuggestion> GetDismissedSuggestions() {
std::vector<ContentSuggestion> dismissed_suggestions;
// This works synchronously because both fake data sources were designed so.
provider()->GetDismissedSuggestionsForDebugging(
downloads_category(),
base::Bind(&CaptureDismissedSuggestions, &dismissed_suggestions));
return dismissed_suggestions;
}
ContentSuggestionsProvider* provider() {
DCHECK(provider_);
return provider_.get();
}
ObservedMockDownloadManager* downloads_manager() {
return &downloads_manager_;
}
FakeOfflinePageModel* offline_pages_model() { return &offline_pages_model_; }
MockContentSuggestionsProviderObserver* observer() { return &observer_; }
TestingPrefServiceSimple* pref_service() { return pref_service_.get(); }
private:
// DownloadHistory requires UI thread.
content::TestBrowserThreadBundle thread_bundle_;
// We do not use DownloadHistory functionality in the tests, so we provide an
// empty manager to ensure no notifications, so that it does not intervene.
ObservedMockDownloadManager downloads_manager_for_history_;
DownloadHistory download_history_;
ObservedMockDownloadManager downloads_manager_;
FakeOfflinePageModel offline_pages_model_;
StrictMock<MockContentSuggestionsProviderObserver> observer_;
std::unique_ptr<TestingPrefServiceSimple> pref_service_;
// Last so that the dependencies are deleted after the provider.
std::unique_ptr<DownloadSuggestionsProvider> provider_;
DISALLOW_COPY_AND_ASSIGN(DownloadSuggestionsProviderTest);
};
TEST_F(DownloadSuggestionsProviderTest,
ShouldConvertOfflinePagesToSuggestions) {
IgnoreOnCategoryStatusChangedToAvailable();
*(offline_pages_model()->mutable_items()) = CreateDummyOfflinePages({1, 2});
EXPECT_CALL(*observer(),
OnNewSuggestions(
_, downloads_category(),
UnorderedElementsAre(AllOf(HasUrl("http://dummy.com/1"),
HasDownloadSuggestionExtra(
/*is_download_asset=*/false,
FILE_PATH_LITERAL(""), "")),
AllOf(HasUrl("http://dummy.com/2"),
HasDownloadSuggestionExtra(
/*is_download_asset=*/false,
FILE_PATH_LITERAL(""), "")))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
}
TEST_F(DownloadSuggestionsProviderTest,
ShouldConvertDownloadItemsToSuggestions) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
EXPECT_CALL(*observer(),
OnNewSuggestions(_, downloads_category(), SizeIs(0)));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
std::vector<std::unique_ptr<FakeDownloadItem>> asset_downloads =
CreateDummyAssetDownloads({1, 2});
EXPECT_CALL(*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(AllOf(
HasUrl("http://download.com/1"),
HasDownloadSuggestionExtra(
/*is_download_asset=*/true,
FILE_PATH_LITERAL("folder/file1.mhtml"),
"application/pdf")))));
FireDownloadCreated(asset_downloads[0].get());
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(
AllOf(HasUrl("http://download.com/1"),
HasDownloadSuggestionExtra(
/*is_download_asset=*/true,
FILE_PATH_LITERAL("folder/file1.mhtml"),
"application/pdf")),
AllOf(HasUrl("http://download.com/2"),
HasDownloadSuggestionExtra(
/*is_download_asset=*/true,
FILE_PATH_LITERAL("folder/file2.mhtml"),
"application/pdf")))));
FireDownloadCreated(asset_downloads[1].get());
}
TEST_F(DownloadSuggestionsProviderTest, ShouldMixInBothSources) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
*(offline_pages_model()->mutable_items()) = CreateDummyOfflinePages({1, 2});
EXPECT_CALL(*observer(), OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(
HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
std::vector<std::unique_ptr<FakeDownloadItem>> asset_downloads =
CreateDummyAssetDownloads({1, 2});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"),
HasUrl("http://download.com/1"))));
FireDownloadCreated(asset_downloads[0].get());
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"),
HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"))));
FireDownloadCreated(asset_downloads[1].get());
}
TEST_F(DownloadSuggestionsProviderTest, ShouldSortSuggestions) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
std::vector<OfflinePageItem> offline_pages = CreateDummyOfflinePages({0, 1});
offline_pages[0].url = GURL("http://dummy.com/0");
offline_pages[0].creation_time = now - base::TimeDelta::FromMinutes(10);
offline_pages[0].last_access_time = offline_pages[0].creation_time;
offline_pages[1].url = GURL("http://dummy.com/1");
offline_pages[1].creation_time = now - base::TimeDelta::FromMinutes(5);
offline_pages[1].last_access_time = offline_pages[1].creation_time;
*(offline_pages_model()->mutable_items()) = offline_pages;
base::SimpleTestClock test_clock;
test_clock.SetNow(now);
EXPECT_CALL(*observer(),
OnNewSuggestions(_, downloads_category(),
ElementsAre(HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/0"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
&test_clock);
std::vector<std::unique_ptr<FakeDownloadItem>> asset_downloads =
CreateDummyAssetDownloads({2, 3});
asset_downloads[0]->SetURL(GURL("http://download.com/2"));
asset_downloads[0]->SetStartTime(now - base::TimeDelta::FromMinutes(3));
asset_downloads[1]->SetURL(GURL("http://download.com/3"));
asset_downloads[1]->SetStartTime(now - base::TimeDelta::FromMinutes(7));
EXPECT_CALL(*observer(),
OnNewSuggestions(_, downloads_category(),
ElementsAre(HasUrl("http://download.com/2"),
HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/0"))));
FireDownloadCreated(asset_downloads[0].get());
EXPECT_CALL(*observer(),
OnNewSuggestions(_, downloads_category(),
ElementsAre(HasUrl("http://download.com/2"),
HasUrl("http://dummy.com/1"),
HasUrl("http://download.com/3"),
HasUrl("http://dummy.com/0"))));
FireDownloadCreated(asset_downloads[1].get());
}
TEST_F(DownloadSuggestionsProviderTest,
ShouldDismissWithoutNotifyingObservers) {
IgnoreOnCategoryStatusChangedToAvailable();
*(offline_pages_model()->mutable_items()) = CreateDummyOfflinePages({1, 2});
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1, 2});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"),
HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(0);
EXPECT_CALL(*observer(), OnSuggestionInvalidated(_, _)).Times(0);
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/true));
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/false));
// |downloads_manager_| is destroyed after the |provider_|, so the provider
// will not observe download items being destroyed.
}
TEST_F(DownloadSuggestionsProviderTest,
ShouldNotReportDismissedSuggestionsOnNewData) {
IgnoreOnCategoryStatusChangedToAvailable();
*(offline_pages_model()->mutable_items()) = CreateDummyOfflinePages({1, 2});
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1, 2});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"),
HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/true));
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/false));
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/2"),
HasUrl("http://dummy.com/3"),
HasUrl("http://download.com/2"))));
AddOfflinePage(CreateDummyOfflinePage(3));
}
TEST_F(DownloadSuggestionsProviderTest, ShouldReturnDismissedSuggestions) {
IgnoreOnCategoryStatusChangedToAvailable();
*(offline_pages_model()->mutable_items()) = CreateDummyOfflinePages({1, 2});
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1, 2});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"),
HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/true));
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/false));
EXPECT_THAT(GetDismissedSuggestions(),
UnorderedElementsAre(HasUrl("http://dummy.com/1"),
HasUrl("http://download.com/1")));
}
TEST_F(DownloadSuggestionsProviderTest, ShouldClearDismissedSuggestions) {
IgnoreOnCategoryStatusChangedToAvailable();
*(offline_pages_model()->mutable_items()) = CreateDummyOfflinePages({1, 2});
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1, 2});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"),
HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/true));
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/false));
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"),
HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"))));
provider()->ClearDismissedSuggestionsForDebugging(downloads_category());
EXPECT_THAT(GetDismissedSuggestions(), IsEmpty());
}
TEST_F(DownloadSuggestionsProviderTest,
ShouldNotDismissOtherTypeWithTheSameID) {
IgnoreOnCategoryStatusChangedToAvailable();
*(offline_pages_model()->mutable_items()) = CreateDummyOfflinePages({1, 2});
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1, 2});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"),
HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/true));
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/2"),
HasUrl("http://dummy.com/3"),
HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"))));
AddOfflinePage(CreateDummyOfflinePage(3));
}
TEST_F(DownloadSuggestionsProviderTest, ShouldReplaceDismissedItemWithNewData) {
IgnoreOnCategoryStatusChangedToAvailable();
// Currently the provider stores five items in its internal cache, so six
// items are needed to check whether all downloads are fetched on dismissal.
*(downloads_manager()->mutable_items()) =
CreateDummyAssetDownloads({1, 2, 3, 4, 5, 6});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"),
HasUrl("http://download.com/3"),
HasUrl("http://download.com/4"),
HasUrl("http://download.com/5"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/false));
provider()->DismissSuggestion(
GetDummySuggestionId(2, /*is_offline_page=*/false));
// The provider is not notified about the 6th item, however, it must report
// it now.
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/1"),
HasUrl("http://download.com/3"),
HasUrl("http://download.com/4"),
HasUrl("http://download.com/5"),
HasUrl("http://download.com/6"))));
AddOfflinePage(CreateDummyOfflinePage(1));
}
TEST_F(DownloadSuggestionsProviderTest,
ShouldInvalidateWhenUnderlyingItemDeleted) {
IgnoreOnCategoryStatusChangedToAvailable();
*(offline_pages_model()->mutable_items()) = CreateDummyOfflinePages({1, 2});
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"),
HasUrl("http://download.com/1"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
// We add another item manually, so that when it gets deleted it is not
// present in DownloadsManager list.
std::unique_ptr<FakeDownloadItem> removed_item = CreateDummyAssetDownload(2);
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"),
HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"))));
FireDownloadCreated(removed_item.get());
EXPECT_CALL(*observer(),
OnSuggestionInvalidated(
_, GetDummySuggestionId(1, /*is_offline_page=*/true)));
FireOfflinePageDeleted(offline_pages_model()->items()[0]);
EXPECT_CALL(*observer(),
OnSuggestionInvalidated(
_, GetDummySuggestionId(2, /*is_offline_page=*/false)));
// |OnDownloadItemDestroyed| is called from |removed_item|'s destructor.
removed_item.reset();
}
TEST_F(DownloadSuggestionsProviderTest, ShouldReplaceRemovedItemWithNewData) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
*(downloads_manager()->mutable_items()) =
CreateDummyAssetDownloads({1, 2, 3, 4, 5});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"),
HasUrl("http://download.com/3"),
HasUrl("http://download.com/4"),
HasUrl("http://download.com/5"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
// Note that |CreateDummyAssetDownloads| creates items "downloaded" before
// |base::Time::Now()|, so for a new item the time is set in future to enforce
// the provider to show the new item.
std::unique_ptr<FakeDownloadItem> removed_item = CreateDummyAssetDownload(
100, base::Time::Now() + base::TimeDelta::FromDays(1));
EXPECT_CALL(
*observer(),
OnNewSuggestions(
_, downloads_category(),
UnorderedElementsAre(
HasUrl("http://download.com/1"), HasUrl("http://download.com/2"),
HasUrl("http://download.com/3"), HasUrl("http://download.com/4"),
HasUrl("http://download.com/100"))));
FireDownloadCreated(removed_item.get());
// |OnDownloadDestroyed| notification is called in |DownloadItem|'s
// destructor.
removed_item.reset();
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/6"),
HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"),
HasUrl("http://download.com/3"),
HasUrl("http://download.com/4"))));
AddOfflinePage(CreateDummyOfflinePage(6));
}
TEST_F(DownloadSuggestionsProviderTest, ShouldPruneOfflinePagesDismissedIDs) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
auto offline_pages = CreateDummyOfflinePages({1, 2, 3});
*(offline_pages_model()->mutable_items()) = offline_pages;
EXPECT_CALL(*observer(), OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(
HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"),
HasUrl("http://dummy.com/3"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/true));
provider()->DismissSuggestion(
GetDummySuggestionId(2, /*is_offline_page=*/true));
provider()->DismissSuggestion(
GetDummySuggestionId(3, /*is_offline_page=*/true));
EXPECT_THAT(GetDismissedSuggestions(), SizeIs(3));
// Note that the first suggestion is not removed from |offline_pages_model|
// storage, because otherwise |GetDismissedSuggestions| cannot return it.
FireOfflinePageDeleted(offline_pages[0]);
EXPECT_THAT(GetDismissedSuggestions(), SizeIs(2));
// Prune when offline page is deleted.
FireOfflinePageDeleted(offline_pages_model()->items()[1]);
EXPECT_THAT(GetDismissedSuggestions(), SizeIs(1));
}
TEST_F(DownloadSuggestionsProviderTest, ShouldPruneAssetDownloadsDismissedIDs) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1, 2});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/false));
provider()->DismissSuggestion(
GetDummySuggestionId(2, /*is_offline_page=*/false));
EXPECT_THAT(GetDismissedSuggestions(), SizeIs(2));
downloads_manager()->items()[0]->NotifyDownloadDestroyed();
EXPECT_THAT(GetDismissedSuggestions(), SizeIs(1));
}
TEST_F(DownloadSuggestionsProviderTest, ShouldFetchAssetDownloadsOnStartup) {
IgnoreOnCategoryStatusChangedToAvailable();
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1, 2});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
}
TEST_F(DownloadSuggestionsProviderTest,
ShouldFetchOfflinePageDownloadsOnStartup) {
IgnoreOnCategoryStatusChangedToAvailable();
*(offline_pages_model()->mutable_items()) = CreateDummyOfflinePages({1, 2});
EXPECT_CALL(*observer(), OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(
HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"))));
CreateProvider(/*show_assets=*/false, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
FireOfflinePageModelLoaded();
}
TEST_F(DownloadSuggestionsProviderTest,
ShouldFetchAssetDownloadsOnHistoryQueryComplete) {
IgnoreOnCategoryStatusChangedToAvailable();
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(0);
CreateProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1, 2});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"))));
FireHistoryQueryComplete();
}
TEST_F(DownloadSuggestionsProviderTest,
ShouldInvalidateAssetDownloadWhenItsFileRemoved) {
IgnoreOnCategoryStatusChangedToAvailable();
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://download.com/1"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
EXPECT_CALL(*observer(),
OnSuggestionInvalidated(
_, GetDummySuggestionId(1, /*is_offline_page=*/false)));
(*downloads_manager()->mutable_items())[0]->SetFileExternallyRemoved(true);
(*downloads_manager()->mutable_items())[0]->NotifyDownloadUpdated();
}
TEST_F(DownloadSuggestionsProviderTest,
ShouldNotShowOfflinePagesWhenTurnedOff) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
*(offline_pages_model()->mutable_items()) = CreateDummyOfflinePages({1, 2});
EXPECT_CALL(*observer(),
OnNewSuggestions(_, downloads_category(), IsEmpty()));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/false,
base::DefaultClock::GetInstance());
std::vector<std::unique_ptr<FakeDownloadItem>> asset_downloads =
CreateDummyAssetDownloads({1});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://download.com/1"))));
FireDownloadCreated(asset_downloads[0].get());
// TODO(vitaliii): Notify the provider that an offline page has been updated.
}
TEST_F(DownloadSuggestionsProviderTest, ShouldNotShowAssetsWhenTurnedOff) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
*(offline_pages_model()->mutable_items()) = CreateDummyOfflinePages({1, 2});
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1, 2});
EXPECT_CALL(*observer(), OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(
HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"))));
CreateProvider(/*show_assets=*/false, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
downloads_manager()->NotifyDownloadCreated(
downloads_manager()->items()[0].get());
// This notification should not reach the provider, because the asset
// downloads data source is not provided. If it is and the provider reacts to
// the notification, the test will fail because the observer is a strict mock.
(*downloads_manager()->mutable_items())[0]->NotifyDownloadUpdated();
}
TEST_F(DownloadSuggestionsProviderTest,
ShouldLoadAndSubmitMissedAssetsEvenIfOfflinePagesAreTurnedOff) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1, 2});
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://download.com/1"),
HasUrl("http://download.com/2"))));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/false,
base::DefaultClock::GetInstance());
}
TEST_F(DownloadSuggestionsProviderTest,
ShouldLoadAndSubmitOfflinePagesEvenIfAssetDownloadsAreTurnedOff) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
*(offline_pages_model()->mutable_items()) = CreateDummyOfflinePages({1, 2});
EXPECT_CALL(*observer(), OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(
HasUrl("http://dummy.com/1"),
HasUrl("http://dummy.com/2"))));
CreateProvider(/*show_assets=*/false, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
}
TEST_F(DownloadSuggestionsProviderTest, ShouldStoreDismissedSuggestions) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
// Dismiss items to store them in the list of dismissed items.
*(offline_pages_model()->mutable_items()) = CreateDummyOfflinePages({1});
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1});
EXPECT_CALL(*observer(), OnNewSuggestions(_, downloads_category(), _));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/true));
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/false));
// Destroy and create provider to simulate turning off Chrome.
DestroyProvider();
EXPECT_CALL(*observer(), OnNewSuggestions(_, downloads_category(), _));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
EXPECT_THAT(GetDismissedSuggestions(),
UnorderedElementsAre(HasUrl("http://dummy.com/1"),
HasUrl("http://download.com/1")));
}
TEST_F(DownloadSuggestionsProviderTest,
ShouldNotPruneDismissedAssetDownloadsBeforeHistoryQueryComplete) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
// Dismiss items to store them in the list of dismissed items.
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1});
EXPECT_CALL(*observer(), OnNewSuggestions(_, downloads_category(), _));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/false,
base::DefaultClock::GetInstance());
provider()->DismissSuggestion(
GetDummySuggestionId(1, /*is_offline_page=*/false));
ASSERT_THAT(GetDismissedSuggestions(),
UnorderedElementsAre(HasUrl("http://download.com/1")));
// Destroy and create provider to simulate turning off Chrome.
DestroyProvider();
downloads_manager()->mutable_items()->clear();
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(0);
CreateProvider(/*show_assets=*/true, /*show_offline_pages=*/false,
base::DefaultClock::GetInstance());
// Dismissed IDs should not be pruned yet, because the downloads list at the
// manager is not complete.
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1});
EXPECT_THAT(GetDismissedSuggestions(),
UnorderedElementsAre(HasUrl("http://download.com/1")));
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _));
downloads_manager()->mutable_items()->clear();
FireHistoryQueryComplete();
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1});
// Once the manager has been loaded, the ids should be pruned.
EXPECT_THAT(GetDismissedSuggestions(), IsEmpty());
}
TEST_F(DownloadSuggestionsProviderTest, ShouldNotShowOutdatedDownloads) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
*(offline_pages_model()->mutable_items()) = CreateDummyOfflinePages({0, 1});
offline_pages_model()->mutable_items()->at(0).url =
GURL("http://dummy.com/0");
offline_pages_model()->mutable_items()->at(0).creation_time =
kNotOutdatedTime;
offline_pages_model()->mutable_items()->at(0).last_access_time =
kNotOutdatedTime;
offline_pages_model()->mutable_items()->at(1).url =
GURL("http://dummy.com/1");
offline_pages_model()->mutable_items()->at(1).creation_time = kOutdatedTime;
offline_pages_model()->mutable_items()->at(1).last_access_time =
kOutdatedTime;
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({0, 1});
downloads_manager()->mutable_items()->at(0)->SetURL(
GURL("http://download.com/0"));
downloads_manager()->mutable_items()->at(0)->SetStartTime(kNotOutdatedTime);
downloads_manager()->mutable_items()->at(1)->SetURL(
GURL("http://download.com/1"));
downloads_manager()->mutable_items()->at(1)->SetStartTime(kOutdatedTime);
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/0"),
HasUrl("http://download.com/0"))));
base::SimpleTestClock test_clock;
test_clock.SetNow(now);
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
&test_clock);
}
TEST_F(DownloadSuggestionsProviderTest,
ShouldShowRecentlyVisitedOfflinePageDownloads) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
std::vector<OfflinePageItem> offline_pages = CreateDummyOfflinePages({0, 1});
offline_pages[0].url = GURL("http://dummy.com/0");
offline_pages[0].creation_time = kOutdatedTime;
offline_pages[0].last_access_time = kNotOutdatedTime;
offline_pages[1].url = GURL("http://dummy.com/1");
offline_pages[1].creation_time = kOutdatedTime;
offline_pages[1].last_access_time = offline_pages[1].creation_time;
*(offline_pages_model()->mutable_items()) = offline_pages;
// Even though page 0 was created long time ago, it should be reported because
// it has been visited recently.
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/0"))));
base::SimpleTestClock test_clock;
test_clock.SetNow(now);
CreateProvider(/*show_assets=*/false, /*show_offline_pages=*/true,
&test_clock);
}
TEST_F(DownloadSuggestionsProviderTest,
ShouldShowRecentlyVisitedAssetDownloads) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({0, 1});
downloads_manager()->mutable_items()->at(0)->SetURL(
GURL("http://download.com/0"));
downloads_manager()->mutable_items()->at(0)->SetStartTime(kOutdatedTime);
downloads_manager()->mutable_items()->at(0)->SetLastAccessTime(
kNotOutdatedTime);
downloads_manager()->mutable_items()->at(1)->SetURL(
GURL("http://download.com/1"));
downloads_manager()->mutable_items()->at(1)->SetStartTime(kOutdatedTime);
downloads_manager()->mutable_items()->at(1)->SetLastAccessTime(
downloads_manager()->mutable_items()->at(1)->GetStartTime());
// Even though asset download 0 was downloaded long time ago, it should be
// reported because it has been visited recently.
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://download.com/0"))));
base::SimpleTestClock test_clock;
test_clock.SetNow(now);
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/false,
&test_clock);
}
TEST_F(DownloadSuggestionsProviderTest, ShouldIgnoreTransientDownloads) {
IgnoreOnCategoryStatusChangedToAvailable();
*(downloads_manager()->mutable_items()) = CreateDummyAssetDownloads({1});
downloads_manager()->mutable_items()->at(0)->SetIsTransient(true);
EXPECT_CALL(*observer(),
OnNewSuggestions(_, downloads_category(), IsEmpty()));
CreateLoadedProvider(/*show_assets=*/true, /*show_offline_pages=*/true,
base::DefaultClock::GetInstance());
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(0);
EXPECT_CALL(*observer(), OnSuggestionInvalidated(_, _)).Times(0);
// |OnDownloadItemDestroyed| is called from items's destructor.
downloads_manager()->mutable_items()->clear();
}
TEST_F(DownloadSuggestionsProviderTest, ShouldNotShowSuggestedDownloads) {
IgnoreOnCategoryStatusChangedToAvailable();
IgnoreOnSuggestionInvalidated();
std::vector<OfflinePageItem> offline_pages = CreateDummyOfflinePages({0, 1});
offline_pages[0].url = GURL("http://dummy.com/0");
offline_pages[0].creation_time = kOutdatedTime;
offline_pages[0].last_access_time = kNotOutdatedTime;
// Suggested page should be ignored.
offline_pages[1].client_id.name_space = "suggested_articles";
offline_pages[1].url = GURL("http://dummy.com/1");
offline_pages[1].creation_time = kNotOutdatedTime;
offline_pages[1].last_access_time = offline_pages[1].creation_time;
*(offline_pages_model()->mutable_items()) = offline_pages;
// Even though page 0 was created long time ago, it should be reported because
// it has been visited recently.
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, downloads_category(),
UnorderedElementsAre(HasUrl("http://dummy.com/0"))));
base::SimpleTestClock test_clock;
test_clock.SetNow(now);
CreateProvider(/*show_assets=*/false, /*show_offline_pages=*/true,
&test_clock);
}