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