blob: 936c293cec6fa3ed7c2b29545d65b848360aad11 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/download/bubble/download_bubble_update_service.h"
#include <memory>
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/download/bubble/download_bubble_display_info.h"
#include "chrome/browser/download/bubble/download_bubble_update_service_factory.h"
#include "chrome/browser/download/chrome_download_manager_delegate.h"
#include "chrome/browser/download/download_core_service.h"
#include "chrome/browser/download/download_core_service_factory.h"
#include "chrome/browser/download/download_core_service_impl.h"
#include "chrome/browser/download/download_item_model.h"
#include "chrome/browser/download/download_item_web_app_data.h"
#include "chrome/browser/download/download_ui_model.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/offline_items_collection/offline_content_aggregator_factory.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/download/public/common/download_item.h"
#include "components/download/public/common/mock_download_item.h"
#include "components/keyed_service/core/service_access_type.h"
#include "components/offline_items_collection/core/offline_item.h"
#include "components/offline_items_collection/core/offline_item_state.h"
#include "components/offline_items_collection/core/test_support/mock_offline_content_provider.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/mock_download_manager.h"
#include "content/public/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
using ::download::DownloadDangerType;
using ::offline_items_collection::OfflineItem;
using ::offline_items_collection::OfflineItemState;
using ::testing::_;
using ::testing::AllOf;
using ::testing::IsEmpty;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::ReturnRefOfCopy;
using ::testing::UnorderedElementsAre;
using ::testing::WithArg;
using Alert = DownloadBubbleAccessibleAlertsMap::Alert;
using DownloadState = download::DownloadItem::DownloadState;
using DownloadUIModelPtrVector =
std::vector<DownloadUIModel::DownloadUIModelPtr>;
using NiceMockDownloadItem = NiceMock<download::MockDownloadItem>;
constexpr char kProfileName[] = "testing_profile";
constexpr char kProviderNamespace[] = "mock_namespace";
// Helper for MockDownloadManager to properly mimic GetAllDownloads(), such that
// it appends all items in |items| to the argument of the action, which should
// be a std::vector<DownloadItem*>.
ACTION_P(AddDownloadItems, items) {
for (download::DownloadItem* item : items) {
arg0->push_back(item);
}
}
// Matcher that works with substrings of u16string.
MATCHER_P(HasSubstr16, substring, "") {
return arg.find(substring) != std::u16string::npos;
}
void RemoveDownloadItemsObserver(
std::vector<std::unique_ptr<NiceMockDownloadItem>>& download_items,
download::DownloadItem::Observer& observer) {
for (auto& item : download_items) {
item->RemoveObserver(&observer);
}
}
std::unique_ptr<KeyedService> SetUpDownloadCoreService(
content::DownloadManager* download_manager,
content::BrowserContext* context) {
Profile* profile = static_cast<Profile*>(context);
auto dcs = std::make_unique<DownloadCoreServiceImpl>(profile);
auto cdmd = std::make_unique<ChromeDownloadManagerDelegate>(profile);
cdmd->SetDownloadManager(download_manager);
dcs->SetDownloadManagerDelegateForTesting(std::move(cdmd));
auto download_history = std::make_unique<DownloadHistory>(
download_manager, std::make_unique<DownloadHistory::HistoryAdapter>(
HistoryServiceFactory::GetInstance()->GetForProfile(
profile, ServiceAccessType::EXPLICIT_ACCESS)));
dcs->SetDownloadHistoryForTesting(std::move(download_history));
return dcs;
}
class DownloadBubbleUpdateServiceTest : public testing::Test {
public:
DownloadBubbleUpdateServiceTest()
: testing_profile_manager_(TestingBrowserProcess::GetGlobal()) {}
DownloadBubbleUpdateServiceTest(const DownloadBubbleUpdateServiceTest&) =
delete;
DownloadBubbleUpdateServiceTest& operator=(
const DownloadBubbleUpdateServiceTest&) = delete;
~DownloadBubbleUpdateServiceTest() override = default;
std::unique_ptr<NiceMock<content::MockDownloadManager>>
SetUpDownloadManager() {
auto download_manager =
std::make_unique<NiceMock<content::MockDownloadManager>>();
EXPECT_CALL(*download_manager, RemoveObserver(_)).WillRepeatedly(Return());
// Default case for when no download exists with the requested guid.
EXPECT_CALL(*download_manager, GetDownloadByGuid(_))
.WillRepeatedly(Return(nullptr));
return download_manager;
}
std::unique_ptr<KeyedService> CreateUpdateService(
DownloadBubbleUpdateService* preexisting_update_service,
content::BrowserContext* context) {
// Unregister the observer from the previous instance of the update service.
// See note below in TearDown() for why this is necessary.
if (preexisting_update_service) {
RemoveDownloadItemsObserver(
download_items_,
preexisting_update_service->download_item_notifier_for_testing());
}
auto service = std::make_unique<DownloadBubbleUpdateService>(
Profile::FromBrowserContext(context));
return service;
}
void InitializeUpdateService() {
update_service_->Initialize(download_manager_);
// Run the offline content provider initialization.
task_environment_.RunUntilIdle();
}
void SetUp() override {
ASSERT_TRUE(testing_profile_manager_.SetUp());
// The order of these initialization steps is important. The DownloadManager
// must be created and set on the DownloadCoreService before setting up the
// history service and calling DownloadBubbleUpdateService::Initialize.
// Otherwise GetDownloadHistory will cause Initialize() to be called a
// second time, violating invariants assumed by the Update Service.
std::unique_ptr<NiceMock<content::MockDownloadManager>> download_manager =
SetUpDownloadManager();
download_manager_ = download_manager.get();
TestingProfile::TestingFactories testing_factories;
testing_factories.emplace_back(HistoryServiceFactory::GetInstance(),
HistoryServiceFactory::GetDefaultFactory());
testing_factories.emplace_back(
DownloadCoreServiceFactory::GetInstance(),
base::BindRepeating(&SetUpDownloadCoreService, download_manager_));
profile_ = testing_profile_manager_.CreateTestingProfile(
kProfileName, std::move(testing_factories));
profile_->SetDownloadManagerForTesting(std::move(download_manager));
EXPECT_CALL(*download_manager_, GetBrowserContext())
.WillRepeatedly(Return(profile_));
download_core_service_ =
DownloadCoreServiceFactory::GetInstance()->GetForBrowserContext(
profile_);
offline_content_provider_ = std::make_unique<
NiceMock<offline_items_collection::MockOfflineContentProvider>>();
OfflineContentAggregatorFactory::GetForKey(profile_->GetProfileKey())
->RegisterProvider(kProviderNamespace, offline_content_provider_.get());
update_service_ = static_cast<DownloadBubbleUpdateService*>(
DownloadBubbleUpdateServiceFactory::GetInstance()
->SetTestingFactoryAndUse(
profile_,
base::BindRepeating(
&DownloadBubbleUpdateServiceTest::CreateUpdateService,
base::Unretained(this), update_service_)));
InitializeUpdateService();
}
void TearDown() override {
// This would normally be done in the Notifier's destructor, but this
// notifier doesn't know what items it's observing because we have manually
// added it as an observer on each item, since the MockDownloadManager does
// not actually call OnDownloadCreated on it.
if (update_service_) {
RemoveDownloadItemsObserver(
download_items_,
update_service_->download_item_notifier_for_testing());
}
// This must be shut down before the DCS is destroyed.
download_core_service_->GetDownloadManagerDelegate()->Shutdown();
offline_content_provider_.reset();
update_service_ = nullptr;
download_manager_ = nullptr;
download_core_service_ = nullptr;
profile_ = nullptr;
testing_profile_manager_.DeleteTestingProfile(kProfileName);
}
protected:
// TODO(chlily): Factor these out into a test utils file.
download::MockDownloadItem& GetDownloadItem(size_t index) {
return *download_items_[index];
}
// Forwards to the below version.
void InitDownloadItem(DownloadState state,
const std::string& guid,
bool is_paused,
base::Time start_time = base::Time::Now(),
const webapps::AppId* web_app_id = nullptr,
bool is_crx = false,
bool observe = true) {
InitDownloadItem(*download_manager_, *update_service_, download_items_,
profile_, state, guid, is_paused, start_time, web_app_id,
is_crx, observe);
}
void InitDownloadItem(
NiceMock<content::MockDownloadManager>& download_manager,
DownloadBubbleUpdateService& update_service,
std::vector<std::unique_ptr<NiceMockDownloadItem>>& download_items,
Profile* profile,
DownloadState state,
const std::string& guid,
bool is_paused,
base::Time start_time = base::Time::Now(),
const webapps::AppId* web_app_id = nullptr,
bool is_crx = false,
bool observe = true) {
size_t index = download_items.size();
download_items.push_back(std::make_unique<NiceMockDownloadItem>());
auto& item = *download_items[index];
EXPECT_CALL(item, GetId())
.WillRepeatedly(
Return(static_cast<uint32_t>(download_items.size() + 1)));
EXPECT_CALL(item, GetGuid()).WillRepeatedly(ReturnRefOfCopy(guid));
EXPECT_CALL(item, GetState()).WillRepeatedly(Return(state));
EXPECT_CALL(item, GetStartTime()).WillRepeatedly(Return(start_time));
if (state == DownloadState::COMPLETE) {
EXPECT_CALL(item, GetEndTime())
.WillRepeatedly(Return(start_time + base::Minutes(1)));
} else {
EXPECT_CALL(item, GetEndTime()).WillRepeatedly(Return(base::Time()));
}
base::FilePath::StringType filename;
#if BUILDFLAG(IS_WIN)
filename = base::UTF8ToWide(guid);
#else
filename = guid;
#endif
EXPECT_CALL(item, GetTargetFilePath())
.WillRepeatedly(ReturnRefOfCopy(base::FilePath(filename)));
EXPECT_CALL(item, GetFileNameToReportUser())
.WillRepeatedly(Return(base::FilePath(filename)));
int received_bytes =
state == download::DownloadItem::IN_PROGRESS ? 50 : 100;
EXPECT_CALL(item, GetReceivedBytes())
.WillRepeatedly(Return(received_bytes));
EXPECT_CALL(item, GetTotalBytes()).WillRepeatedly(Return(100));
EXPECT_CALL(item, IsDone()).WillRepeatedly(Return(false));
EXPECT_CALL(item, IsTransient()).WillRepeatedly(Return(false));
EXPECT_CALL(item, IsPaused()).WillRepeatedly(Return(is_paused));
EXPECT_CALL(item, GetTargetDisposition())
.WillRepeatedly(
Return(is_crx ? download::DownloadItem::TARGET_DISPOSITION_OVERWRITE
: download::DownloadItem::TARGET_DISPOSITION_PROMPT));
EXPECT_CALL(item, GetMimeType())
.WillRepeatedly(Return(is_crx ? "application/x-chrome-extension" : ""));
std::vector<download::DownloadItem*> items;
for (auto& download_item : download_items) {
items.push_back(download_item.get());
}
EXPECT_CALL(download_manager, GetAllDownloads(_))
.WillRepeatedly(WithArg<0>(AddDownloadItems(items)));
EXPECT_CALL(download_manager, GetDownloadByGuid(guid))
.WillRepeatedly(Return(&item));
content::DownloadItemUtils::AttachInfoForTesting(&item, profile, nullptr);
if (web_app_id != nullptr) {
DownloadItemWebAppData::CreateAndAttachToItem(&item, *web_app_id);
}
if (observe) {
item.AddObserver(&update_service.download_item_notifier_for_testing());
item.NotifyObserversDownloadUpdated();
}
}
void UpdateDownloadItem(
download::MockDownloadItem& item,
DownloadState state,
bool is_paused = false,
DownloadDangerType danger_type =
DownloadDangerType::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS) {
EXPECT_CALL(item, GetState()).WillRepeatedly(Return(state));
EXPECT_CALL(item, IsDone())
.WillRepeatedly(Return(state == DownloadState::COMPLETE));
EXPECT_CALL(item, IsDangerous())
.WillRepeatedly(
Return(danger_type !=
DownloadDangerType::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS));
EXPECT_CALL(item, GetDangerType()).WillRepeatedly(Return(danger_type));
EXPECT_CALL(item, IsPaused()).WillRepeatedly(Return(is_paused));
item.NotifyObserversDownloadUpdated();
}
// Overload of the above that identifies the item by index.
void UpdateDownloadItem(
int item_index,
DownloadState state,
bool is_paused = false,
DownloadDangerType danger_type =
DownloadDangerType::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS) {
DCHECK_GT(download_items_.size(), static_cast<size_t>(item_index));
auto& item = GetDownloadItem(item_index);
UpdateDownloadItem(item, state, is_paused, danger_type);
}
void RemoveDownloadItem(size_t item_index) {
download_items_[item_index]->NotifyObserversDownloadRemoved();
download_items_.erase(download_items_.begin() + item_index);
std::vector<download::DownloadItem*> items;
for (size_t i = 0; i < download_items_.size(); ++i) {
items.push_back(&GetDownloadItem(i));
}
EXPECT_CALL(*download_manager_, GetAllDownloads(_))
.WillRepeatedly(WithArg<0>(AddDownloadItems(items)));
}
void InitOfflineItems(const std::vector<OfflineItemState>& states,
const std::vector<std::string>& ids,
const std::vector<base::Time>& start_times) {
ASSERT_EQ(states.size(), ids.size());
ASSERT_EQ(states.size(), start_times.size());
std::vector<OfflineItem> new_items;
for (size_t i = 0; i < states.size(); ++i) {
OfflineItem item;
item.state = states[i];
item.id.id = ids[i];
item.title = ids[i];
item.creation_time = start_times[i];
if (states[i] != OfflineItemState::COMPLETE) {
item.time_remaining_ms = 1000 * 60;
item.progress.value = 50;
item.progress.max = 100;
item.progress.unit =
offline_items_collection::OfflineItemProgressUnit::PERCENTAGE;
}
new_items.push_back(item);
offline_items_.push_back(std::move(item));
}
offline_content_provider_->SetItems(new_items);
offline_content_provider_->NotifyOnItemsAdded(new_items);
}
void UpdateOfflineItem(int item_index, OfflineItemState state) {
offline_items_[item_index].state = state;
UpdateDelta delta;
delta.state_changed = true;
offline_content_provider_->NotifyOnItemUpdated(offline_items_[item_index],
delta);
}
content::BrowserTaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
raw_ptr<NiceMock<content::MockDownloadManager>, DanglingUntriaged>
download_manager_ = nullptr;
std::vector<std::unique_ptr<NiceMockDownloadItem>> download_items_;
std::vector<offline_items_collection::OfflineItem> offline_items_;
TestingProfileManager testing_profile_manager_;
raw_ptr<TestingProfile, DanglingUntriaged> profile_ = nullptr;
std::unique_ptr<
NiceMock<offline_items_collection::MockOfflineContentProvider>>
offline_content_provider_;
raw_ptr<DownloadBubbleUpdateService, DanglingUntriaged> update_service_ =
nullptr;
raw_ptr<DownloadCoreService> download_core_service_ = nullptr;
};
TEST_F(DownloadBubbleUpdateServiceTest, PopulatesCaches) {
base::Time now = base::Time::Now();
base::Time older_time = now - base::Hours(1);
InitDownloadItem(DownloadState::IN_PROGRESS, "in_progress_active_download",
/*is_paused=*/false, older_time);
InitDownloadItem(DownloadState::IN_PROGRESS, "in_progress_paused_download",
/*is_paused=*/true, now);
InitDownloadItem(DownloadState::COMPLETE, "completed_download",
/*is_paused=*/false, now);
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 3u);
EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_download");
EXPECT_EQ(models[1]->GetContentId().id, "in_progress_paused_download");
EXPECT_EQ(models[2]->GetContentId().id, "completed_download");
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
UnorderedElementsAre(HasSubstr16(u"in_progress_active_download"),
HasSubstr16(u"in_progress_paused_download"),
HasSubstr16(u"completed_download")));
// Recreate the update service to check that it pulls in the existing
// download items upon initialization.
auto service = CreateUpdateService(update_service_, profile_);
update_service_ = static_cast<DownloadBubbleUpdateService*>(service.get());
InitializeUpdateService();
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 3u);
EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_download");
EXPECT_EQ(models[1]->GetContentId().id, "in_progress_paused_download");
EXPECT_EQ(models[2]->GetContentId().id, "completed_download");
// Add some more (offline) items.
InitOfflineItems(
{OfflineItemState::IN_PROGRESS, OfflineItemState::PAUSED,
OfflineItemState::COMPLETE},
{"in_progress_active_offline_item", "in_progress_paused_offline_item",
"completed_offline_item"},
{now, older_time, older_time});
// All items are returned in sorted order.
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 6u);
EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_offline_item");
EXPECT_EQ(models[1]->GetContentId().id, "in_progress_active_download");
EXPECT_EQ(models[2]->GetContentId().id, "in_progress_paused_download");
EXPECT_EQ(models[3]->GetContentId().id, "in_progress_paused_offline_item");
EXPECT_EQ(models[4]->GetContentId().id, "completed_download");
EXPECT_EQ(models[5]->GetContentId().id, "completed_offline_item");
// Only have alerts for the new offline items, not the download items that
// were preexisting when this service instance was initialized.
EXPECT_THAT(
update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
UnorderedElementsAre(HasSubstr16(u"in_progress_active_offline_item"),
HasSubstr16(u"in_progress_paused_offline_item"),
HasSubstr16(u"completed_offline_item")));
// Manually clean up the second service instance to avoid UAF.
RemoveDownloadItemsObserver(
download_items_, update_service_->download_item_notifier_for_testing());
update_service_ = nullptr;
}
TEST_F(DownloadBubbleUpdateServiceTest, AddsNonCrxDownloadItems) {
InitDownloadItem(DownloadState::IN_PROGRESS, "new_download",
/*is_paused=*/false, base::Time::Now(),
/*web_app_id=*/nullptr, /*is_crx=*/false,
/*observe=*/false);
// Manually notify the service of the new download rather than going through
// the observer update notification in InitDownloadItem().
update_service_->OnDownloadCreated(download_manager_.get(),
&GetDownloadItem(0));
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 1u);
EXPECT_EQ(models[0]->GetContentId().id, "new_download");
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
UnorderedElementsAre(HasSubstr16(u"new_download")));
}
TEST_F(DownloadBubbleUpdateServiceTest, DelaysCrx) {
InitDownloadItem(DownloadState::IN_PROGRESS, "in_progress_crx",
/*is_paused=*/false, base::Time::Now(),
/*web_app_id=*/nullptr, /*is_crx=*/true,
/*observe=*/false);
// Manually notify the service of the new download rather than going through
// the observer update notification in InitDownloadItem().
update_service_->OnDownloadCreated(download_manager_, &GetDownloadItem(0));
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
// The crx download does not show up immediately.
EXPECT_EQ(models.size(), 0u);
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
IsEmpty());
// Updates are also withheld.
UpdateDownloadItem(0, DownloadState::IN_PROGRESS, /*is_paused=*/true);
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
EXPECT_EQ(models.size(), 0u);
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
IsEmpty());
task_environment_.FastForwardBy(base::Seconds(2));
// After the delay, the crx is added.
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 1u);
EXPECT_EQ(models[0]->GetContentId().id, "in_progress_crx");
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
UnorderedElementsAre(HasSubstr16(u"in_progress_crx")));
}
TEST_F(DownloadBubbleUpdateServiceTest, EvictsExcessItemsAndBackfills) {
update_service_->set_max_num_items_to_show_for_testing(3);
update_service_->set_extra_items_to_cache_for_testing(0);
base::Time now = base::Time::Now();
base::Time older_time = now - base::Hours(1);
InitDownloadItem(DownloadState::IN_PROGRESS, "in_progress_active_download",
/*is_paused=*/false, older_time);
InitDownloadItem(DownloadState::IN_PROGRESS, "in_progress_paused_download",
/*is_paused=*/true, now);
InitDownloadItem(DownloadState::COMPLETE, "completed_download_older",
/*is_paused=*/false, older_time);
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 3u);
EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_download");
EXPECT_EQ(models[1]->GetContentId().id, "in_progress_paused_download");
EXPECT_EQ(models[2]->GetContentId().id, "completed_download_older");
// Add another item. This sorts before the last item and should cause it to be
// evicted.
InitDownloadItem(DownloadState::COMPLETE, "completed_download_newer",
/*is_paused=*/false, now);
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 3u);
EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_download");
EXPECT_EQ(models[1]->GetContentId().id, "in_progress_paused_download");
EXPECT_EQ(models[2]->GetContentId().id, "completed_download_newer");
// Alerts for all the downloads are included.
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
UnorderedElementsAre(HasSubstr16(u"in_progress_active_download"),
HasSubstr16(u"in_progress_paused_download"),
HasSubstr16(u"completed_download_older"),
HasSubstr16(u"completed_download_newer")));
// Remove an item. The previously evicted item should come back via backfill.
RemoveDownloadItem(1);
task_environment_.RunUntilIdle();
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 3u);
EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_download");
EXPECT_EQ(models[1]->GetContentId().id, "completed_download_newer");
EXPECT_EQ(models[2]->GetContentId().id, "completed_download_older");
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
IsEmpty());
}
TEST_F(DownloadBubbleUpdateServiceTest, BackfillsOnUpdate) {
update_service_->set_max_num_items_to_show_for_testing(3);
update_service_->set_extra_items_to_cache_for_testing(0);
base::Time now = base::Time::Now();
base::Time older_time = now - base::Hours(1);
base::Time even_older_time = older_time - base::Hours(1);
base::Time oldest_time = even_older_time - base::Hours(1);
InitDownloadItem(DownloadState::IN_PROGRESS, "now",
/*is_paused=*/false, now);
InitDownloadItem(DownloadState::IN_PROGRESS, "older",
/*is_paused=*/false, older_time);
InitDownloadItem(DownloadState::IN_PROGRESS, "even_older",
/*is_paused=*/false, even_older_time);
InitDownloadItem(DownloadState::IN_PROGRESS, "oldest",
/*is_paused=*/false, oldest_time);
// Only the 3 newest downloads are shown at first.
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 3u);
EXPECT_EQ(models[0]->GetContentId().id, "now");
EXPECT_EQ(models[1]->GetContentId().id, "older");
EXPECT_EQ(models[2]->GetContentId().id, "even_older");
// Alerts for all the downloads are included.
EXPECT_THAT(
update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
UnorderedElementsAre(HasSubstr16(u"now"), HasSubstr16(u"older"),
HasSubstr16(u"even_older"), HasSubstr16(u"oldest")));
// Update the newest item to be paused, so it should sort behind all the other
// items.
UpdateDownloadItem(0, DownloadState::IN_PROGRESS, /*is_paused=*/true);
task_environment_.RunUntilIdle();
// The oldest download, previously too low in sort order to display, is
// retrieved after backfilling.
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 3u);
EXPECT_EQ(models[0]->GetContentId().id, "older");
EXPECT_EQ(models[1]->GetContentId().id, "even_older");
EXPECT_EQ(models[2]->GetContentId().id, "oldest");
// Only the item that was updated has an alert.
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
UnorderedElementsAre(HasSubstr16(u"now")));
}
TEST_F(DownloadBubbleUpdateServiceTest, UpdatesOfflineItems) {
base::Time now = base::Time::Now();
InitOfflineItems({OfflineItemState::IN_PROGRESS},
{"in_progress_active_offline_item"}, {now});
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 1u);
EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_offline_item");
EXPECT_EQ(models[0]->GetState(), DownloadState::IN_PROGRESS);
EXPECT_THAT(
update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
UnorderedElementsAre(HasSubstr16(u"in_progress_active_offline_item")));
UpdateOfflineItem(0, OfflineItemState::COMPLETE);
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 1u);
EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_offline_item");
EXPECT_EQ(models[0]->GetState(), DownloadState::COMPLETE);
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
UnorderedElementsAre(
AllOf(HasSubstr16(u"in_progress_active_offline_item"),
HasSubstr16(u"complete"))));
}
TEST_F(DownloadBubbleUpdateServiceTest, RemovesOfflineItems) {
base::Time now = base::Time::Now();
InitOfflineItems(
{OfflineItemState::IN_PROGRESS, OfflineItemState::PAUSED,
OfflineItemState::COMPLETE},
{"in_progress_active_offline_item", "in_progress_paused_offline_item",
"completed_offline_item"},
{now, now, now});
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 3u);
EXPECT_EQ(models[0]->GetContentId().id, "in_progress_active_offline_item");
EXPECT_EQ(models[1]->GetContentId().id, "in_progress_paused_offline_item");
EXPECT_EQ(models[2]->GetContentId().id, "completed_offline_item");
EXPECT_THAT(
update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
UnorderedElementsAre(HasSubstr16(u"in_progress_active_offline_item"),
HasSubstr16(u"in_progress_paused_offline_item"),
HasSubstr16(u"completed_offline_item")));
offline_content_provider_->NotifyOnItemRemoved(models[0]->GetContentId());
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 2u);
EXPECT_EQ(models[0]->GetContentId().id, "in_progress_paused_offline_item");
EXPECT_EQ(models[1]->GetContentId().id, "completed_offline_item");
// No alert generated for removal.
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
IsEmpty());
}
TEST_F(DownloadBubbleUpdateServiceTest, DoesNotAddExpiredItems) {
base::Time too_old_time = base::Time::Now() - base::Hours(25);
InitDownloadItem(DownloadState::IN_PROGRESS, "old",
/*is_paused=*/false, too_old_time);
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
EXPECT_EQ(models.size(), 0u);
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
IsEmpty());
}
TEST_F(DownloadBubbleUpdateServiceTest, PrunesExpiredItems) {
base::Time now = base::Time::Now();
base::Time two_hours_ago = now - base::Hours(2);
// Add some items that are currently recent enough to add.
InitDownloadItem(DownloadState::IN_PROGRESS, "now_download",
/*is_paused=*/false, now);
InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago_download",
/*is_paused=*/false, two_hours_ago);
InitOfflineItems({OfflineItemState::PAUSED, OfflineItemState::PAUSED},
{"now_offline_item", "two_hours_ago_offline_item"},
{now, two_hours_ago});
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 4u);
EXPECT_EQ(models[0]->GetContentId().id, "now_download");
EXPECT_EQ(models[1]->GetContentId().id, "two_hours_ago_download");
EXPECT_EQ(models[2]->GetContentId().id, "now_offline_item");
EXPECT_EQ(models[3]->GetContentId().id, "two_hours_ago_offline_item");
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
UnorderedElementsAre(HasSubstr16(u"now_download"),
HasSubstr16(u"two_hours_ago_download"),
HasSubstr16(u"now_offline_item"),
HasSubstr16(u"two_hours_ago_offline_item")));
// Fast forward so that the older items become too old.
task_environment_.FastForwardBy(base::Hours(23));
// Only the newer items should remain.
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 2u);
EXPECT_EQ(models[0]->GetContentId().id, "now_download");
EXPECT_EQ(models[1]->GetContentId().id, "now_offline_item");
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
IsEmpty());
}
TEST_F(DownloadBubbleUpdateServiceTest, DoesNotBackfillIfNotForced) {
update_service_->set_max_num_items_to_show_for_testing(3);
update_service_->set_extra_items_to_cache_for_testing(0);
base::Time now = base::Time::Now();
base::Time recent = now - base::Minutes(1);
base::Time two_hours_ago = now - base::Hours(2);
InitDownloadItem(DownloadState::IN_PROGRESS, "now",
/*is_paused=*/false, now);
InitDownloadItem(DownloadState::IN_PROGRESS, "recent",
/*is_paused=*/false, recent);
InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago",
/*is_paused=*/false, two_hours_ago);
InitDownloadItem(DownloadState::IN_PROGRESS, "now_paused",
/*is_paused=*/true, now);
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 3u);
EXPECT_EQ(models[0]->GetContentId().id, "now");
EXPECT_EQ(models[1]->GetContentId().id, "recent");
EXPECT_EQ(models[2]->GetContentId().id, "two_hours_ago");
task_environment_.FastForwardBy(base::Hours(23));
// Since items are pruned, return the unpruned ones immediately and indicate
// that results are not complete.
EXPECT_FALSE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 2u);
EXPECT_EQ(models[0]->GetContentId().id, "now");
EXPECT_EQ(models[1]->GetContentId().id, "recent");
// Sometime later, once the backfilling is complete, we will start to return
// all the non-expired items.
task_environment_.RunUntilIdle();
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 3u);
EXPECT_EQ(models[0]->GetContentId().id, "now");
EXPECT_EQ(models[1]->GetContentId().id, "recent");
EXPECT_EQ(models[2]->GetContentId().id, "now_paused");
}
TEST_F(DownloadBubbleUpdateServiceTest, BackfillsSynchronouslyIfForced) {
update_service_->set_max_num_items_to_show_for_testing(3);
update_service_->set_extra_items_to_cache_for_testing(0);
base::Time now = base::Time::Now();
base::Time recent = now - base::Minutes(1);
base::Time two_hours_ago = now - base::Hours(2);
InitDownloadItem(DownloadState::IN_PROGRESS, "now",
/*is_paused=*/false, now);
InitDownloadItem(DownloadState::IN_PROGRESS, "recent",
/*is_paused=*/false, recent);
InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago",
/*is_paused=*/false, two_hours_ago);
InitDownloadItem(DownloadState::IN_PROGRESS, "now_paused",
/*is_paused=*/true, now);
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 3u);
EXPECT_EQ(models[0]->GetContentId().id, "now");
EXPECT_EQ(models[1]->GetContentId().id, "recent");
EXPECT_EQ(models[2]->GetContentId().id, "two_hours_ago");
task_environment_.FastForwardBy(base::Hours(23));
EXPECT_TRUE(update_service_->GetAllModelsToDisplay(
models, /*web_app_id=*/nullptr, /*force_backfill_download_items=*/true));
ASSERT_EQ(models.size(), 3u);
EXPECT_EQ(models[0]->GetContentId().id, "now");
EXPECT_EQ(models[1]->GetContentId().id, "recent");
EXPECT_EQ(models[2]->GetContentId().id, "now_paused");
}
TEST_F(DownloadBubbleUpdateServiceTest, CachesExtraItems) {
update_service_->set_max_num_items_to_show_for_testing(3);
// Cache an extra item.
update_service_->set_extra_items_to_cache_for_testing(1);
base::Time now = base::Time::Now();
base::Time recent = now - base::Minutes(2);
base::Time two_hours_ago = now - base::Hours(2);
InitDownloadItem(DownloadState::IN_PROGRESS, "now",
/*is_paused=*/false, now);
InitDownloadItem(DownloadState::IN_PROGRESS, "recent",
/*is_paused=*/false, recent);
InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago",
/*is_paused=*/false, two_hours_ago);
InitDownloadItem(DownloadState::IN_PROGRESS, "now_paused",
/*is_paused=*/true, now);
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 3u);
EXPECT_EQ(models[0]->GetContentId().id, "now");
EXPECT_EQ(models[1]->GetContentId().id, "recent");
EXPECT_EQ(models[2]->GetContentId().id, "two_hours_ago");
task_environment_.FastForwardBy(base::Hours(23));
// This returns true despite not forcing a backfill, because the extra item
// in the cache is available to be returned.
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 3u);
EXPECT_EQ(models[0]->GetContentId().id, "now");
EXPECT_EQ(models[1]->GetContentId().id, "recent");
EXPECT_EQ(models[2]->GetContentId().id, "now_paused");
}
// Test that downloads from web apps are only displayed when queried for the
// specific web app.
TEST_F(DownloadBubbleUpdateServiceTest, GetAllModelsToDisplayForWebApp) {
base::Time now = base::Time::Now();
base::Time before = now - base::Hours(1);
webapps::AppId app_a_id = "app_a";
webapps::AppId app_b_id = "app_b";
InitDownloadItem(DownloadState::IN_PROGRESS, "app_a_download",
/*is_paused=*/false, now, &app_a_id);
InitDownloadItem(DownloadState::IN_PROGRESS, "app_b_download",
/*is_paused=*/false, now, &app_b_id);
InitDownloadItem(DownloadState::IN_PROGRESS, "non_app_download",
/*is_paused=*/false, now);
// Offline items should only be returned for non-web-app queries.
InitOfflineItems({OfflineItemState::IN_PROGRESS}, {"offline_item"}, {before});
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 2u);
EXPECT_EQ(models[0]->GetContentId().id, "non_app_download");
EXPECT_EQ(models[1]->GetContentId().id, "offline_item");
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(nullptr),
UnorderedElementsAre(HasSubstr16(u"non_app_download"),
HasSubstr16(u"offline_item")));
EXPECT_TRUE(update_service_->GetAllModelsToDisplay(models, &app_a_id));
ASSERT_EQ(models.size(), 1u);
EXPECT_EQ(models[0]->GetContentId().id, "app_a_download");
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(&app_a_id),
UnorderedElementsAre(HasSubstr16(u"app_a_download")));
EXPECT_TRUE(update_service_->GetAllModelsToDisplay(models, &app_b_id));
ASSERT_EQ(models.size(), 1u);
EXPECT_EQ(models[0]->GetContentId().id, "app_b_download");
EXPECT_THAT(update_service_->TakeAccessibleAlertsForAnnouncement(&app_b_id),
UnorderedElementsAre(HasSubstr16(u"app_b_download")));
}
TEST_F(DownloadBubbleUpdateServiceTest, GetProgressInfo) {
InitDownloadItem(DownloadState::IN_PROGRESS, "in_progress_active",
/*is_paused=*/false);
InitDownloadItem(DownloadState::IN_PROGRESS, "in_progress_paused",
/*is_paused=*/true);
InitDownloadItem(DownloadState::COMPLETE, "completed",
/*is_paused=*/false);
DownloadDisplay::ProgressInfo progress_info =
update_service_->GetProgressInfo(/*web_app_id=*/nullptr);
EXPECT_EQ(progress_info.download_count, 2);
EXPECT_TRUE(progress_info.progress_certain);
EXPECT_EQ(progress_info.progress_percentage, 50);
InitOfflineItems({OfflineItemState::IN_PROGRESS}, {"offline_item"},
{base::Time::Now()});
progress_info = update_service_->GetProgressInfo(/*web_app_id=*/nullptr);
EXPECT_EQ(progress_info.download_count, 3);
EXPECT_FALSE(progress_info.progress_certain);
EXPECT_EQ(progress_info.progress_percentage, 50);
}
TEST_F(DownloadBubbleUpdateServiceTest, GetProgressInfoForWebApp) {
base::Time now = base::Time::Now();
webapps::AppId app_a_id = "app_a";
webapps::AppId app_b_id = "app_b";
InitDownloadItem(DownloadState::IN_PROGRESS, "app_a_download1",
/*is_paused=*/false, now, &app_a_id);
InitDownloadItem(DownloadState::IN_PROGRESS, "app_a_download2",
/*is_paused=*/false, now, &app_a_id);
InitDownloadItem(DownloadState::IN_PROGRESS, "app_b_download1",
/*is_paused=*/false, now, &app_b_id);
InitDownloadItem(DownloadState::IN_PROGRESS, "app_b_download2",
/*is_paused=*/false, now, &app_b_id);
InitDownloadItem(DownloadState::IN_PROGRESS, "app_b_download3",
/*is_paused=*/false, now, &app_b_id);
InitDownloadItem(DownloadState::IN_PROGRESS, "non_app_download",
/*is_paused=*/false, now);
DownloadDisplay::ProgressInfo non_app_progress_info =
update_service_->GetProgressInfo(/*web_app_id=*/nullptr);
EXPECT_EQ(non_app_progress_info.download_count, 1);
EXPECT_TRUE(non_app_progress_info.progress_certain);
EXPECT_EQ(non_app_progress_info.progress_percentage, 50);
DownloadDisplay::ProgressInfo app_a_progress_info =
update_service_->GetProgressInfo(&app_a_id);
EXPECT_EQ(app_a_progress_info.download_count, 2);
EXPECT_TRUE(app_a_progress_info.progress_certain);
EXPECT_EQ(app_a_progress_info.progress_percentage, 50);
DownloadDisplay::ProgressInfo app_b_progress_info =
update_service_->GetProgressInfo(&app_b_id);
EXPECT_EQ(app_b_progress_info.download_count, 3);
EXPECT_TRUE(app_b_progress_info.progress_certain);
EXPECT_EQ(app_b_progress_info.progress_percentage, 50);
}
TEST_F(DownloadBubbleUpdateServiceTest, GetDisplayInfo_InProgress) {
base::Time now = base::Time::Now();
base::Time two_hours_ago = now - base::Hours(2);
InitDownloadItem(DownloadState::IN_PROGRESS, "now_download",
/*is_paused=*/false, now);
InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago_download",
/*is_paused=*/false, two_hours_ago);
InitOfflineItems({OfflineItemState::PAUSED, OfflineItemState::PAUSED},
{"now_offline_item", "two_hours_ago_offline_item"},
{now, two_hours_ago});
DownloadBubbleDisplayInfo info =
update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
EXPECT_EQ(info.all_models_size, 4u);
// The last_completed_time is null, because no downloads have finished.
EXPECT_EQ(info.last_completed_time, base::Time());
EXPECT_EQ(info.in_progress_count, 4);
EXPECT_EQ(info.paused_count, 2);
EXPECT_TRUE(info.has_unactioned);
EXPECT_FALSE(info.has_deep_scanning);
}
TEST_F(DownloadBubbleUpdateServiceTest,
GetDisplayInfo_UpdateForCompleteDownload) {
base::Time now = base::Time::Now();
base::Time two_hours_ago = now - base::Hours(2);
InitDownloadItem(DownloadState::IN_PROGRESS, "now_download",
/*is_paused=*/false, now);
InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago_download",
/*is_paused=*/false, two_hours_ago);
InitDownloadItem(DownloadState::COMPLETE, "completed_download",
/*is_paused=*/false, two_hours_ago);
InitOfflineItems({OfflineItemState::PAUSED, OfflineItemState::PAUSED},
{"now_offline_item", "two_hours_ago_offline_item"},
{now, two_hours_ago});
// Make the download completed to update the last_completed_time.
UpdateDownloadItem(0, DownloadState::COMPLETE);
DownloadBubbleDisplayInfo info =
update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
EXPECT_EQ(info.all_models_size, 5u);
EXPECT_EQ(info.last_completed_time, now);
EXPECT_EQ(info.in_progress_count, 3);
EXPECT_EQ(info.paused_count, 2);
EXPECT_TRUE(info.has_unactioned);
EXPECT_FALSE(info.has_deep_scanning);
}
TEST_F(DownloadBubbleUpdateServiceTest,
GetDisplayInfo_ExistingCompleteDownload) {
base::Time now = base::Time::Now();
base::Time two_hours_ago = now - base::Hours(2);
InitDownloadItem(DownloadState::IN_PROGRESS, "now_download",
/*is_paused=*/false, now);
InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago_download",
/*is_paused=*/false, two_hours_ago);
InitDownloadItem(DownloadState::COMPLETE, "completed_download",
/*is_paused=*/false, two_hours_ago);
InitOfflineItems({OfflineItemState::PAUSED, OfflineItemState::PAUSED},
{"now_offline_item", "two_hours_ago_offline_item"},
{now, two_hours_ago});
DownloadBubbleDisplayInfo info =
update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
EXPECT_EQ(info.all_models_size, 5u);
// The last_completed_time should be set to the end time of the completed
// download, which is 1 minute past its start time (set in InitDownloadItem).
EXPECT_EQ(info.last_completed_time, two_hours_ago + base::Minutes(1));
EXPECT_EQ(info.in_progress_count, 4);
EXPECT_EQ(info.paused_count, 2);
EXPECT_TRUE(info.has_unactioned);
EXPECT_FALSE(info.has_deep_scanning);
}
TEST_F(DownloadBubbleUpdateServiceTest, GetDisplayInfo_UpdateForDangerous) {
base::Time now = base::Time::Now();
base::Time two_hours_ago = now - base::Hours(2);
InitDownloadItem(DownloadState::IN_PROGRESS, "now_download",
/*is_paused=*/false, now);
InitDownloadItem(DownloadState::IN_PROGRESS, "two_hours_ago_download",
/*is_paused=*/false, two_hours_ago);
InitDownloadItem(DownloadState::COMPLETE, "completed_download",
/*is_paused=*/false, two_hours_ago);
InitOfflineItems({OfflineItemState::PAUSED, OfflineItemState::PAUSED},
{"now_offline_item", "two_hours_ago_offline_item"},
{now, two_hours_ago});
// Make the download dangerous to update the last_completed_time.
UpdateDownloadItem(
0, DownloadState::IN_PROGRESS, /*is_paused=*/false,
DownloadDangerType::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT);
DownloadBubbleDisplayInfo info =
update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
EXPECT_EQ(info.all_models_size, 5u);
EXPECT_EQ(info.last_completed_time, now);
EXPECT_EQ(info.in_progress_count, 3);
EXPECT_EQ(info.paused_count, 2);
EXPECT_TRUE(info.has_unactioned);
EXPECT_FALSE(info.has_deep_scanning);
}
TEST_F(DownloadBubbleUpdateServiceTest, GetDisplayInfoForWebApp) {
base::Time now = base::Time::Now();
base::Time two_hours_ago = now - base::Hours(2);
webapps::AppId app_a_id = "app_a";
webapps::AppId app_b_id = "app_b";
InitDownloadItem(DownloadState::IN_PROGRESS, "non_app_download",
/*is_paused=*/false, now);
InitOfflineItems({OfflineItemState::PAUSED, OfflineItemState::PAUSED},
{"now_offline_item", "two_hours_ago_offline_item"},
{now, two_hours_ago});
InitDownloadItem(DownloadState::IN_PROGRESS, "app_a_download1",
/*is_paused=*/false, now, &app_a_id);
InitDownloadItem(DownloadState::IN_PROGRESS, "app_a_download2",
/*is_paused=*/false, now, &app_a_id);
InitDownloadItem(DownloadState::IN_PROGRESS, "app_b_download",
/*is_paused=*/false, now, &app_b_id);
DownloadBubbleDisplayInfo non_app_info =
update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
EXPECT_EQ(non_app_info.all_models_size, 3u);
EXPECT_EQ(non_app_info.paused_count, 2);
DownloadBubbleDisplayInfo app_a_info =
update_service_->GetDisplayInfo(&app_a_id);
EXPECT_EQ(app_a_info.all_models_size, 2u);
DownloadBubbleDisplayInfo app_b_info =
update_service_->GetDisplayInfo(&app_b_id);
EXPECT_EQ(app_b_info.all_models_size, 1u);
}
TEST_F(DownloadBubbleUpdateServiceTest,
DownloadUpdatedWithWebAppDataAfterCreation) {
base::Time now = base::Time::Now();
webapps::AppId app_id = "app";
// This simulates the restoration of a web app download from the history
// database, during which the item is created first without the
// DownloadItemWebAppData, and then subsequently tagged with the data.
InitDownloadItem(DownloadState::IN_PROGRESS, "app_download",
/*is_paused=*/false, now);
DownloadItemWebAppData::CreateAndAttachToItem(&GetDownloadItem(0), app_id);
GetDownloadItem(0).NotifyObserversDownloadUpdated();
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
EXPECT_TRUE(models.empty());
EXPECT_TRUE(update_service_->GetAllModelsToDisplay(models, &app_id));
ASSERT_EQ(models.size(), 1u);
EXPECT_EQ(models[0]->GetContentId().id, "app_download");
DownloadDisplay::ProgressInfo non_app_progress_info =
update_service_->GetProgressInfo(/*web_app_id=*/nullptr);
EXPECT_EQ(non_app_progress_info.download_count, 0);
DownloadDisplay::ProgressInfo app_progress_info =
update_service_->GetProgressInfo(&app_id);
EXPECT_EQ(app_progress_info.download_count, 1);
DownloadBubbleDisplayInfo non_app_info =
update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
EXPECT_EQ(non_app_info.all_models_size, 0u);
DownloadBubbleDisplayInfo app_info = update_service_->GetDisplayInfo(&app_id);
EXPECT_EQ(app_info.all_models_size, 1u);
}
class DownloadBubbleUpdateServiceIncognitoTest
: public DownloadBubbleUpdateServiceTest {
public:
DownloadBubbleUpdateServiceIncognitoTest() = default;
DownloadBubbleUpdateServiceIncognitoTest(
const DownloadBubbleUpdateServiceIncognitoTest&) = delete;
DownloadBubbleUpdateServiceIncognitoTest& operator=(
const DownloadBubbleUpdateServiceIncognitoTest&) = delete;
~DownloadBubbleUpdateServiceIncognitoTest() override = default;
void SetUp() override {
DownloadBubbleUpdateServiceTest::SetUp();
TestingProfile::Builder builder;
TestingProfile::TestingFactories testing_factories;
testing_factories.emplace_back(HistoryServiceFactory::GetInstance(),
HistoryServiceFactory::GetDefaultFactory());
testing_factories.emplace_back(
DownloadCoreServiceFactory::GetInstance(),
base::BindRepeating(&SetUpDownloadCoreService, download_manager_));
builder.AddTestingFactories(std::move(testing_factories));
incognito_profile_ = builder.BuildIncognito(profile_);
std::unique_ptr<NiceMock<content::MockDownloadManager>>
incognito_download_manager = SetUpDownloadManager();
incognito_download_manager_ = incognito_download_manager.get();
EXPECT_CALL(*incognito_download_manager_, GetBrowserContext())
.WillRepeatedly(Return(incognito_profile_));
incognito_profile_->SetDownloadManagerForTesting(
std::move(incognito_download_manager));
incognito_update_service_ = static_cast<DownloadBubbleUpdateService*>(
DownloadBubbleUpdateServiceFactory::GetInstance()
->SetTestingFactoryAndUse(
incognito_profile_,
base::BindRepeating(
&DownloadBubbleUpdateServiceTest::CreateUpdateService,
base::Unretained(this), incognito_update_service_)));
incognito_download_core_service_ =
DownloadCoreServiceFactory::GetInstance()->GetForBrowserContext(
incognito_profile_);
// Do not call Initialize here to delay registering the download manager for
// the test.
}
void TearDown() override {
RemoveDownloadItemsObserver(
incognito_download_items_,
incognito_update_service_->download_item_notifier_for_testing());
RemoveDownloadItemsObserver(
download_items_, incognito_update_service_
->original_download_item_notifier_for_testing());
incognito_download_core_service_->GetDownloadManagerDelegate()->Shutdown();
incognito_update_service_ = nullptr;
incognito_download_manager_ = nullptr;
incognito_download_core_service_ = nullptr;
incognito_profile_ = nullptr;
DownloadBubbleUpdateServiceTest::TearDown();
}
protected:
raw_ptr<Profile, DanglingUntriaged> incognito_profile_ = nullptr;
raw_ptr<NiceMock<content::MockDownloadManager>, DanglingUntriaged>
incognito_download_manager_ = nullptr;
std::vector<std::unique_ptr<NiceMockDownloadItem>> incognito_download_items_;
raw_ptr<DownloadBubbleUpdateService, DanglingUntriaged>
incognito_update_service_ = nullptr;
raw_ptr<DownloadCoreService> incognito_download_core_service_ = nullptr;
};
// Tests that initializing an update service for an incognito profile sets both
// the download manager and the original download manager.
TEST_F(DownloadBubbleUpdateServiceIncognitoTest, InitIncognito) {
base::Time now = base::Time::Now();
// |observe| is false because this only tests initialization.
InitDownloadItem(DownloadState::COMPLETE, "regular_profile_download",
/*is_paused=*/false, now - base::Hours(1),
/*web_app_id=*/nullptr, /*is_crx=*/false,
/*observe=*/false);
InitDownloadItem(*incognito_download_manager_, *incognito_update_service_,
incognito_download_items_, incognito_profile_,
DownloadState::COMPLETE, "incognito_profile_download",
/*is_paused=*/false, now, /*web_app_id=*/nullptr,
/*is_crx=*/false,
/*observe=*/false);
// Initial state: Regular profile's update service has a manager, incognito's
// does not.
EXPECT_NE(update_service_->GetDownloadManager(), nullptr);
EXPECT_EQ(update_service_->GetDownloadManager(), download_manager_);
EXPECT_EQ(incognito_update_service_->GetDownloadManager(), nullptr);
incognito_update_service_->Initialize(incognito_download_manager_);
// Run the offline content provider initialization.
task_environment_.RunUntilIdle();
// Regular profile's update service's manager hasn't changed.
EXPECT_EQ(update_service_->GetDownloadManager(), download_manager_);
// Incognito profile's update service's manager now set correctly.
EXPECT_EQ(incognito_update_service_->GetDownloadManager(),
incognito_download_manager_);
// Both download items should be present after initialization.
DownloadUIModelPtrVector models;
EXPECT_TRUE(incognito_update_service_->GetAllModelsToDisplay(
models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 2u);
EXPECT_EQ(models[0]->GetContentId().id, "incognito_profile_download");
EXPECT_EQ(models[1]->GetContentId().id, "regular_profile_download");
}
// Tests that the DownloadBubbleDisplayInfo is updated when a download with an
// ephemeral warning expires.
TEST_F(DownloadBubbleUpdateServiceTest, OnEphemeralWarningExpired) {
base::Time now = base::Time::Now();
InitDownloadItem(DownloadState::IN_PROGRESS, "normal_download",
/*is_paused=*/false, now);
InitDownloadItem(DownloadState::IN_PROGRESS, "ephemeral_warning_download",
/*is_paused=*/false, now);
DownloadUIModelPtrVector models;
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
EXPECT_EQ(models.size(), 2u);
DownloadBubbleDisplayInfo info =
update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
EXPECT_EQ(info.all_models_size, 2u);
// Mark the download with an ephemeral warning.
UpdateDownloadItem(1, DownloadState::IN_PROGRESS, /*is_paused=*/false,
DownloadDangerType::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE);
// Simulate showing the warning in the UI and waiting past the expiry time.
DownloadItemModel(&GetDownloadItem(1)).SetEphemeralWarningUiShownTime(now);
task_environment_.FastForwardBy(
DownloadItemModel::kEphemeralWarningLifetimeOnBubble * 2);
update_service_->OnEphemeralWarningExpired(GetDownloadItem(1).GetGuid());
// The ephemeral warning download should no longer be observable.
// Check GetDisplayInfo first, because GetAllModelsToDisplay will prune it.
info = update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
EXPECT_EQ(info.all_models_size, 1u);
EXPECT_TRUE(
update_service_->GetAllModelsToDisplay(models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 1u);
EXPECT_EQ(models[0]->GetContentId().id, "normal_download");
}
// Tests that a download with an ephemeral warning from the original profile is
// properly handled when it expires.
TEST_F(DownloadBubbleUpdateServiceIncognitoTest,
OnEphemeralWarningExpiredFromOriginalProfile) {
base::Time now = base::Time::Now();
InitDownloadItem(DownloadState::COMPLETE, "regular_profile_normal_download",
/*is_paused=*/false, now - base::Hours(1),
/*web_app_id=*/nullptr, /*is_crx=*/false,
/*observe=*/false);
InitDownloadItem(DownloadState::COMPLETE,
"regular_profile_ephemeral_warning_download",
/*is_paused=*/false, now - base::Hours(1),
/*web_app_id=*/nullptr, /*is_crx=*/false,
/*observe=*/false);
InitDownloadItem(*incognito_download_manager_, *incognito_update_service_,
incognito_download_items_, incognito_profile_,
DownloadState::COMPLETE,
"incognito_profile_ephemeral_warning_download",
/*is_paused=*/false, now, /*web_app_id=*/nullptr,
/*is_crx=*/false,
/*observe=*/false);
incognito_update_service_->Initialize(incognito_download_manager_);
// Run the offline content provider initialization.
task_environment_.RunUntilIdle();
DownloadUIModelPtrVector models;
EXPECT_TRUE(incognito_update_service_->GetAllModelsToDisplay(
models, /*web_app_id=*/nullptr));
EXPECT_EQ(models.size(), 3u);
DownloadBubbleDisplayInfo info =
incognito_update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
EXPECT_EQ(info.all_models_size, 3u);
// Mark the regular profile ephemeral download with an ephemeral warning.
UpdateDownloadItem(1, DownloadState::IN_PROGRESS, /*is_paused=*/false,
DownloadDangerType::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE);
// Mark the incognito profile ephemeral download with an ephemeral warning.
UpdateDownloadItem(*incognito_download_items_[0], DownloadState::IN_PROGRESS,
/*is_paused=*/false,
DownloadDangerType::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE);
// Simulate showing the warning in the UI and waiting past the expiry time.
DownloadItemModel(&GetDownloadItem(1)).SetEphemeralWarningUiShownTime(now);
DownloadItemModel(incognito_download_items_[0].get())
.SetEphemeralWarningUiShownTime(now);
task_environment_.FastForwardBy(
DownloadItemModel::kEphemeralWarningLifetimeOnBubble * 2);
incognito_update_service_->OnEphemeralWarningExpired(
GetDownloadItem(1).GetGuid());
incognito_update_service_->OnEphemeralWarningExpired(
incognito_download_items_[0]->GetGuid());
// The ephemeral warning downloads should no longer be observable.
// Check GetDisplayInfo first, because GetAllModelsToDisplay will prune
// them.
info = incognito_update_service_->GetDisplayInfo(/*web_app_id=*/nullptr);
EXPECT_EQ(info.all_models_size, 1u);
EXPECT_TRUE(incognito_update_service_->GetAllModelsToDisplay(
models, /*web_app_id=*/nullptr));
ASSERT_EQ(models.size(), 1u);
EXPECT_EQ(models[0]->GetContentId().id, "regular_profile_normal_download");
}
} // namespace