1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/download/download_item_model.h" 6 7 #include <vector> 8 9 #include "base/i18n/rtl.h" 10 #include "base/logging.h" 11 #include "base/message_loop/message_loop.h" 12 #include "base/strings/string16.h" 13 #include "base/strings/string_util.h" 14 #include "base/strings/utf_string_conversions.h" 15 #include "content/public/test/mock_download_item.h" 16 #include "extensions/common/extension.h" 17 #include "grit/generated_resources.h" 18 #include "testing/gmock/include/gmock/gmock.h" 19 #include "testing/gtest/include/gtest/gtest.h" 20 #include "ui/base/l10n/l10n_util.h" 21 #include "ui/base/resource/resource_bundle.h" 22 #include "ui/base/text/bytes_formatting.h" 23 #include "ui/gfx/font_list.h" 24 #include "ui/gfx/text_utils.h" 25 26 using content::DownloadItem; 27 using ::testing::Mock; 28 using ::testing::NiceMock; 29 using ::testing::Return; 30 using ::testing::ReturnRefOfCopy; 31 using ::testing::SetArgPointee; 32 using ::testing::_; 33 34 namespace { 35 36 // Create a char array that has as many elements as there are download 37 // interrupt reasons. We can then use that in a COMPILE_ASSERT to make sure 38 // that all the interrupt reason codes are accounted for. The reason codes are 39 // unfortunately sparse, making this necessary. 40 char kInterruptReasonCounter[] = { 41 0, // content::DOWNLOAD_INTERRUPT_REASON_NONE 42 #define INTERRUPT_REASON(name,value) 0, 43 #include "content/public/browser/download_interrupt_reason_values.h" 44 #undef INTERRUPT_REASON 45 }; 46 const size_t kInterruptReasonCount = ARRAYSIZE_UNSAFE(kInterruptReasonCounter); 47 48 // Default target path for a mock download item in DownloadItemModelTest. 49 const base::FilePath::CharType kDefaultTargetFilePath[] = 50 FILE_PATH_LITERAL("/foo/bar/foo.bar"); 51 52 const base::FilePath::CharType kDefaultDisplayFileName[] = 53 FILE_PATH_LITERAL("foo.bar"); 54 55 // Default URL for a mock download item in DownloadItemModelTest. 56 const char kDefaultURL[] = "http://example.com/foo.bar"; 57 58 class DownloadItemModelTest : public testing::Test { 59 public: 60 DownloadItemModelTest() 61 : model_(&item_) {} 62 63 virtual ~DownloadItemModelTest() { 64 } 65 66 protected: 67 // Sets up defaults for the download item and sets |model_| to a new 68 // DownloadItemModel that uses the mock download item. 69 void SetupDownloadItemDefaults() { 70 ON_CALL(item_, GetReceivedBytes()).WillByDefault(Return(1)); 71 ON_CALL(item_, GetTotalBytes()).WillByDefault(Return(2)); 72 ON_CALL(item_, TimeRemaining(_)).WillByDefault(Return(false)); 73 ON_CALL(item_, GetMimeType()).WillByDefault(Return("text/html")); 74 ON_CALL(item_, AllDataSaved()).WillByDefault(Return(false)); 75 ON_CALL(item_, GetOpenWhenComplete()).WillByDefault(Return(false)); 76 ON_CALL(item_, GetFileExternallyRemoved()).WillByDefault(Return(false)); 77 ON_CALL(item_, GetState()) 78 .WillByDefault(Return(DownloadItem::IN_PROGRESS)); 79 ON_CALL(item_, GetURL()) 80 .WillByDefault(ReturnRefOfCopy(GURL(kDefaultURL))); 81 ON_CALL(item_, GetFileNameToReportUser()) 82 .WillByDefault(Return(base::FilePath(kDefaultDisplayFileName))); 83 ON_CALL(item_, GetTargetFilePath()) 84 .WillByDefault(ReturnRefOfCopy(base::FilePath(kDefaultTargetFilePath))); 85 ON_CALL(item_, GetTargetDisposition()) 86 .WillByDefault( 87 Return(DownloadItem::TARGET_DISPOSITION_OVERWRITE)); 88 ON_CALL(item_, IsPaused()).WillByDefault(Return(false)); 89 } 90 91 void SetupInterruptedDownloadItem(content::DownloadInterruptReason reason) { 92 EXPECT_CALL(item_, GetLastReason()).WillRepeatedly(Return(reason)); 93 EXPECT_CALL(item_, GetState()) 94 .WillRepeatedly(Return( 95 (reason == content::DOWNLOAD_INTERRUPT_REASON_NONE) ? 96 DownloadItem::IN_PROGRESS : 97 DownloadItem::INTERRUPTED)); 98 } 99 100 content::MockDownloadItem& item() { 101 return item_; 102 } 103 104 DownloadItemModel& model() { 105 return model_; 106 } 107 108 private: 109 NiceMock<content::MockDownloadItem> item_; 110 DownloadItemModel model_; 111 }; 112 113 } // namespace 114 115 TEST_F(DownloadItemModelTest, InterruptedStatus) { 116 // Test that we have the correct interrupt status message for downloads that 117 // are in the INTERRUPTED state. 118 const struct TestCase { 119 // The reason. 120 content::DownloadInterruptReason reason; 121 122 // Expected status string. This will include the progress as well. 123 const char* expected_status; 124 } kTestCases[] = { 125 { content::DOWNLOAD_INTERRUPT_REASON_NONE, 126 "1/2 B" }, 127 { content::DOWNLOAD_INTERRUPT_REASON_FILE_FAILED, 128 "Failed - Download error" }, 129 { content::DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED, 130 "Failed - Insufficient permissions" }, 131 { content::DOWNLOAD_INTERRUPT_REASON_FILE_NO_SPACE, 132 "Failed - Disk full" }, 133 { content::DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG, 134 "Failed - Path too long" }, 135 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE, 136 "Failed - File too large" }, 137 { content::DOWNLOAD_INTERRUPT_REASON_FILE_VIRUS_INFECTED, 138 "Failed - Virus detected" }, 139 { content::DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED, 140 "Failed - Blocked" }, 141 { content::DOWNLOAD_INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED, 142 "Failed - Virus scan failed" }, 143 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_SHORT, 144 "Failed - File truncated" }, 145 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR, 146 "Failed - System busy" }, 147 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_FAILED, 148 "Failed - Network error" }, 149 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_TIMEOUT, 150 "Failed - Network timeout" }, 151 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED, 152 "Failed - Network disconnected" }, 153 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_SERVER_DOWN, 154 "Failed - Server unavailable" }, 155 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST, 156 "Failed - Network error" }, 157 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_FAILED, 158 "Failed - Server problem" }, 159 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_NO_RANGE, 160 "Failed - Download error" }, 161 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_PRECONDITION, 162 "Failed - Download error" }, 163 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_BAD_CONTENT, 164 "Failed - No file" }, 165 { content::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED, 166 "Cancelled" }, 167 { content::DOWNLOAD_INTERRUPT_REASON_USER_SHUTDOWN, 168 "Failed - Shutdown" }, 169 { content::DOWNLOAD_INTERRUPT_REASON_CRASH, 170 "Failed - Crash" }, 171 }; 172 COMPILE_ASSERT(kInterruptReasonCount == ARRAYSIZE_UNSAFE(kTestCases), 173 interrupt_reason_mismatch); 174 175 SetupDownloadItemDefaults(); 176 for (unsigned i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); ++i) { 177 const TestCase& test_case = kTestCases[i]; 178 SetupInterruptedDownloadItem(test_case.reason); 179 EXPECT_STREQ(test_case.expected_status, 180 base::UTF16ToUTF8(model().GetStatusText()).c_str()); 181 } 182 } 183 184 // Note: This test is currently skipped on Android. See http://crbug.com/139398 185 TEST_F(DownloadItemModelTest, InterruptTooltip) { 186 // Test that we have the correct interrupt tooltip for downloads that are in 187 // the INTERRUPTED state. 188 const struct TestCase { 189 // The reason. 190 content::DownloadInterruptReason reason; 191 192 // Expected tooltip text. The tooltip text for interrupted downloads 193 // typically consist of two lines. One for the filename and one for the 194 // interrupt reason. The returned string contains a newline. 195 const char* expected_tooltip; 196 } kTestCases[] = { 197 { content::DOWNLOAD_INTERRUPT_REASON_NONE, 198 "foo.bar" }, 199 { content::DOWNLOAD_INTERRUPT_REASON_FILE_FAILED, 200 "foo.bar\nDownload error" }, 201 { content::DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED, 202 "foo.bar\nInsufficient permissions" }, 203 { content::DOWNLOAD_INTERRUPT_REASON_FILE_NO_SPACE, 204 "foo.bar\nDisk full" }, 205 { content::DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG, 206 "foo.bar\nPath too long" }, 207 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE, 208 "foo.bar\nFile too large" }, 209 { content::DOWNLOAD_INTERRUPT_REASON_FILE_VIRUS_INFECTED, 210 "foo.bar\nVirus detected" }, 211 { content::DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED, 212 "foo.bar\nBlocked" }, 213 { content::DOWNLOAD_INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED, 214 "foo.bar\nVirus scan failed" }, 215 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_SHORT, 216 "foo.bar\nFile truncated" }, 217 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR, 218 "foo.bar\nSystem busy" }, 219 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_FAILED, 220 "foo.bar\nNetwork error" }, 221 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_TIMEOUT, 222 "foo.bar\nNetwork timeout" }, 223 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED, 224 "foo.bar\nNetwork disconnected" }, 225 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_SERVER_DOWN, 226 "foo.bar\nServer unavailable" }, 227 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST, 228 "foo.bar\nNetwork error" }, 229 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_FAILED, 230 "foo.bar\nServer problem" }, 231 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_NO_RANGE, 232 "foo.bar\nDownload error" }, 233 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_PRECONDITION, 234 "foo.bar\nDownload error" }, 235 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_BAD_CONTENT, 236 "foo.bar\nNo file" }, 237 { content::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED, 238 "foo.bar" }, 239 { content::DOWNLOAD_INTERRUPT_REASON_USER_SHUTDOWN, 240 "foo.bar\nShutdown" }, 241 { content::DOWNLOAD_INTERRUPT_REASON_CRASH, 242 "foo.bar\nCrash" }, 243 }; 244 COMPILE_ASSERT(kInterruptReasonCount == ARRAYSIZE_UNSAFE(kTestCases), 245 interrupt_reason_mismatch); 246 247 // Large tooltip width. Should be large enough to accommodate the entire 248 // tooltip without truncation. 249 const int kLargeTooltipWidth = 1000; 250 251 // Small tooltip width. Small enough to require truncation of most 252 // tooltips. Used to test eliding logic. 253 const int kSmallTooltipWidth = 40; 254 255 const gfx::FontList& font_list = 256 ui::ResourceBundle::GetSharedInstance().GetFontList( 257 ui::ResourceBundle::BaseFont); 258 SetupDownloadItemDefaults(); 259 for (unsigned i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); ++i) { 260 const TestCase& test_case = kTestCases[i]; 261 SetupInterruptedDownloadItem(test_case.reason); 262 263 // GetTooltipText() elides the tooltip so that the text would fit within a 264 // given width. The following test would fail if kLargeTooltipWidth isn't 265 // large enough to accomodate all the strings. 266 EXPECT_STREQ( 267 test_case.expected_tooltip, 268 base::UTF16ToUTF8(model().GetTooltipText(font_list, 269 kLargeTooltipWidth)).c_str()); 270 271 // Check that if the width is small, the returned tooltip only contains 272 // lines of the given width or smaller. 273 std::vector<base::string16> lines; 274 base::string16 truncated_tooltip = 275 model().GetTooltipText(font_list, kSmallTooltipWidth); 276 Tokenize(truncated_tooltip, base::ASCIIToUTF16("\n"), &lines); 277 for (unsigned i = 0; i < lines.size(); ++i) 278 EXPECT_GE(kSmallTooltipWidth, gfx::GetStringWidth(lines[i], font_list)); 279 } 280 } 281 282 TEST_F(DownloadItemModelTest, InProgressStatus) { 283 const struct TestCase { 284 int64 received_bytes; // Return value of GetReceivedBytes(). 285 int64 total_bytes; // Return value of GetTotalBytes(). 286 bool time_remaining_known; // If TimeRemaining() is known. 287 bool open_when_complete; // GetOpenWhenComplete(). 288 bool is_paused; // IsPaused(). 289 const char* expected_status; // Expected status text. 290 } kTestCases[] = { 291 // These are all the valid combinations of the above fields for a download 292 // that is in IN_PROGRESS state. Go through all of them and check the return 293 // value of DownloadItemModel::GetStatusText(). The point isn't to lock down 294 // the status strings, but to make sure we end up with something sane for 295 // all the circumstances we care about. 296 // 297 // For GetReceivedBytes()/GetTotalBytes(), we only check whether each is 298 // non-zero. In addition, if |total_bytes| is zero, then 299 // |time_remaining_known| is also false. 300 // 301 // .-- .TimeRemaining() is known. 302 // | .-- .GetOpenWhenComplete() 303 // | | .---- .IsPaused() 304 { 0, 0, false, false, false, "Starting..." }, 305 { 1, 0, false, false, false, "1 B" }, 306 { 0, 2, false, false, false, "Starting..." }, 307 { 1, 2, false, false, false, "1/2 B" }, 308 { 0, 2, true, false, false, "0/2 B, 10 secs left" }, 309 { 1, 2, true, false, false, "1/2 B, 10 secs left" }, 310 { 0, 0, false, true, false, "Opening when complete" }, 311 { 1, 0, false, true, false, "Opening when complete" }, 312 { 0, 2, false, true, false, "Opening when complete" }, 313 { 1, 2, false, true, false, "Opening when complete" }, 314 { 0, 2, true, true, false, "Opening in 10 secs..." }, 315 { 1, 2, true, true, false, "Opening in 10 secs..." }, 316 { 0, 0, false, false, true, "0 B, Paused" }, 317 { 1, 0, false, false, true, "1 B, Paused" }, 318 { 0, 2, false, false, true, "0/2 B, Paused" }, 319 { 1, 2, false, false, true, "1/2 B, Paused" }, 320 { 0, 2, true, false, true, "0/2 B, Paused" }, 321 { 1, 2, true, false, true, "1/2 B, Paused" }, 322 { 0, 0, false, true, true, "0 B, Paused" }, 323 { 1, 0, false, true, true, "1 B, Paused" }, 324 { 0, 2, false, true, true, "0/2 B, Paused" }, 325 { 1, 2, false, true, true, "1/2 B, Paused" }, 326 { 0, 2, true, true, true, "0/2 B, Paused" }, 327 { 1, 2, true, true, true, "1/2 B, Paused" }, 328 }; 329 330 SetupDownloadItemDefaults(); 331 332 for (unsigned i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); i++) { 333 const TestCase& test_case = kTestCases[i]; 334 Mock::VerifyAndClearExpectations(&item()); 335 Mock::VerifyAndClearExpectations(&model()); 336 EXPECT_CALL(item(), GetReceivedBytes()) 337 .WillRepeatedly(Return(test_case.received_bytes)); 338 EXPECT_CALL(item(), GetTotalBytes()) 339 .WillRepeatedly(Return(test_case.total_bytes)); 340 EXPECT_CALL(item(), TimeRemaining(_)) 341 .WillRepeatedly(testing::DoAll( 342 testing::SetArgPointee<0>(base::TimeDelta::FromSeconds(10)), 343 Return(test_case.time_remaining_known))); 344 EXPECT_CALL(item(), GetOpenWhenComplete()) 345 .WillRepeatedly(Return(test_case.open_when_complete)); 346 EXPECT_CALL(item(), IsPaused()) 347 .WillRepeatedly(Return(test_case.is_paused)); 348 349 EXPECT_STREQ(test_case.expected_status, 350 base::UTF16ToUTF8(model().GetStatusText()).c_str()); 351 } 352 } 353 354 TEST_F(DownloadItemModelTest, ShouldShowInShelf) { 355 SetupDownloadItemDefaults(); 356 357 // By default the download item should be displayable on the shelf. 358 EXPECT_TRUE(model().ShouldShowInShelf()); 359 360 // Once explicitly set, ShouldShowInShelf() should return the explicit value. 361 model().SetShouldShowInShelf(false); 362 EXPECT_FALSE(model().ShouldShowInShelf()); 363 364 model().SetShouldShowInShelf(true); 365 EXPECT_TRUE(model().ShouldShowInShelf()); 366 } 367 368 TEST_F(DownloadItemModelTest, ShouldRemoveFromShelfWhenComplete) { 369 const struct TestCase { 370 DownloadItem::DownloadState state; 371 bool is_dangerous; // Expectation for IsDangerous(). 372 bool is_auto_open; // Expectation for GetOpenWhenComplete(). 373 bool auto_opened; // Whether the download was successfully 374 // auto-opened. Expecation for GetAutoOpened(). 375 bool expected_result; 376 } kTestCases[] = { 377 // All the valid combinations of state, is_dangerous, is_auto_open and 378 // auto_opened. 379 // 380 // .--- Is dangerous. 381 // | .--- Auto open or temporary. 382 // | | .--- Auto opened. 383 // | | | .--- Expected result. 384 { DownloadItem::IN_PROGRESS, false, false, false, false}, 385 { DownloadItem::IN_PROGRESS, false, true , false, true }, 386 { DownloadItem::IN_PROGRESS, true , false, false, false}, 387 { DownloadItem::IN_PROGRESS, true , true , false, false}, 388 { DownloadItem::COMPLETE, false, false, false, false}, 389 { DownloadItem::COMPLETE, false, true , false, false}, 390 { DownloadItem::COMPLETE, false, false, true , true }, 391 { DownloadItem::COMPLETE, false, true , true , true }, 392 { DownloadItem::CANCELLED, false, false, false, false}, 393 { DownloadItem::CANCELLED, false, true , false, false}, 394 { DownloadItem::CANCELLED, true , false, false, false}, 395 { DownloadItem::CANCELLED, true , true , false, false}, 396 { DownloadItem::INTERRUPTED, false, false, false, false}, 397 { DownloadItem::INTERRUPTED, false, true , false, false}, 398 { DownloadItem::INTERRUPTED, true , false, false, false}, 399 { DownloadItem::INTERRUPTED, true , true , false, false} 400 }; 401 402 SetupDownloadItemDefaults(); 403 404 for (unsigned i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); i++) { 405 const TestCase& test_case = kTestCases[i]; 406 EXPECT_CALL(item(), GetOpenWhenComplete()) 407 .WillRepeatedly(Return(test_case.is_auto_open)); 408 EXPECT_CALL(item(), GetState()) 409 .WillRepeatedly(Return(test_case.state)); 410 EXPECT_CALL(item(), IsDangerous()) 411 .WillRepeatedly(Return(test_case.is_dangerous)); 412 EXPECT_CALL(item(), GetAutoOpened()) 413 .WillRepeatedly(Return(test_case.auto_opened)); 414 415 EXPECT_EQ(test_case.expected_result, 416 model().ShouldRemoveFromShelfWhenComplete()) 417 << "Test case: " << i; 418 Mock::VerifyAndClearExpectations(&item()); 419 Mock::VerifyAndClearExpectations(&model()); 420 } 421 } 422