// Copyright (c) 2011 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 <string>
#include <set>
#include "base/file_util.h"
#include "base/memory/scoped_ptr.h"
#include "base/stl_util-inl.h"
#include "base/string_util.h"
#include "build/build_config.h"
#include "chrome/browser/download/download_file.h"
#include "chrome/browser/download/download_file_manager.h"
#include "chrome/browser/download/download_item.h"
#include "chrome/browser/download/download_manager.h"
#include "chrome/browser/download/download_prefs.h"
#include "chrome/browser/download/download_status_updater.h"
#include "chrome/browser/download/download_util.h"
#include "chrome/browser/download/mock_download_manager.h"
#include "chrome/browser/history/download_create_info.h"
#include "chrome/browser/prefs/pref_service.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/testing_profile.h"
#include "content/browser/browser_thread.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gmock_mutant.h"
#include "testing/gtest/include/gtest/gtest.h"
class DownloadManagerTest : public testing::Test {
public:
static const char* kTestData;
static const size_t kTestDataLen;
DownloadManagerTest()
: profile_(new TestingProfile()),
download_manager_(new MockDownloadManager(&download_status_updater_)),
ui_thread_(BrowserThread::UI, &message_loop_),
file_thread_(BrowserThread::FILE, &message_loop_) {
download_manager_->Init(profile_.get());
}
~DownloadManagerTest() {
download_manager_->Shutdown();
// profile_ must outlive download_manager_, so we explicitly delete
// download_manager_ first.
download_manager_ = NULL;
profile_.reset(NULL);
message_loop_.RunAllPending();
}
void AddDownloadToFileManager(int id, DownloadFile* download_file) {
file_manager()->downloads_[id] = download_file;
}
void OnAllDataSaved(int32 download_id, int64 size, const std::string& hash) {
download_manager_->OnAllDataSaved(download_id, size, hash);
}
void FileSelected(const FilePath& path, int index, void* params) {
download_manager_->FileSelected(path, index, params);
}
void AttachDownloadItem(DownloadCreateInfo* info) {
download_manager_->AttachDownloadItem(info);
}
void OnDownloadError(int32 download_id, int64 size, int os_error) {
download_manager_->OnDownloadError(download_id, size, os_error);
}
// Get the download item with ID |id|.
DownloadItem* GetActiveDownloadItem(int32 id) {
if (ContainsKey(download_manager_->active_downloads_, id))
return download_manager_->active_downloads_[id];
return NULL;
}
protected:
DownloadStatusUpdater download_status_updater_;
scoped_ptr<TestingProfile> profile_;
scoped_refptr<DownloadManager> download_manager_;
scoped_refptr<DownloadFileManager> file_manager_;
MessageLoopForUI message_loop_;
BrowserThread ui_thread_;
BrowserThread file_thread_;
DownloadFileManager* file_manager() {
if (!file_manager_) {
file_manager_ = new DownloadFileManager(NULL);
download_manager_->file_manager_ = file_manager_;
}
return file_manager_;
}
// Make sure download item |id| was set with correct safety state for
// given |is_dangerous_file| and |is_dangerous_url|.
bool VerifySafetyState(bool is_dangerous_file,
bool is_dangerous_url,
int id) {
DownloadItem::SafetyState safety_state =
download_manager_->GetDownloadItem(id)->safety_state();
return (is_dangerous_file || is_dangerous_url) ?
safety_state != DownloadItem::SAFE : safety_state == DownloadItem::SAFE;
}
DISALLOW_COPY_AND_ASSIGN(DownloadManagerTest);
};
const char* DownloadManagerTest::kTestData = "a;sdlfalsdfjalsdkfjad";
const size_t DownloadManagerTest::kTestDataLen =
strlen(DownloadManagerTest::kTestData);
namespace {
const struct {
const char* url;
const char* mime_type;
bool save_as;
bool prompt_for_download;
bool expected_save_as;
} kStartDownloadCases[] = {
{ "http://www.foo.com/dont-open.html",
"text/html",
false,
false,
false, },
{ "http://www.foo.com/save-as.html",
"text/html",
true,
false,
true, },
{ "http://www.foo.com/always-prompt.html",
"text/html",
false,
true,
true, },
{ "http://www.foo.com/user-script-text-html-mimetype.user.js",
"text/html",
false,
false,
false, },
{ "http://www.foo.com/extensionless-extension",
"application/x-chrome-extension",
true,
false,
true, },
{ "http://www.foo.com/save-as.pdf",
"application/pdf",
true,
false,
true, },
{ "http://www.foo.com/sometimes_prompt.pdf",
"application/pdf",
false,
true,
false, },
{ "http://www.foo.com/always_prompt.jar",
"application/jar",
false,
true,
true, },
};
const struct {
FilePath::StringType suggested_path;
bool is_dangerous_file;
bool is_dangerous_url;
bool finish_before_rename;
int expected_rename_count;
} kDownloadRenameCases[] = {
// Safe download, download finishes BEFORE file name determined.
// Renamed twice (linear path through UI). Crdownload file does not need
// to be deleted.
{ FILE_PATH_LITERAL("foo.zip"),
false, false, true, 2, },
// Dangerous download (file is dangerous or download URL is not safe or both),
// download finishes BEFORE file name determined. Needs to be renamed only
// once.
{ FILE_PATH_LITERAL("Unconfirmed xxx.crdownload"),
true, false, true, 1, },
{ FILE_PATH_LITERAL("Unconfirmed xxx.crdownload"),
false, true, true, 1, },
{ FILE_PATH_LITERAL("Unconfirmed xxx.crdownload"),
true, true, true, 1, },
// Safe download, download finishes AFTER file name determined.
// Needs to be renamed twice.
{ FILE_PATH_LITERAL("foo.zip"),
false, false, false, 2, },
// Dangerous download, download finishes AFTER file name determined.
// Needs to be renamed only once.
{ FILE_PATH_LITERAL("Unconfirmed xxx.crdownload"),
true, false, false, 1, },
{ FILE_PATH_LITERAL("Unconfirmed xxx.crdownload"),
false, true, false, 1, },
{ FILE_PATH_LITERAL("Unconfirmed xxx.crdownload"),
true, true, false, 1, },
};
class MockDownloadFile : public DownloadFile {
public:
MockDownloadFile(DownloadCreateInfo* info, DownloadManager* manager)
: DownloadFile(info, manager), renamed_count_(0) { }
virtual ~MockDownloadFile() { Destructed(); }
MOCK_METHOD1(Rename, bool(const FilePath&));
MOCK_METHOD0(Destructed, void());
bool TestMultipleRename(
int expected_count, const FilePath& expected,
const FilePath& path) {
++renamed_count_;
EXPECT_EQ(expected_count, renamed_count_);
EXPECT_EQ(expected.value(), path.value());
return true;
}
private:
int renamed_count_;
};
// This is an observer that records what download IDs have opened a select
// file dialog.
class SelectFileObserver : public DownloadManager::Observer {
public:
explicit SelectFileObserver(DownloadManager* download_manager)
: download_manager_(download_manager) {
DCHECK(download_manager_.get());
download_manager_->AddObserver(this);
}
~SelectFileObserver() {
download_manager_->RemoveObserver(this);
}
// Downloadmanager::Observer functions.
virtual void ModelChanged() {}
virtual void ManagerGoingDown() {}
virtual void SelectFileDialogDisplayed(int32 id) {
file_dialog_ids_.insert(id);
}
bool ShowedFileDialogForId(int32 id) {
return file_dialog_ids_.find(id) != file_dialog_ids_.end();
}
private:
std::set<int32> file_dialog_ids_;
scoped_refptr<DownloadManager> download_manager_;
};
// This observer tracks the progress of |DownloadItem|s.
class ItemObserver : public DownloadItem::Observer {
public:
explicit ItemObserver(DownloadItem* tracked)
: tracked_(tracked), states_hit_(0),
was_updated_(false), was_opened_(false) {
DCHECK(tracked_);
tracked_->AddObserver(this);
// Record the initial state.
OnDownloadUpdated(tracked_);
}
~ItemObserver() {
tracked_->RemoveObserver(this);
}
bool hit_state(int state) const {
return (1 << state) & states_hit_;
}
bool was_updated() const { return was_updated_; }
bool was_opened() const { return was_opened_; }
private:
// DownloadItem::Observer methods
virtual void OnDownloadUpdated(DownloadItem* download) {
DCHECK_EQ(tracked_, download);
states_hit_ |= (1 << download->state());
was_updated_ = true;
}
virtual void OnDownloadOpened(DownloadItem* download) {
DCHECK_EQ(tracked_, download);
states_hit_ |= (1 << download->state());
was_opened_ = true;
}
DownloadItem* tracked_;
int states_hit_;
bool was_updated_;
bool was_opened_;
};
} // namespace
#if !defined(OS_CHROMEOS)
TEST_F(DownloadManagerTest, StartDownload) {
BrowserThread io_thread(BrowserThread::IO, &message_loop_);
PrefService* prefs = profile_->GetPrefs();
prefs->SetFilePath(prefs::kDownloadDefaultDirectory, FilePath());
download_manager_->download_prefs()->EnableAutoOpenBasedOnExtension(
FilePath(FILE_PATH_LITERAL("example.pdf")));
for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kStartDownloadCases); ++i) {
prefs->SetBoolean(prefs::kPromptForDownload,
kStartDownloadCases[i].prompt_for_download);
SelectFileObserver observer(download_manager_);
DownloadCreateInfo* info = new DownloadCreateInfo;
info->download_id = static_cast<int>(i);
info->prompt_user_for_save_location = kStartDownloadCases[i].save_as;
info->url_chain.push_back(GURL(kStartDownloadCases[i].url));
info->mime_type = kStartDownloadCases[i].mime_type;
download_manager_->CreateDownloadItem(info);
DownloadFile* download_file(new DownloadFile(info, download_manager_));
AddDownloadToFileManager(info->download_id, download_file);
download_file->Initialize(false);
download_manager_->StartDownload(info);
message_loop_.RunAllPending();
// NOTE: At this point, |AttachDownloadItem| will have been run if we don't
// need to prompt the user, so |info| could have been destructed.
// This means that we can't check any of its values.
// However, SelectFileObserver will have recorded any attempt to open the
// select file dialog.
EXPECT_EQ(kStartDownloadCases[i].expected_save_as,
observer.ShowedFileDialogForId(i));
// If the Save As dialog pops up, it never reached
// DownloadManager::AttachDownloadItem(), and never deleted info or
// completed. This cleans up info.
// Note that DownloadManager::FileSelectionCanceled() is never called.
if (observer.ShowedFileDialogForId(i)) {
delete info;
}
}
}
#endif // !defined(OS_CHROMEOS)
TEST_F(DownloadManagerTest, DownloadRenameTest) {
using ::testing::_;
using ::testing::CreateFunctor;
using ::testing::Invoke;
using ::testing::Return;
for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kDownloadRenameCases); ++i) {
// |info| will be destroyed in download_manager_.
DownloadCreateInfo* info(new DownloadCreateInfo);
info->download_id = static_cast<int>(i);
info->prompt_user_for_save_location = false;
info->url_chain.push_back(GURL());
info->is_dangerous_file = kDownloadRenameCases[i].is_dangerous_file;
info->is_dangerous_url = kDownloadRenameCases[i].is_dangerous_url;
FilePath new_path(kDownloadRenameCases[i].suggested_path);
MockDownloadFile* download_file(
new MockDownloadFile(info, download_manager_));
AddDownloadToFileManager(info->download_id, download_file);
// |download_file| is owned by DownloadFileManager.
::testing::Mock::AllowLeak(download_file);
EXPECT_CALL(*download_file, Destructed()).Times(1);
if (kDownloadRenameCases[i].expected_rename_count == 1) {
EXPECT_CALL(*download_file, Rename(new_path)).WillOnce(Return(true));
} else {
ASSERT_EQ(2, kDownloadRenameCases[i].expected_rename_count);
FilePath crdownload(download_util::GetCrDownloadPath(new_path));
EXPECT_CALL(*download_file, Rename(_))
.WillOnce(testing::WithArgs<0>(Invoke(CreateFunctor(
download_file, &MockDownloadFile::TestMultipleRename,
1, crdownload))))
.WillOnce(testing::WithArgs<0>(Invoke(CreateFunctor(
download_file, &MockDownloadFile::TestMultipleRename,
2, new_path))));
}
download_manager_->CreateDownloadItem(info);
if (kDownloadRenameCases[i].finish_before_rename) {
OnAllDataSaved(i, 1024, std::string("fake_hash"));
message_loop_.RunAllPending();
FileSelected(new_path, i, info);
} else {
FileSelected(new_path, i, info);
message_loop_.RunAllPending();
OnAllDataSaved(i, 1024, std::string("fake_hash"));
}
message_loop_.RunAllPending();
EXPECT_TRUE(VerifySafetyState(kDownloadRenameCases[i].is_dangerous_file,
kDownloadRenameCases[i].is_dangerous_url,
i));
}
}
TEST_F(DownloadManagerTest, DownloadInterruptTest) {
using ::testing::_;
using ::testing::CreateFunctor;
using ::testing::Invoke;
using ::testing::Return;
// |info| will be destroyed in download_manager_.
DownloadCreateInfo* info(new DownloadCreateInfo);
info->download_id = static_cast<int>(0);
info->prompt_user_for_save_location = false;
info->url_chain.push_back(GURL());
info->is_dangerous_file = false;
info->is_dangerous_url = false;
const FilePath new_path(FILE_PATH_LITERAL("foo.zip"));
const FilePath cr_path(download_util::GetCrDownloadPath(new_path));
MockDownloadFile* download_file(
new MockDownloadFile(info, download_manager_));
AddDownloadToFileManager(info->download_id, download_file);
// |download_file| is owned by DownloadFileManager.
::testing::Mock::AllowLeak(download_file);
EXPECT_CALL(*download_file, Destructed()).Times(1);
EXPECT_CALL(*download_file, Rename(cr_path)).WillOnce(Return(true));
download_manager_->CreateDownloadItem(info);
DownloadItem* download = GetActiveDownloadItem(0);
ASSERT_TRUE(download != NULL);
EXPECT_EQ(DownloadItem::IN_PROGRESS, download->state());
scoped_ptr<ItemObserver> observer(new ItemObserver(download));
download_file->AppendDataToFile(kTestData, kTestDataLen);
info->path = new_path;
AttachDownloadItem(info);
message_loop_.RunAllPending();
EXPECT_TRUE(GetActiveDownloadItem(0) != NULL);
OnDownloadError(0, 1024, -6);
message_loop_.RunAllPending();
EXPECT_TRUE(GetActiveDownloadItem(0) == NULL);
EXPECT_EQ(DownloadItem::INTERRUPTED, download->state());
EXPECT_TRUE(observer->hit_state(DownloadItem::IN_PROGRESS));
EXPECT_TRUE(observer->hit_state(DownloadItem::INTERRUPTED));
EXPECT_FALSE(observer->hit_state(DownloadItem::COMPLETE));
EXPECT_FALSE(observer->hit_state(DownloadItem::CANCELLED));
EXPECT_FALSE(observer->hit_state(DownloadItem::REMOVING));
EXPECT_TRUE(observer->was_updated());
EXPECT_FALSE(observer->was_opened());
download->Cancel(true);
EXPECT_EQ(DownloadItem::INTERRUPTED, download->state());
EXPECT_TRUE(observer->hit_state(DownloadItem::IN_PROGRESS));
EXPECT_TRUE(observer->hit_state(DownloadItem::INTERRUPTED));
EXPECT_FALSE(observer->hit_state(DownloadItem::COMPLETE));
EXPECT_FALSE(observer->hit_state(DownloadItem::CANCELLED));
EXPECT_FALSE(observer->hit_state(DownloadItem::REMOVING));
EXPECT_TRUE(observer->was_updated());
EXPECT_FALSE(observer->was_opened());
}
TEST_F(DownloadManagerTest, DownloadCancelTest) {
using ::testing::_;
using ::testing::CreateFunctor;
using ::testing::Invoke;
using ::testing::Return;
// |info| will be destroyed in download_manager_.
DownloadCreateInfo* info(new DownloadCreateInfo);
info->download_id = static_cast<int>(0);
info->prompt_user_for_save_location = false;
info->url_chain.push_back(GURL());
info->is_dangerous_file = false;
info->is_dangerous_url = false;
const FilePath new_path(FILE_PATH_LITERAL("foo.zip"));
const FilePath cr_path(download_util::GetCrDownloadPath(new_path));
MockDownloadFile* download_file(
new MockDownloadFile(info, download_manager_));
AddDownloadToFileManager(info->download_id, download_file);
// |download_file| is owned by DownloadFileManager.
::testing::Mock::AllowLeak(download_file);
EXPECT_CALL(*download_file, Destructed()).Times(1);
EXPECT_CALL(*download_file, Rename(cr_path)).WillOnce(Return(true));
download_manager_->CreateDownloadItem(info);
DownloadItem* download = GetActiveDownloadItem(0);
ASSERT_TRUE(download != NULL);
EXPECT_EQ(DownloadItem::IN_PROGRESS, download->state());
scoped_ptr<ItemObserver> observer(new ItemObserver(download));
info->path = new_path;
AttachDownloadItem(info);
message_loop_.RunAllPending();
EXPECT_TRUE(GetActiveDownloadItem(0) != NULL);
download_file->AppendDataToFile(kTestData, kTestDataLen);
download->Cancel(false);
message_loop_.RunAllPending();
EXPECT_TRUE(GetActiveDownloadItem(0) != NULL);
EXPECT_TRUE(observer->hit_state(DownloadItem::IN_PROGRESS));
EXPECT_TRUE(observer->hit_state(DownloadItem::CANCELLED));
EXPECT_FALSE(observer->hit_state(DownloadItem::INTERRUPTED));
EXPECT_FALSE(observer->hit_state(DownloadItem::COMPLETE));
EXPECT_FALSE(observer->hit_state(DownloadItem::REMOVING));
EXPECT_TRUE(observer->was_updated());
EXPECT_FALSE(observer->was_opened());
EXPECT_FALSE(file_util::PathExists(new_path));
EXPECT_FALSE(file_util::PathExists(cr_path));
}
TEST_F(DownloadManagerTest, DownloadOverwriteTest) {
using ::testing::_;
using ::testing::CreateFunctor;
using ::testing::Invoke;
using ::testing::Return;
// Create a temporary directory.
ScopedTempDir temp_dir_;
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
// File names we're using.
const FilePath new_path(temp_dir_.path().AppendASCII("foo.txt"));
const FilePath cr_path(download_util::GetCrDownloadPath(new_path));
EXPECT_FALSE(file_util::PathExists(new_path));
// Create the file that we will overwrite. Will be automatically cleaned
// up when temp_dir_ is destroyed.
FILE* fp = file_util::OpenFile(new_path, "w");
file_util::CloseFile(fp);
EXPECT_TRUE(file_util::PathExists(new_path));
// Construct the unique file name that normally would be created, but
// which we will override.
int uniquifier = download_util::GetUniquePathNumber(new_path);
FilePath unique_new_path = new_path;
EXPECT_NE(0, uniquifier);
download_util::AppendNumberToPath(&unique_new_path, uniquifier);
// |info| will be destroyed in download_manager_.
DownloadCreateInfo* info(new DownloadCreateInfo);
info->download_id = static_cast<int>(0);
info->prompt_user_for_save_location = true;
info->url_chain.push_back(GURL());
info->is_dangerous_file = false;
info->is_dangerous_url = false;
download_manager_->CreateDownloadItem(info);
DownloadItem* download = GetActiveDownloadItem(0);
ASSERT_TRUE(download != NULL);
EXPECT_EQ(DownloadItem::IN_PROGRESS, download->state());
scoped_ptr<ItemObserver> observer(new ItemObserver(download));
// Create and initialize the download file. We're bypassing the first part
// of the download process and skipping to the part after the final file
// name has been chosen, so we need to initialize the download file
// properly.
DownloadFile* download_file(
new DownloadFile(info, download_manager_));
download_file->Rename(cr_path);
// This creates the .crdownload version of the file.
download_file->Initialize(false);
// |download_file| is owned by DownloadFileManager.
AddDownloadToFileManager(info->download_id, download_file);
info->path = new_path;
AttachDownloadItem(info);
message_loop_.RunAllPending();
EXPECT_TRUE(GetActiveDownloadItem(0) != NULL);
download_file->AppendDataToFile(kTestData, kTestDataLen);
// Finish the download.
OnAllDataSaved(0, kTestDataLen, "");
message_loop_.RunAllPending();
// Download is complete.
EXPECT_TRUE(GetActiveDownloadItem(0) == NULL);
EXPECT_TRUE(observer->hit_state(DownloadItem::IN_PROGRESS));
EXPECT_FALSE(observer->hit_state(DownloadItem::CANCELLED));
EXPECT_FALSE(observer->hit_state(DownloadItem::INTERRUPTED));
EXPECT_TRUE(observer->hit_state(DownloadItem::COMPLETE));
EXPECT_FALSE(observer->hit_state(DownloadItem::REMOVING));
EXPECT_TRUE(observer->was_updated());
EXPECT_FALSE(observer->was_opened());
EXPECT_EQ(DownloadItem::COMPLETE, download->state());
EXPECT_TRUE(file_util::PathExists(new_path));
EXPECT_FALSE(file_util::PathExists(cr_path));
EXPECT_FALSE(file_util::PathExists(unique_new_path));
std::string file_contents;
EXPECT_TRUE(file_util::ReadFileToString(new_path, &file_contents));
EXPECT_EQ(std::string(kTestData), file_contents);
}