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
24using content::DownloadItem;
25using ::testing::Mock;
26using ::testing::NiceMock;
27using ::testing::Return;
28using ::testing::ReturnRefOfCopy;
29using ::testing::SetArgPointee;
30using ::testing::_;
31
32namespace {
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.
38char 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};
44const size_t kInterruptReasonCount = ARRAYSIZE_UNSAFE(kInterruptReasonCounter);
45
46// Default target path for a mock download item in DownloadItemModelTest.
47const base::FilePath::CharType kDefaultTargetFilePath[] =
48    FILE_PATH_LITERAL("/foo/bar/foo.bar");
49
50const base::FilePath::CharType kDefaultDisplayFileName[] =
51    FILE_PATH_LITERAL("foo.bar");
52
53// Default URL for a mock download item in DownloadItemModelTest.
54const char kDefaultURL[] = "http://example.com/foo.bar";
55
56class 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
113TEST_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
187TEST_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
288TEST_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
360TEST_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
374TEST_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