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_SERVER_FAILED, 156 "Failed - Server problem" }, 157 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_NO_RANGE, 158 "Failed - Download error" }, 159 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_PRECONDITION, 160 "Failed - Download error" }, 161 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_BAD_CONTENT, 162 "Failed - No file" }, 163 { content::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED, 164 "Cancelled" }, 165 { content::DOWNLOAD_INTERRUPT_REASON_USER_SHUTDOWN, 166 "Failed - Shutdown" }, 167 { content::DOWNLOAD_INTERRUPT_REASON_CRASH, 168 "Failed - Crash" }, 169 }; 170 COMPILE_ASSERT(kInterruptReasonCount == ARRAYSIZE_UNSAFE(kTestCases), 171 interrupt_reason_mismatch); 172 173 SetupDownloadItemDefaults(); 174 for (unsigned i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); ++i) { 175 const TestCase& test_case = kTestCases[i]; 176 SetupInterruptedDownloadItem(test_case.reason); 177 EXPECT_STREQ(test_case.expected_status, 178 UTF16ToUTF8(model().GetStatusText()).c_str()); 179 } 180 } 181 182 // Note: This test is currently skipped on Android. See http://crbug.com/139398 183 TEST_F(DownloadItemModelTest, InterruptTooltip) { 184 // Test that we have the correct interrupt tooltip for downloads that are in 185 // the INTERRUPTED state. 186 const struct TestCase { 187 // The reason. 188 content::DownloadInterruptReason reason; 189 190 // Expected tooltip text. The tooltip text for interrupted downloads 191 // typically consist of two lines. One for the filename and one for the 192 // interrupt reason. The returned string contains a newline. 193 const char* expected_tooltip; 194 } kTestCases[] = { 195 { content::DOWNLOAD_INTERRUPT_REASON_NONE, 196 "foo.bar" }, 197 { content::DOWNLOAD_INTERRUPT_REASON_FILE_FAILED, 198 "foo.bar\nDownload error" }, 199 { content::DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED, 200 "foo.bar\nInsufficient permissions" }, 201 { content::DOWNLOAD_INTERRUPT_REASON_FILE_NO_SPACE, 202 "foo.bar\nDisk full" }, 203 { content::DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG, 204 "foo.bar\nPath too long" }, 205 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE, 206 "foo.bar\nFile too large" }, 207 { content::DOWNLOAD_INTERRUPT_REASON_FILE_VIRUS_INFECTED, 208 "foo.bar\nVirus detected" }, 209 { content::DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED, 210 "foo.bar\nBlocked" }, 211 { content::DOWNLOAD_INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED, 212 "foo.bar\nVirus scan failed" }, 213 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_SHORT, 214 "foo.bar\nFile truncated" }, 215 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR, 216 "foo.bar\nSystem busy" }, 217 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_FAILED, 218 "foo.bar\nNetwork error" }, 219 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_TIMEOUT, 220 "foo.bar\nNetwork timeout" }, 221 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED, 222 "foo.bar\nNetwork disconnected" }, 223 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_SERVER_DOWN, 224 "foo.bar\nServer unavailable" }, 225 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_FAILED, 226 "foo.bar\nServer problem" }, 227 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_NO_RANGE, 228 "foo.bar\nDownload error" }, 229 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_PRECONDITION, 230 "foo.bar\nDownload error" }, 231 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_BAD_CONTENT, 232 "foo.bar\nNo file" }, 233 { content::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED, 234 "foo.bar" }, 235 { content::DOWNLOAD_INTERRUPT_REASON_USER_SHUTDOWN, 236 "foo.bar\nShutdown" }, 237 { content::DOWNLOAD_INTERRUPT_REASON_CRASH, 238 "foo.bar\nCrash" }, 239 }; 240 COMPILE_ASSERT(kInterruptReasonCount == ARRAYSIZE_UNSAFE(kTestCases), 241 interrupt_reason_mismatch); 242 243 // Large tooltip width. Should be large enough to accommodate the entire 244 // tooltip without truncation. 245 const int kLargeTooltipWidth = 1000; 246 247 // Small tooltip width. Small enough to require truncation of most 248 // tooltips. Used to test eliding logic. 249 const int kSmallTooltipWidth = 40; 250 251 const gfx::FontList& font_list = 252 ui::ResourceBundle::GetSharedInstance().GetFontList( 253 ui::ResourceBundle::BaseFont); 254 SetupDownloadItemDefaults(); 255 for (unsigned i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); ++i) { 256 const TestCase& test_case = kTestCases[i]; 257 SetupInterruptedDownloadItem(test_case.reason); 258 259 // GetTooltipText() elides the tooltip so that the text would fit within a 260 // given width. The following test would fail if kLargeTooltipWidth isn't 261 // large enough to accomodate all the strings. 262 EXPECT_STREQ( 263 test_case.expected_tooltip, 264 UTF16ToUTF8(model().GetTooltipText(font_list, 265 kLargeTooltipWidth)).c_str()); 266 267 // Check that if the width is small, the returned tooltip only contains 268 // lines of the given width or smaller. 269 std::vector<base::string16> lines; 270 base::string16 truncated_tooltip = 271 model().GetTooltipText(font_list, kSmallTooltipWidth); 272 Tokenize(truncated_tooltip, ASCIIToUTF16("\n"), &lines); 273 for (unsigned i = 0; i < lines.size(); ++i) 274 EXPECT_GE(kSmallTooltipWidth, gfx::GetStringWidth(lines[i], font_list)); 275 } 276 } 277 278 TEST_F(DownloadItemModelTest, InProgressStatus) { 279 const struct TestCase { 280 int64 received_bytes; // Return value of GetReceivedBytes(). 281 int64 total_bytes; // Return value of GetTotalBytes(). 282 bool time_remaining_known; // If TimeRemaining() is known. 283 bool open_when_complete; // GetOpenWhenComplete(). 284 bool is_paused; // IsPaused(). 285 const char* expected_status; // Expected status text. 286 } kTestCases[] = { 287 // These are all the valid combinations of the above fields for a download 288 // that is in IN_PROGRESS state. Go through all of them and check the return 289 // value of DownloadItemModel::GetStatusText(). The point isn't to lock down 290 // the status strings, but to make sure we end up with something sane for 291 // all the circumstances we care about. 292 // 293 // For GetReceivedBytes()/GetTotalBytes(), we only check whether each is 294 // non-zero. In addition, if |total_bytes| is zero, then 295 // |time_remaining_known| is also false. 296 // 297 // .-- .TimeRemaining() is known. 298 // | .-- .GetOpenWhenComplete() 299 // | | .---- .IsPaused() 300 { 0, 0, false, false, false, "Starting..." }, 301 { 1, 0, false, false, false, "1 B" }, 302 { 0, 2, false, false, false, "Starting..." }, 303 { 1, 2, false, false, false, "1/2 B" }, 304 { 0, 2, true, false, false, "0/2 B, 10 secs left" }, 305 { 1, 2, true, false, false, "1/2 B, 10 secs left" }, 306 { 0, 0, false, true, false, "Opening when complete" }, 307 { 1, 0, false, true, false, "Opening when complete" }, 308 { 0, 2, false, true, false, "Opening when complete" }, 309 { 1, 2, false, true, false, "Opening when complete" }, 310 { 0, 2, true, true, false, "Opening in 10 secs..." }, 311 { 1, 2, true, true, false, "Opening in 10 secs..." }, 312 { 0, 0, false, false, true, "0 B, Paused" }, 313 { 1, 0, false, false, true, "1 B, Paused" }, 314 { 0, 2, false, false, true, "0/2 B, Paused" }, 315 { 1, 2, false, false, true, "1/2 B, Paused" }, 316 { 0, 2, true, false, true, "0/2 B, Paused" }, 317 { 1, 2, true, false, true, "1/2 B, Paused" }, 318 { 0, 0, false, true, true, "0 B, Paused" }, 319 { 1, 0, false, true, true, "1 B, Paused" }, 320 { 0, 2, false, true, true, "0/2 B, Paused" }, 321 { 1, 2, false, true, true, "1/2 B, Paused" }, 322 { 0, 2, true, true, true, "0/2 B, Paused" }, 323 { 1, 2, true, true, true, "1/2 B, Paused" }, 324 }; 325 326 SetupDownloadItemDefaults(); 327 328 for (unsigned i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); i++) { 329 const TestCase& test_case = kTestCases[i]; 330 Mock::VerifyAndClearExpectations(&item()); 331 Mock::VerifyAndClearExpectations(&model()); 332 EXPECT_CALL(item(), GetReceivedBytes()) 333 .WillRepeatedly(Return(test_case.received_bytes)); 334 EXPECT_CALL(item(), GetTotalBytes()) 335 .WillRepeatedly(Return(test_case.total_bytes)); 336 EXPECT_CALL(item(), TimeRemaining(_)) 337 .WillRepeatedly(testing::DoAll( 338 testing::SetArgPointee<0>(base::TimeDelta::FromSeconds(10)), 339 Return(test_case.time_remaining_known))); 340 EXPECT_CALL(item(), GetOpenWhenComplete()) 341 .WillRepeatedly(Return(test_case.open_when_complete)); 342 EXPECT_CALL(item(), IsPaused()) 343 .WillRepeatedly(Return(test_case.is_paused)); 344 345 EXPECT_STREQ(test_case.expected_status, 346 UTF16ToUTF8(model().GetStatusText()).c_str()); 347 } 348 } 349 350 TEST_F(DownloadItemModelTest, ShouldShowInShelf) { 351 SetupDownloadItemDefaults(); 352 353 // By default the download item should be displayable on the shelf. 354 EXPECT_TRUE(model().ShouldShowInShelf()); 355 356 // Once explicitly set, ShouldShowInShelf() should return the explicit value. 357 model().SetShouldShowInShelf(false); 358 EXPECT_FALSE(model().ShouldShowInShelf()); 359 360 model().SetShouldShowInShelf(true); 361 EXPECT_TRUE(model().ShouldShowInShelf()); 362 } 363 364 TEST_F(DownloadItemModelTest, ShouldRemoveFromShelfWhenComplete) { 365 const struct TestCase { 366 DownloadItem::DownloadState state; 367 bool is_dangerous; // Expectation for IsDangerous(). 368 bool is_auto_open; // Expectation for GetOpenWhenComplete(). 369 bool auto_opened; // Whether the download was successfully 370 // auto-opened. Expecation for GetAutoOpened(). 371 bool expected_result; 372 } kTestCases[] = { 373 // All the valid combinations of state, is_dangerous, is_auto_open and 374 // auto_opened. 375 // 376 // .--- Is dangerous. 377 // | .--- Auto open or temporary. 378 // | | .--- Auto opened. 379 // | | | .--- Expected result. 380 { DownloadItem::IN_PROGRESS, false, false, false, false}, 381 { DownloadItem::IN_PROGRESS, false, true , false, true }, 382 { DownloadItem::IN_PROGRESS, true , false, false, false}, 383 { DownloadItem::IN_PROGRESS, true , true , false, false}, 384 { DownloadItem::COMPLETE, false, false, false, false}, 385 { DownloadItem::COMPLETE, false, true , false, false}, 386 { DownloadItem::COMPLETE, false, false, true , true }, 387 { DownloadItem::COMPLETE, false, true , true , true }, 388 { DownloadItem::CANCELLED, false, false, false, false}, 389 { DownloadItem::CANCELLED, false, true , false, false}, 390 { DownloadItem::CANCELLED, true , false, false, false}, 391 { DownloadItem::CANCELLED, true , true , false, false}, 392 { DownloadItem::INTERRUPTED, false, false, false, false}, 393 { DownloadItem::INTERRUPTED, false, true , false, false}, 394 { DownloadItem::INTERRUPTED, true , false, false, false}, 395 { DownloadItem::INTERRUPTED, true , true , false, false} 396 }; 397 398 SetupDownloadItemDefaults(); 399 400 for (unsigned i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); i++) { 401 const TestCase& test_case = kTestCases[i]; 402 EXPECT_CALL(item(), GetOpenWhenComplete()) 403 .WillRepeatedly(Return(test_case.is_auto_open)); 404 EXPECT_CALL(item(), GetState()) 405 .WillRepeatedly(Return(test_case.state)); 406 EXPECT_CALL(item(), IsDangerous()) 407 .WillRepeatedly(Return(test_case.is_dangerous)); 408 EXPECT_CALL(item(), GetAutoOpened()) 409 .WillRepeatedly(Return(test_case.auto_opened)); 410 411 EXPECT_EQ(test_case.expected_result, 412 model().ShouldRemoveFromShelfWhenComplete()) 413 << "Test case: " << i; 414 Mock::VerifyAndClearExpectations(&item()); 415 Mock::VerifyAndClearExpectations(&model()); 416 } 417 } 418