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