blob: bd201e3e1479aca278a65e47cec988e18081a00d [file] [log] [blame]
// Copyright (c) 2012 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/download/download_item_model.h"
#include <stddef.h>
#include <stdint.h>
#include <vector>
#include "base/i18n/rtl.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/strings/string16.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "components/download/public/common/mock_download_item.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/text/bytes_formatting.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/text_utils.h"
using download::DownloadItem;
using safe_browsing::DownloadFileType;
using ::testing::Mock;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::ReturnRef;
using ::testing::ReturnRefOfCopy;
using ::testing::SetArgPointee;
using ::testing::_;
namespace {
// Create a char array that has as many elements as there are download
// interrupt reasons. We can then use that in a static_assert to make sure
// that all the interrupt reason codes are accounted for. The reason codes are
// unfortunately sparse, making this necessary.
char kInterruptReasonCounter[] = {
0, // download::DOWNLOAD_INTERRUPT_REASON_NONE
#define INTERRUPT_REASON(name,value) 0,
#include "components/download/public/common/download_interrupt_reason_values.h"
#undef INTERRUPT_REASON
};
const size_t kInterruptReasonCount = arraysize(kInterruptReasonCounter);
// Default target path for a mock download item in DownloadItemModelTest.
const base::FilePath::CharType kDefaultTargetFilePath[] =
FILE_PATH_LITERAL("/foo/bar/foo.bar");
const base::FilePath::CharType kDefaultDisplayFileName[] =
FILE_PATH_LITERAL("foo.bar");
// Default URL for a mock download item in DownloadItemModelTest.
const char kDefaultURL[] = "http://example.com/foo.bar";
class DownloadItemModelTest : public testing::Test {
public:
DownloadItemModelTest()
: model_(&item_) {}
~DownloadItemModelTest() override {}
protected:
// Sets up defaults for the download item and sets |model_| to a new
// DownloadItemModel that uses the mock download item.
void SetupDownloadItemDefaults() {
ON_CALL(item_, GetReceivedBytes()).WillByDefault(Return(1));
ON_CALL(item_, GetTotalBytes()).WillByDefault(Return(2));
ON_CALL(item_, TimeRemaining(_)).WillByDefault(Return(false));
ON_CALL(item_, GetMimeType()).WillByDefault(Return("text/html"));
ON_CALL(item_, AllDataSaved()).WillByDefault(Return(false));
ON_CALL(item_, GetOpenWhenComplete()).WillByDefault(Return(false));
ON_CALL(item_, GetFileExternallyRemoved()).WillByDefault(Return(false));
ON_CALL(item_, GetState())
.WillByDefault(Return(DownloadItem::IN_PROGRESS));
ON_CALL(item_, GetURL())
.WillByDefault(ReturnRefOfCopy(GURL(kDefaultURL)));
ON_CALL(item_, GetFileNameToReportUser())
.WillByDefault(Return(base::FilePath(kDefaultDisplayFileName)));
ON_CALL(item_, GetTargetFilePath())
.WillByDefault(ReturnRefOfCopy(base::FilePath(kDefaultTargetFilePath)));
ON_CALL(item_, GetTargetDisposition())
.WillByDefault(
Return(DownloadItem::TARGET_DISPOSITION_OVERWRITE));
ON_CALL(item_, IsPaused()).WillByDefault(Return(false));
}
void SetupInterruptedDownloadItem(download::DownloadInterruptReason reason) {
EXPECT_CALL(item_, GetLastReason()).WillRepeatedly(Return(reason));
EXPECT_CALL(item_, GetState())
.WillRepeatedly(
Return((reason == download::DOWNLOAD_INTERRUPT_REASON_NONE)
? DownloadItem::IN_PROGRESS
: DownloadItem::INTERRUPTED));
}
download::MockDownloadItem& item() { return item_; }
DownloadItemModel& model() {
return model_;
}
private:
NiceMock<download::MockDownloadItem> item_;
DownloadItemModel model_;
};
} // namespace
TEST_F(DownloadItemModelTest, InterruptedStatus) {
// Test that we have the correct interrupt status message for downloads that
// are in the INTERRUPTED state.
const struct TestCase {
// The reason.
download::DownloadInterruptReason reason;
// Expected status string. This will include the progress as well.
const char* expected_status;
} kTestCases[] = {
{download::DOWNLOAD_INTERRUPT_REASON_NONE, "1/2 B"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_FAILED,
"Failed - Download error"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED,
"Failed - Insufficient permissions"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_NO_SPACE, "Failed - Disk full"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG,
"Failed - Path too long"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE,
"Failed - File too large"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_VIRUS_INFECTED,
"Failed - Virus detected"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED, "Failed - Blocked"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED,
"Failed - Virus scan failed"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_SHORT,
"Failed - File truncated"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_SAME_AS_SOURCE,
"Failed - Already downloaded"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR,
"Failed - System busy"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_HASH_MISMATCH,
"Failed - Download error"},
{download::DOWNLOAD_INTERRUPT_REASON_NETWORK_FAILED,
"Failed - Network error"},
{download::DOWNLOAD_INTERRUPT_REASON_NETWORK_TIMEOUT,
"Failed - Network timeout"},
{download::DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED,
"Failed - Network disconnected"},
{download::DOWNLOAD_INTERRUPT_REASON_NETWORK_SERVER_DOWN,
"Failed - Server unavailable"},
{download::DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST,
"Failed - Network error"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_FAILED,
"Failed - Server problem"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_NO_RANGE,
"Failed - Download error"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_BAD_CONTENT,
"Failed - No file"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_UNAUTHORIZED,
"Failed - Needs authorization"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_CERT_PROBLEM,
"Failed - Bad certificate"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_FORBIDDEN,
"Failed - Forbidden"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_UNREACHABLE,
"Failed - Server unreachable"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_CONTENT_LENGTH_MISMATCH,
"Failed - File incomplete"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_CROSS_ORIGIN_REDIRECT,
"Failed - Download error"},
{download::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED, "Canceled"},
{download::DOWNLOAD_INTERRUPT_REASON_USER_SHUTDOWN, "Failed - Shutdown"},
{download::DOWNLOAD_INTERRUPT_REASON_CRASH, "Failed - Crash"},
};
static_assert(kInterruptReasonCount == arraysize(kTestCases),
"interrupt reason mismatch");
SetupDownloadItemDefaults();
for (unsigned i = 0; i < arraysize(kTestCases); ++i) {
const TestCase& test_case = kTestCases[i];
SetupInterruptedDownloadItem(test_case.reason);
EXPECT_STREQ(test_case.expected_status,
base::UTF16ToUTF8(model().GetStatusText()).c_str());
}
}
// Note: This test is currently skipped on Android. See http://crbug.com/139398
TEST_F(DownloadItemModelTest, InterruptTooltip) {
// Test that we have the correct interrupt tooltip for downloads that are in
// the INTERRUPTED state.
const struct TestCase {
// The reason.
download::DownloadInterruptReason reason;
// Expected tooltip text. The tooltip text for interrupted downloads
// typically consist of two lines. One for the filename and one for the
// interrupt reason. The returned string contains a newline.
const char* expected_tooltip;
} kTestCases[] = {
{download::DOWNLOAD_INTERRUPT_REASON_NONE, "foo.bar"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_FAILED,
"foo.bar\nDownload error"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED,
"foo.bar\nInsufficient permissions"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_NO_SPACE, "foo.bar\nDisk full"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG,
"foo.bar\nPath too long"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE,
"foo.bar\nFile too large"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_VIRUS_INFECTED,
"foo.bar\nVirus detected"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED, "foo.bar\nBlocked"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED,
"foo.bar\nVirus scan failed"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_SHORT,
"foo.bar\nFile truncated"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_SAME_AS_SOURCE,
"foo.bar\nAlready downloaded"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR,
"foo.bar\nSystem busy"},
{download::DOWNLOAD_INTERRUPT_REASON_FILE_HASH_MISMATCH,
"foo.bar\nDownload error"},
{download::DOWNLOAD_INTERRUPT_REASON_NETWORK_FAILED,
"foo.bar\nNetwork error"},
{download::DOWNLOAD_INTERRUPT_REASON_NETWORK_TIMEOUT,
"foo.bar\nNetwork timeout"},
{download::DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED,
"foo.bar\nNetwork disconnected"},
{download::DOWNLOAD_INTERRUPT_REASON_NETWORK_SERVER_DOWN,
"foo.bar\nServer unavailable"},
{download::DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST,
"foo.bar\nNetwork error"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_FAILED,
"foo.bar\nServer problem"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_NO_RANGE,
"foo.bar\nDownload error"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_BAD_CONTENT,
"foo.bar\nNo file"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_UNAUTHORIZED,
"foo.bar\nNeeds authorization"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_CERT_PROBLEM,
"foo.bar\nBad certificate"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_FORBIDDEN,
"foo.bar\nForbidden"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_UNREACHABLE,
"foo.bar\nServer unreachable"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_CONTENT_LENGTH_MISMATCH,
"foo.bar\nFile incomplete"},
{download::DOWNLOAD_INTERRUPT_REASON_SERVER_CROSS_ORIGIN_REDIRECT,
"foo.bar\nDownload error"},
{download::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED, "foo.bar"},
{download::DOWNLOAD_INTERRUPT_REASON_USER_SHUTDOWN, "foo.bar\nShutdown"},
{download::DOWNLOAD_INTERRUPT_REASON_CRASH, "foo.bar\nCrash"},
};
static_assert(kInterruptReasonCount == arraysize(kTestCases),
"interrupt reason mismatch");
// Large tooltip width. Should be large enough to accommodate the entire
// tooltip without truncation.
const int kLargeTooltipWidth = 1000;
// Small tooltip width. Small enough to require truncation of most
// tooltips. Used to test eliding logic.
const int kSmallTooltipWidth = 40;
const gfx::FontList& font_list =
ui::ResourceBundle::GetSharedInstance().GetFontList(
ui::ResourceBundle::BaseFont);
SetupDownloadItemDefaults();
for (unsigned i = 0; i < arraysize(kTestCases); ++i) {
const TestCase& test_case = kTestCases[i];
SetupInterruptedDownloadItem(test_case.reason);
// GetTooltipText() elides the tooltip so that the text would fit within a
// given width. The following test would fail if kLargeTooltipWidth isn't
// large enough to accomodate all the strings.
EXPECT_STREQ(
test_case.expected_tooltip,
base::UTF16ToUTF8(model().GetTooltipText(font_list,
kLargeTooltipWidth)).c_str());
// Check that if the width is small, the returned tooltip only contains
// lines of the given width or smaller.
base::string16 truncated_tooltip =
model().GetTooltipText(font_list, kSmallTooltipWidth);
for (const base::string16& line :
base::SplitString(truncated_tooltip, base::ASCIIToUTF16("\n"),
base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY))
// Tooltips are always typeset with the native typesetter.
EXPECT_GE(kSmallTooltipWidth,
gfx::GetStringWidth(line, font_list, gfx::Typesetter::NATIVE));
}
}
TEST_F(DownloadItemModelTest, InProgressStatus) {
const struct TestCase {
int64_t received_bytes; // Return value of GetReceivedBytes().
int64_t total_bytes; // Return value of GetTotalBytes().
bool time_remaining_known; // If TimeRemaining() is known.
bool open_when_complete; // GetOpenWhenComplete().
bool is_paused; // IsPaused().
const char* expected_status; // Expected status text.
} kTestCases[] = {
// These are all the valid combinations of the above fields for a download
// that is in IN_PROGRESS state. Go through all of them and check the return
// value of DownloadItemModel::GetStatusText(). The point isn't to lock down
// the status strings, but to make sure we end up with something sane for
// all the circumstances we care about.
//
// For GetReceivedBytes()/GetTotalBytes(), we only check whether each is
// non-zero. In addition, if |total_bytes| is zero, then
// |time_remaining_known| is also false.
//
// .-- .TimeRemaining() is known.
// | .-- .GetOpenWhenComplete()
// | | .---- .IsPaused()
{ 0, 0, false, false, false, "Starting\xE2\x80\xA6" },
{ 1, 0, false, false, false, "1 B" },
{ 0, 2, false, false, false, "Starting\xE2\x80\xA6"},
{ 1, 2, false, false, false, "1/2 B" },
{ 0, 2, true, false, false, "0/2 B, 10 secs left" },
{ 1, 2, true, false, false, "1/2 B, 10 secs left" },
{ 0, 0, false, true, false, "Opening when complete" },
{ 1, 0, false, true, false, "Opening when complete" },
{ 0, 2, false, true, false, "Opening when complete" },
{ 1, 2, false, true, false, "Opening when complete" },
{ 0, 2, true, true, false, "Opening in 10 secs\xE2\x80\xA6"},
{ 1, 2, true, true, false, "Opening in 10 secs\xE2\x80\xA6"},
{ 0, 0, false, false, true, "0 B, Paused" },
{ 1, 0, false, false, true, "1 B, Paused" },
{ 0, 2, false, false, true, "0/2 B, Paused" },
{ 1, 2, false, false, true, "1/2 B, Paused" },
{ 0, 2, true, false, true, "0/2 B, Paused" },
{ 1, 2, true, false, true, "1/2 B, Paused" },
{ 0, 0, false, true, true, "0 B, Paused" },
{ 1, 0, false, true, true, "1 B, Paused" },
{ 0, 2, false, true, true, "0/2 B, Paused" },
{ 1, 2, false, true, true, "1/2 B, Paused" },
{ 0, 2, true, true, true, "0/2 B, Paused" },
{ 1, 2, true, true, true, "1/2 B, Paused" },
};
SetupDownloadItemDefaults();
for (unsigned i = 0; i < arraysize(kTestCases); i++) {
const TestCase& test_case = kTestCases[i];
Mock::VerifyAndClearExpectations(&item());
Mock::VerifyAndClearExpectations(&model());
EXPECT_CALL(item(), GetReceivedBytes())
.WillRepeatedly(Return(test_case.received_bytes));
EXPECT_CALL(item(), GetTotalBytes())
.WillRepeatedly(Return(test_case.total_bytes));
EXPECT_CALL(item(), TimeRemaining(_))
.WillRepeatedly(testing::DoAll(
testing::SetArgPointee<0>(base::TimeDelta::FromSeconds(10)),
Return(test_case.time_remaining_known)));
EXPECT_CALL(item(), GetOpenWhenComplete())
.WillRepeatedly(Return(test_case.open_when_complete));
EXPECT_CALL(item(), IsPaused())
.WillRepeatedly(Return(test_case.is_paused));
EXPECT_STREQ(test_case.expected_status,
base::UTF16ToUTF8(model().GetStatusText()).c_str());
}
}
TEST_F(DownloadItemModelTest, ShouldShowInShelf) {
SetupDownloadItemDefaults();
// By default the download item should be displayable on the shelf when it is
// not a transient download.
EXPECT_CALL(item(), IsTransient()).WillOnce(Return(false));
EXPECT_TRUE(model().ShouldShowInShelf());
EXPECT_CALL(item(), IsTransient()).WillOnce(Return(true));
EXPECT_FALSE(model().ShouldShowInShelf());
// Once explicitly set, ShouldShowInShelf() should return the explicit value
// regardless of whether it's a transient download, which should no longer
// be considered by the model after initializing it.
EXPECT_CALL(item(), IsTransient()).Times(1);
model().SetShouldShowInShelf(true);
EXPECT_TRUE(model().ShouldShowInShelf());
model().SetShouldShowInShelf(false);
EXPECT_FALSE(model().ShouldShowInShelf());
}
TEST_F(DownloadItemModelTest, DangerLevel) {
SetupDownloadItemDefaults();
// Default danger level is NOT_DANGEROUS.
EXPECT_EQ(DownloadFileType::NOT_DANGEROUS, model().GetDangerLevel());
model().SetDangerLevel(DownloadFileType::ALLOW_ON_USER_GESTURE);
EXPECT_EQ(DownloadFileType::ALLOW_ON_USER_GESTURE, model().GetDangerLevel());
}
TEST_F(DownloadItemModelTest, HasSupportedImageMimeType) {
SetupDownloadItemDefaults();
// When the item has a supported image MIME type, true should be returned.
ON_CALL(item(), GetMimeType()).WillByDefault(Return("image/png"));
EXPECT_TRUE(model().HasSupportedImageMimeType());
// An unsupported MIME type should result in false being returned...
ON_CALL(item(), GetMimeType()).WillByDefault(Return("image/unsupported"));
EXPECT_FALSE(model().HasSupportedImageMimeType());
// ... unless the target path has a well-known image extension.
const base::FilePath kImagePath(FILE_PATH_LITERAL("/foo/image.png"));
ON_CALL(item(), GetTargetFilePath()).WillByDefault(ReturnRef(kImagePath));
EXPECT_TRUE(model().HasSupportedImageMimeType());
// .txt and missing extensions should also result in false being returned.
const base::FilePath kTextPath(FILE_PATH_LITERAL("/foo/image.txt"));
ON_CALL(item(), GetTargetFilePath()).WillByDefault(ReturnRef(kTextPath));
EXPECT_FALSE(model().HasSupportedImageMimeType());
const base::FilePath kNoExtensionPath(FILE_PATH_LITERAL("/foo/image."));
ON_CALL(item(), GetTargetFilePath())
.WillByDefault(ReturnRef(kNoExtensionPath));
EXPECT_FALSE(model().HasSupportedImageMimeType());
}
TEST_F(DownloadItemModelTest, ShouldRemoveFromShelfWhenComplete) {
const struct TestCase {
DownloadItem::DownloadState state;
bool is_dangerous; // Expectation for IsDangerous().
bool is_auto_open; // Expectation for GetOpenWhenComplete().
bool auto_opened; // Whether the download was successfully
// auto-opened. Expecation for GetAutoOpened().
bool expected_result;
} kTestCases[] = {
// All the valid combinations of state, is_dangerous, is_auto_open and
// auto_opened.
//
// .--- Is dangerous.
// | .--- Auto open or temporary.
// | | .--- Auto opened.
// | | | .--- Expected result.
{ DownloadItem::IN_PROGRESS, false, false, false, false},
{ DownloadItem::IN_PROGRESS, false, true , false, true },
{ DownloadItem::IN_PROGRESS, true , false, false, false},
{ DownloadItem::IN_PROGRESS, true , true , false, false},
{ DownloadItem::COMPLETE, false, false, false, false},
{ DownloadItem::COMPLETE, false, true , false, false},
{ DownloadItem::COMPLETE, false, false, true , true },
{ DownloadItem::COMPLETE, false, true , true , true },
{ DownloadItem::CANCELLED, false, false, false, false},
{ DownloadItem::CANCELLED, false, true , false, false},
{ DownloadItem::CANCELLED, true , false, false, false},
{ DownloadItem::CANCELLED, true , true , false, false},
{ DownloadItem::INTERRUPTED, false, false, false, false},
{ DownloadItem::INTERRUPTED, false, true , false, false},
{ DownloadItem::INTERRUPTED, true , false, false, false},
{ DownloadItem::INTERRUPTED, true , true , false, false}
};
SetupDownloadItemDefaults();
for (unsigned i = 0; i < arraysize(kTestCases); i++) {
const TestCase& test_case = kTestCases[i];
EXPECT_CALL(item(), GetOpenWhenComplete())
.WillRepeatedly(Return(test_case.is_auto_open));
EXPECT_CALL(item(), GetState())
.WillRepeatedly(Return(test_case.state));
EXPECT_CALL(item(), IsDangerous())
.WillRepeatedly(Return(test_case.is_dangerous));
EXPECT_CALL(item(), GetAutoOpened())
.WillRepeatedly(Return(test_case.auto_opened));
EXPECT_EQ(test_case.expected_result,
model().ShouldRemoveFromShelfWhenComplete())
<< "Test case: " << i;
Mock::VerifyAndClearExpectations(&item());
Mock::VerifyAndClearExpectations(&model());
}
}