blob: bce3fe4f40954b7b532de336d9d4eb5570ffed80 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/safe_browsing/incident_reporting/download_metadata_manager.h"
#include <stdint.h>
#include <memory>
#include <string>
#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/run_loop.h"
#include "base/sequenced_task_runner.h"
#include "chrome/test/base/testing_profile.h"
#include "components/safe_browsing/proto/csd.pb.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/download_manager.h"
#include "content/public/test/mock_download_item.h"
#include "content/public/test/mock_download_manager.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "content/public/test/test_utils.cc"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::AllOf;
using ::testing::Eq;
using ::testing::IsNull;
using ::testing::Ne;
using ::testing::NiceMock;
using ::testing::NotNull;
using ::testing::ResultOf;
using ::testing::Return;
using ::testing::SaveArg;
using ::testing::StrEq;
using ::testing::_;
namespace safe_browsing {
namespace {
const uint32_t kTestDownloadId = 47;
const uint32_t kOtherDownloadId = 48;
const uint32_t kCrazyDowloadId = 655;
const int64_t kTestDownloadTimeMsec = 84;
const char kTestUrl[] = "http://test.test/foo";
const uint64_t kTestDownloadLength = 1000;
const double kTestDownloadEndTimeMs = 1413514824057;
// A utility class suitable for mocking that exposes a
// GetDownloadDetailsCallback.
class DownloadDetailsGetter {
public:
virtual ~DownloadDetailsGetter() {}
virtual void OnDownloadDetails(
ClientIncidentReport_DownloadDetails* details) = 0;
DownloadMetadataManager::GetDownloadDetailsCallback GetCallback() {
return base::Bind(&DownloadDetailsGetter::DownloadDetailsCallback,
base::Unretained(this));
}
private:
void DownloadDetailsCallback(
std::unique_ptr<ClientIncidentReport_DownloadDetails> details) {
OnDownloadDetails(details.get());
}
};
// A mock DownloadDetailsGetter.
class MockDownloadDetailsGetter : public DownloadDetailsGetter {
public:
MOCK_METHOD1(OnDownloadDetails, void(ClientIncidentReport_DownloadDetails*));
};
// A mock DownloadMetadataManager that can be used to map a BrowserContext to
// a DownloadManager.
class MockDownloadMetadataManager : public DownloadMetadataManager {
public:
MockDownloadMetadataManager() = default;
MOCK_METHOD1(GetDownloadManagerForBrowserContext,
content::DownloadManager*(content::BrowserContext*));
};
// A helper function that returns the download URL from a DownloadDetails.
const std::string& GetDetailsDownloadUrl(
const ClientIncidentReport_DownloadDetails* details) {
return details->download().url();
}
// A helper function that returns the open time from a DownloadDetails.
int64_t GetDetailsOpenTime(
const ClientIncidentReport_DownloadDetails* details) {
return details->open_time_msec();
}
} // namespace
// The basis upon which unit tests of the DownloadMetadataManager are built.
class DownloadMetadataManagerTestBase : public ::testing::Test {
protected:
// Sets up a DownloadMetadataManager that will run tasks on the main test
// thread.
DownloadMetadataManagerTestBase() = default;
// Returns the path to the test profile's DownloadMetadata file.
base::FilePath GetMetadataPath() const {
return profile_.GetPath().Append(FILE_PATH_LITERAL("DownloadMetadata"));
}
// Returns a new ClientDownloadRequest for the given download URL.
static std::unique_ptr<ClientDownloadRequest> MakeTestRequest(
const char* url) {
std::unique_ptr<ClientDownloadRequest> request(new ClientDownloadRequest());
request->set_url(url);
request->mutable_digests();
request->set_length(kTestDownloadLength);
return request;
}
// Returns a new DownloadMetdata for the given download id.
static std::unique_ptr<DownloadMetadata> GetTestMetadata(
uint32_t download_id) {
std::unique_ptr<DownloadMetadata> metadata(new DownloadMetadata());
metadata->set_download_id(download_id);
ClientIncidentReport_DownloadDetails* details =
metadata->mutable_download();
details->set_download_time_msec(kTestDownloadTimeMsec);
details->set_allocated_download(MakeTestRequest(kTestUrl).release());
return metadata;
}
// Writes a test DownloadMetadata file for the given download id to the
// test profile directory.
void WriteTestMetadataFileForItem(uint32_t download_id) {
std::string data;
ASSERT_TRUE(GetTestMetadata(download_id)->SerializeToString(&data));
ASSERT_EQ(static_cast<int>(data.size()),
base::WriteFile(GetMetadataPath(), data.data(), data.size()));
}
// Writes a test DownloadMetadata file for kTestDownloadId to the test profile
// directory.
void WriteTestMetadataFile() {
WriteTestMetadataFileForItem(kTestDownloadId);
}
// Returns the DownloadMetadata read from the test profile's directory.
std::unique_ptr<DownloadMetadata> ReadTestMetadataFile() const {
std::string data;
if (!base::ReadFileToString(GetMetadataPath(), &data))
return std::unique_ptr<DownloadMetadata>();
std::unique_ptr<DownloadMetadata> result(new DownloadMetadata);
EXPECT_TRUE(result->ParseFromString(data));
return result;
}
// Runs all tasks posted to the test thread's message loop.
void RunAllTasks() { content::RunAllTasksUntilIdle(); }
// Adds a DownloadManager for the test profile. The DownloadMetadataManager's
// observer is stashed for later use. Only call once per call to
// ShutdownDownloadManager.
void AddDownloadManager() {
ASSERT_EQ(nullptr, dm_observer_);
// Shove the manager into the browser context.
ON_CALL(download_manager_, GetBrowserContext())
.WillByDefault(Return(&profile_));
ON_CALL(manager_, GetDownloadManagerForBrowserContext(Eq(&profile_)))
.WillByDefault(Return(&download_manager_));
// Capture the metadata manager's observer on the download manager.
EXPECT_CALL(download_manager_, AddObserver(&manager_))
.WillOnce(SaveArg<0>(&dm_observer_));
manager_.AddDownloadManager(&download_manager_);
}
// Shuts down the DownloadManager. Safe to call any number of times.
void ShutdownDownloadManager() {
if (dm_observer_) {
dm_observer_->ManagerGoingDown(&download_manager_);
// Note: these calls may result in "Uninteresting mock function call"
// warnings as a result of MockDownloadItem invoking observers in its
// dtor. This happens after the NiceMock wrapper has removed its niceness
// hook. These can safely be ignored, as they are entirely expected. The
// values specified by ON_CALL invocations in AddDownloadItems are
// returned as desired.
other_item_.reset();
test_item_.reset();
zero_item_.reset();
dm_observer_ = nullptr;
}
}
// Adds two test DownloadItems to the DownloadManager.
void AddDownloadItems() {
ASSERT_NE(nullptr, dm_observer_);
// Add the item under test.
test_item_.reset(new NiceMock<content::MockDownloadItem>);
ON_CALL(*test_item_, GetId())
.WillByDefault(Return(kTestDownloadId));
ON_CALL(*test_item_, GetBrowserContext())
.WillByDefault(Return(&profile_));
ON_CALL(*test_item_, GetEndTime())
.WillByDefault(Return(base::Time::FromJsTime(kTestDownloadEndTimeMs)));
ON_CALL(*test_item_, GetState())
.WillByDefault(Return(content::DownloadItem::COMPLETE));
dm_observer_->OnDownloadCreated(&download_manager_, test_item_.get());
// Add another item.
other_item_.reset(new NiceMock<content::MockDownloadItem>);
ON_CALL(*other_item_, GetId())
.WillByDefault(Return(kOtherDownloadId));
ON_CALL(*other_item_, GetBrowserContext())
.WillByDefault(Return(&profile_));
ON_CALL(*test_item_, GetEndTime())
.WillByDefault(Return(base::Time::FromJsTime(kTestDownloadEndTimeMs)));
dm_observer_->OnDownloadCreated(&download_manager_, other_item_.get());
// Add an item with an id of zero.
zero_item_.reset(new NiceMock<content::MockDownloadItem>);
ON_CALL(*zero_item_, GetId())
.WillByDefault(Return(0));
ON_CALL(*zero_item_, GetBrowserContext())
.WillByDefault(Return(&profile_));
ON_CALL(*zero_item_, GetEndTime())
.WillByDefault(Return(base::Time::FromJsTime(kTestDownloadEndTimeMs)));
ON_CALL(*zero_item_, GetState())
.WillByDefault(Return(content::DownloadItem::COMPLETE));
dm_observer_->OnDownloadCreated(&download_manager_, zero_item_.get());
ON_CALL(download_manager_, GetAllDownloads(_))
.WillByDefault(
Invoke(this, &DownloadMetadataManagerTestBase::GetAllDownloads));
}
// An implementation of the MockDownloadManager's
// DownloadManager::GetAllDownloads method that returns all items.
void GetAllDownloads(content::DownloadManager::DownloadVector* downloads) {
downloads->clear();
if (test_item_)
downloads->push_back(test_item_.get());
if (other_item_)
downloads->push_back(other_item_.get());
if (zero_item_)
downloads->push_back(zero_item_.get());
}
content::TestBrowserThreadBundle thread_bundle_;
NiceMock<MockDownloadMetadataManager> manager_;
TestingProfile profile_;
NiceMock<content::MockDownloadManager> download_manager_;
std::unique_ptr<content::MockDownloadItem> test_item_;
std::unique_ptr<content::MockDownloadItem> other_item_;
std::unique_ptr<content::MockDownloadItem> zero_item_;
// The DownloadMetadataManager's content::DownloadManager::Observer. Captured
// by download_manager_'s AddObserver action.
content::DownloadManager::Observer* dm_observer_ = nullptr;
};
// A parameterized test that exercises GetDownloadDetails. The parameters
// dictate the exact state of affairs leading up to the call as follows:
// 0: if "present", the profile has a pre-existing DownloadMetadata file.
// 1: if "managed", the profile's DownloadManager has been created.
// 2: the state of the DownloadItem prior to the call:
// "not_created": the DownloadItem has not been created.
// "created": the DownloadItem has been created.
// "opened": the DownloadItem has been opened.
// "removed": the DownloadItem has been removed.
// 3: if "loaded", the task to load the DownloadMetadata file is allowed to
// complete.
// 4: if "early_shutdown", the DownloadManager is shut down before the callback
// is allowed to complete.
class GetDetailsTest
: public DownloadMetadataManagerTestBase,
public ::testing::WithParamInterface<testing::tuple<const char*,
const char*,
const char*,
const char*,
const char*>> {
protected:
enum DownloadItemAction {
NOT_CREATED,
CREATED,
OPENED,
REMOVED,
};
GetDetailsTest()
: metadata_file_present_(),
manager_added_(),
item_action_(NOT_CREATED),
details_loaded_(),
early_shutdown_() {}
void SetUp() override {
DownloadMetadataManagerTestBase::SetUp();
metadata_file_present_ =
(std::string(testing::get<0>(GetParam())) == "present");
manager_added_ = (std::string(testing::get<1>(GetParam())) == "managed");
const std::string item_action(testing::get<2>(GetParam()));
item_action_ = (item_action == "not_created" ? NOT_CREATED :
(item_action == "created" ? CREATED :
(item_action == "opened" ? OPENED : REMOVED)));
details_loaded_ = (std::string(testing::get<3>(GetParam())) == "loaded");
early_shutdown_ =
(std::string(testing::get<4>(GetParam())) == "early_shutdown");
// Fixup combinations that don't make sense.
if (!manager_added_)
item_action_ = NOT_CREATED;
}
bool metadata_file_present_;
bool manager_added_;
DownloadItemAction item_action_;
bool details_loaded_;
bool early_shutdown_;
};
// Tests that DownloadMetadataManager::GetDownloadDetails works for all
// combinations of states.
TEST_P(GetDetailsTest, GetDownloadDetails) {
// Optionally put a metadata file in the profile directory.
if (metadata_file_present_)
WriteTestMetadataFile();
// Optionally add a download manager for the profile.
if (manager_added_)
AddDownloadManager();
// Optionally create download items and perform actions on the one under test.
if (item_action_ != NOT_CREATED)
AddDownloadItems();
if (item_action_ == OPENED)
test_item_->NotifyObserversDownloadOpened();
else if (item_action_ == REMOVED)
test_item_->NotifyObserversDownloadRemoved();
// Optionally allow the task to read the file to complete.
if (details_loaded_)
RunAllTasks();
// In http://crbug.com/433928, open after removal during load caused a crash.
if (item_action_ == REMOVED)
test_item_->NotifyObserversDownloadOpened();
MockDownloadDetailsGetter details_getter;
if (metadata_file_present_ && item_action_ != REMOVED) {
// The file is present, so expect that the callback is invoked with the
// details of the test download data written by WriteTestMetadataFile.
if (item_action_ == OPENED) {
EXPECT_CALL(details_getter,
OnDownloadDetails(
AllOf(ResultOf(GetDetailsDownloadUrl, StrEq(kTestUrl)),
ResultOf(GetDetailsOpenTime, Ne(0)))));
} else {
EXPECT_CALL(details_getter,
OnDownloadDetails(
AllOf(ResultOf(GetDetailsDownloadUrl, StrEq(kTestUrl)),
ResultOf(GetDetailsOpenTime, Eq(0)))));
}
} else {
// No file on disk, so expect that the callback is invoked with null.
EXPECT_CALL(details_getter, OnDownloadDetails(IsNull()));
}
// Fire in the hole!
manager_.GetDownloadDetails(&profile_, details_getter.GetCallback());
// Shutdown the download manager, if relevant.
if (early_shutdown_)
ShutdownDownloadManager();
// Allow the read task and the response callback to run.
RunAllTasks();
// Shutdown the download manager, if relevant.
ShutdownDownloadManager();
}
INSTANTIATE_TEST_CASE_P(
DownloadMetadataManager,
GetDetailsTest,
testing::Combine(
testing::Values("absent", "present"),
testing::Values("not_managed", "managed"),
testing::Values("not_created", "created", "opened", "removed"),
testing::Values("waiting", "loaded"),
testing::Values("normal_shutdown", "early_shutdown")));
// A parameterized test that exercises SetRequest. The parameters dictate the
// exact state of affairs leading up to the call as follows:
// 0: the state of the DownloadMetadata file for the test profile:
// "absent": no file is present.
// "this": the file corresponds to the item being updated.
// "other": the file correponds to a different item.
// "unknown": the file corresponds to an item that has not been created.
// 1: if "pending", an operation is applied to the item being updated prior to
// the call.
// 2: if "pending", an operation is applied to a different item prior to the
// call.
// 3: if "loaded", the task to load the DownloadMetadata file is allowed to
// complete.
// 4: if "set", the call to SetRequest contains a new request; otherwise it
// does not, leading to removal of metadata.
class SetRequestTest
: public DownloadMetadataManagerTestBase,
public ::testing::WithParamInterface<testing::tuple<const char*,
const char*,
const char*,
const char*,
const char*>> {
protected:
enum MetadataFilePresent {
ABSENT,
PRESENT_FOR_THIS_ITEM,
PRESENT_FOR_OTHER_ITEM,
PRESENT_FOR_UNKNOWN_ITEM,
};
SetRequestTest()
: metadata_file_present_(ABSENT),
same_ops_(),
other_ops_(),
details_loaded_(),
set_request_() {}
void SetUp() override {
DownloadMetadataManagerTestBase::SetUp();
const std::string present(testing::get<0>(GetParam()));
metadata_file_present_ = (present == "absent" ? ABSENT :
(present == "this" ? PRESENT_FOR_THIS_ITEM :
(present == "other" ? PRESENT_FOR_OTHER_ITEM :
PRESENT_FOR_UNKNOWN_ITEM)));
same_ops_ = (std::string(testing::get<1>(GetParam())) == "pending");
other_ops_ = (std::string(testing::get<2>(GetParam())) == "pending");
details_loaded_ = (std::string(testing::get<3>(GetParam())) == "loaded");
set_request_ = (std::string(testing::get<4>(GetParam())) == "set");
}
MetadataFilePresent metadata_file_present_;
bool same_ops_;
bool other_ops_;
bool details_loaded_;
bool set_request_;
};
// Tests that DownloadMetadataManager::SetRequest works for all combinations of
// states.
TEST_P(SetRequestTest, SetRequest) {
// Optionally put a metadata file in the profile directory.
switch (metadata_file_present_) {
case ABSENT:
break;
case PRESENT_FOR_THIS_ITEM:
WriteTestMetadataFile();
break;
case PRESENT_FOR_OTHER_ITEM:
WriteTestMetadataFileForItem(kOtherDownloadId);
break;
case PRESENT_FOR_UNKNOWN_ITEM:
WriteTestMetadataFileForItem(kCrazyDowloadId);
break;
}
AddDownloadManager();
AddDownloadItems();
// Optionally allow the task to read the file to complete.
if (details_loaded_) {
RunAllTasks();
} else {
// Optionally add pending operations if the load is outstanding.
if (same_ops_)
test_item_->NotifyObserversDownloadOpened();
if (other_ops_)
other_item_->NotifyObserversDownloadOpened();
}
static const char kNewUrl[] = "http://blorf";
if (set_request_)
manager_.SetRequest(test_item_.get(), MakeTestRequest(kNewUrl).get());
// Allow the write or remove task to run.
RunAllTasks();
if (set_request_) {
MockDownloadDetailsGetter details_getter;
// Expect that the callback is invoked with details for this item.
EXPECT_CALL(
details_getter,
OnDownloadDetails(ResultOf(GetDetailsDownloadUrl, StrEq(kNewUrl))));
manager_.GetDownloadDetails(&profile_, details_getter.GetCallback());
}
// In http://crbug.com/433928, open after SetRequest(nullpr) caused a crash.
test_item_->NotifyObserversDownloadOpened();
ShutdownDownloadManager();
std::unique_ptr<DownloadMetadata> metadata(ReadTestMetadataFile());
if (set_request_) {
// Expect that the file contains metadata for the download.
ASSERT_TRUE(metadata);
EXPECT_EQ(kTestDownloadId, metadata->download_id());
EXPECT_STREQ(kNewUrl, metadata->download().download().url().c_str());
} else if (metadata_file_present_) {
// Expect that the metadata file has not been overwritten.
ASSERT_TRUE(metadata);
} else {
// Expect that the file is not present.
ASSERT_FALSE(metadata);
}
}
INSTANTIATE_TEST_CASE_P(
DownloadMetadataManager,
SetRequestTest,
testing::Combine(testing::Values("absent", "this", "other", "unknown"),
testing::Values("none", "pending"),
testing::Values("none", "pending"),
testing::Values("waiting", "loaded"),
testing::Values("clear", "set")));
TEST_F(DownloadMetadataManagerTestBase, ActiveDownloadNoRequest) {
// Put some metadata on disk from a previous download.
WriteTestMetadataFileForItem(kOtherDownloadId);
AddDownloadManager();
AddDownloadItems();
// Allow everything to load into steady-state.
RunAllTasks();
// The test item is in progress.
ON_CALL(*test_item_, GetState())
.WillByDefault(Return(content::DownloadItem::IN_PROGRESS));
test_item_->NotifyObserversDownloadUpdated();
test_item_->NotifyObserversDownloadUpdated();
// The test item completes.
ON_CALL(*test_item_, GetState())
.WillByDefault(Return(content::DownloadItem::COMPLETE));
test_item_->NotifyObserversDownloadUpdated();
RunAllTasks();
ShutdownDownloadManager();
// Expect that the metadata file is still present.
std::unique_ptr<DownloadMetadata> metadata(ReadTestMetadataFile());
ASSERT_TRUE(metadata);
EXPECT_EQ(kOtherDownloadId, metadata->download_id());
}
TEST_F(DownloadMetadataManagerTestBase, ActiveDownloadWithRequest) {
// Put some metadata on disk from a previous download.
WriteTestMetadataFileForItem(kOtherDownloadId);
AddDownloadManager();
AddDownloadItems();
// Allow everything to load into steady-state.
RunAllTasks();
// The test item is in progress.
ON_CALL(*test_item_, GetState())
.WillByDefault(Return(content::DownloadItem::IN_PROGRESS));
test_item_->NotifyObserversDownloadUpdated();
// A request is set for it.
static const char kNewUrl[] = "http://blorf";
manager_.SetRequest(test_item_.get(), MakeTestRequest(kNewUrl).get());
test_item_->NotifyObserversDownloadUpdated();
// The test item completes.
ON_CALL(*test_item_, GetState())
.WillByDefault(Return(content::DownloadItem::COMPLETE));
test_item_->NotifyObserversDownloadUpdated();
RunAllTasks();
MockDownloadDetailsGetter details_getter;
// Expect that the callback is invoked with details for this item.
EXPECT_CALL(
details_getter,
OnDownloadDetails(ResultOf(GetDetailsDownloadUrl, StrEq(kNewUrl))));
manager_.GetDownloadDetails(&profile_, details_getter.GetCallback());
ShutdownDownloadManager();
// Expect that the file contains metadata for the download.
std::unique_ptr<DownloadMetadata> metadata(ReadTestMetadataFile());
ASSERT_TRUE(metadata);
EXPECT_EQ(kTestDownloadId, metadata->download_id());
EXPECT_STREQ(kNewUrl, metadata->download().download().url().c_str());
}
// Regression test for http://crbug.com/504092: open an item with id==0 when
// there is no metadata loaded.
TEST_F(DownloadMetadataManagerTestBase, OpenItemWithZeroId) {
AddDownloadManager();
AddDownloadItems();
// Allow everything to load into steady-state.
RunAllTasks();
// Open the zero-id item.
zero_item_->NotifyObserversDownloadOpened();
ShutdownDownloadManager();
}
} // namespace safe_browsing