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/drive/drive_uploader.h"
6
7#include <string>
8#include <vector>
9
10#include "base/bind.h"
11#include "base/files/scoped_temp_dir.h"
12#include "base/memory/scoped_ptr.h"
13#include "base/message_loop/message_loop.h"
14#include "base/run_loop.h"
15#include "base/values.h"
16#include "chrome/browser/drive/dummy_drive_service.h"
17#include "content/public/test/test_browser_thread_bundle.h"
18#include "google_apis/drive/drive_api_parser.h"
19#include "google_apis/drive/test_util.h"
20#include "testing/gtest/include/gtest/gtest.h"
21
22using google_apis::CancelCallback;
23using google_apis::FileResource;
24using google_apis::GDataErrorCode;
25using google_apis::GDATA_NO_CONNECTION;
26using google_apis::GDATA_OTHER_ERROR;
27using google_apis::HTTP_CONFLICT;
28using google_apis::HTTP_CREATED;
29using google_apis::HTTP_NOT_FOUND;
30using google_apis::HTTP_PRECONDITION;
31using google_apis::HTTP_RESUME_INCOMPLETE;
32using google_apis::HTTP_SUCCESS;
33using google_apis::InitiateUploadCallback;
34using google_apis::ProgressCallback;
35using google_apis::UploadRangeResponse;
36using google_apis::drive::UploadRangeCallback;
37namespace test_util = google_apis::test_util;
38
39namespace drive {
40
41namespace {
42
43const char kTestDummyMd5[] = "dummy_md5";
44const char kTestDocumentTitle[] = "Hello world";
45const char kTestInitiateUploadParentResourceId[] = "parent_resource_id";
46const char kTestInitiateUploadResourceId[] = "resource_id";
47const char kTestMimeType[] = "text/plain";
48const char kTestUploadNewFileURL[] = "http://test/upload_location/new_file";
49const char kTestUploadExistingFileURL[] =
50    "http://test/upload_location/existing_file";
51const int64 kUploadChunkSize = 512 * 1024;
52const char kTestETag[] = "test_etag";
53
54// Mock DriveService that verifies if the uploaded content matches the preset
55// expectation.
56class MockDriveServiceWithUploadExpectation : public DummyDriveService {
57 public:
58  // Sets up an expected upload content. InitiateUpload and ResumeUpload will
59  // verify that the specified data is correctly uploaded.
60  MockDriveServiceWithUploadExpectation(
61      const base::FilePath& expected_upload_file,
62      int64 expected_content_length)
63     : expected_upload_file_(expected_upload_file),
64       expected_content_length_(expected_content_length),
65       received_bytes_(0),
66       resume_upload_call_count_(0) {}
67
68  int64 received_bytes() const { return received_bytes_; }
69  void set_received_bytes(int64 received_bytes) {
70    received_bytes_ = received_bytes;
71  }
72
73  int64 resume_upload_call_count() const { return resume_upload_call_count_; }
74
75 private:
76  // DriveServiceInterface overrides.
77  // Handles a request for obtaining an upload location URL.
78  virtual CancelCallback InitiateUploadNewFile(
79      const std::string& content_type,
80      int64 content_length,
81      const std::string& parent_resource_id,
82      const std::string& title,
83      const InitiateUploadNewFileOptions& options,
84      const InitiateUploadCallback& callback) OVERRIDE {
85    EXPECT_EQ(kTestDocumentTitle, title);
86    EXPECT_EQ(kTestMimeType, content_type);
87    EXPECT_EQ(expected_content_length_, content_length);
88    EXPECT_EQ(kTestInitiateUploadParentResourceId, parent_resource_id);
89
90    // Calls back the upload URL for subsequent ResumeUpload requests.
91    // InitiateUpload is an asynchronous function, so don't callback directly.
92    base::MessageLoop::current()->PostTask(FROM_HERE,
93        base::Bind(callback, HTTP_SUCCESS, GURL(kTestUploadNewFileURL)));
94    return CancelCallback();
95  }
96
97  virtual CancelCallback InitiateUploadExistingFile(
98      const std::string& content_type,
99      int64 content_length,
100      const std::string& resource_id,
101      const InitiateUploadExistingFileOptions& options,
102      const InitiateUploadCallback& callback) OVERRIDE {
103    EXPECT_EQ(kTestMimeType, content_type);
104    EXPECT_EQ(expected_content_length_, content_length);
105    EXPECT_EQ(kTestInitiateUploadResourceId, resource_id);
106
107    if (!options.etag.empty() && options.etag != kTestETag) {
108      base::MessageLoop::current()->PostTask(FROM_HERE,
109          base::Bind(callback, HTTP_PRECONDITION, GURL()));
110      return CancelCallback();
111    }
112
113    // Calls back the upload URL for subsequent ResumeUpload requests.
114    // InitiateUpload is an asynchronous function, so don't callback directly.
115    base::MessageLoop::current()->PostTask(FROM_HERE,
116        base::Bind(callback, HTTP_SUCCESS, GURL(kTestUploadExistingFileURL)));
117    return CancelCallback();
118  }
119
120  // Handles a request for uploading a chunk of bytes.
121  virtual CancelCallback ResumeUpload(
122      const GURL& upload_location,
123      int64 start_position,
124      int64 end_position,
125      int64 content_length,
126      const std::string& content_type,
127      const base::FilePath& local_file_path,
128      const UploadRangeCallback& callback,
129      const ProgressCallback& progress_callback) OVERRIDE {
130    // The upload range should start from the current first unreceived byte.
131    EXPECT_EQ(received_bytes_, start_position);
132    EXPECT_EQ(expected_upload_file_, local_file_path);
133
134    // The upload data must be split into 512KB chunks.
135    const int64 expected_chunk_end =
136        std::min(received_bytes_ + kUploadChunkSize, expected_content_length_);
137    EXPECT_EQ(expected_chunk_end, end_position);
138
139    // The upload URL returned by InitiateUpload() must be used.
140    EXPECT_TRUE(GURL(kTestUploadNewFileURL) == upload_location ||
141                GURL(kTestUploadExistingFileURL) == upload_location);
142
143    // Other parameters should be the exact values passed to DriveUploader.
144    EXPECT_EQ(expected_content_length_, content_length);
145    EXPECT_EQ(kTestMimeType, content_type);
146
147    // Update the internal status of the current upload session.
148    resume_upload_call_count_++;
149    received_bytes_ = end_position;
150
151    // Callback progress
152    if (!progress_callback.is_null()) {
153      // For the testing purpose, it always notifies the progress at the end of
154      // each chunk uploading.
155      int64 chunk_size = end_position - start_position;
156      base::MessageLoop::current()->PostTask(FROM_HERE,
157          base::Bind(progress_callback, chunk_size, chunk_size));
158    }
159
160    SendUploadRangeResponse(upload_location, callback);
161    return CancelCallback();
162  }
163
164  // Handles a request to fetch the current upload status.
165  virtual CancelCallback GetUploadStatus(
166      const GURL& upload_location,
167      int64 content_length,
168      const UploadRangeCallback& callback) OVERRIDE {
169    EXPECT_EQ(expected_content_length_, content_length);
170    // The upload URL returned by InitiateUpload() must be used.
171    EXPECT_TRUE(GURL(kTestUploadNewFileURL) == upload_location ||
172                GURL(kTestUploadExistingFileURL) == upload_location);
173
174    SendUploadRangeResponse(upload_location, callback);
175    return CancelCallback();
176  }
177
178  // Runs |callback| with the current upload status.
179  void SendUploadRangeResponse(const GURL& upload_location,
180                               const UploadRangeCallback& callback) {
181    // Callback with response.
182    UploadRangeResponse response;
183    scoped_ptr<FileResource> entry;
184    if (received_bytes_ == expected_content_length_) {
185      GDataErrorCode response_code =
186          upload_location == GURL(kTestUploadNewFileURL) ?
187          HTTP_CREATED : HTTP_SUCCESS;
188      response = UploadRangeResponse(response_code, -1, -1);
189
190      entry.reset(new FileResource);
191      entry->set_md5_checksum(kTestDummyMd5);
192    } else {
193      response = UploadRangeResponse(
194          HTTP_RESUME_INCOMPLETE, 0, received_bytes_);
195    }
196    // ResumeUpload is an asynchronous function, so don't callback directly.
197    base::MessageLoop::current()->PostTask(FROM_HERE,
198        base::Bind(callback, response, base::Passed(&entry)));
199  }
200
201  const base::FilePath expected_upload_file_;
202  const int64 expected_content_length_;
203  int64 received_bytes_;
204  int64 resume_upload_call_count_;
205};
206
207// Mock DriveService that returns a failure at InitiateUpload().
208class MockDriveServiceNoConnectionAtInitiate : public DummyDriveService {
209  // Returns error.
210  virtual CancelCallback InitiateUploadNewFile(
211      const std::string& content_type,
212      int64 content_length,
213      const std::string& parent_resource_id,
214      const std::string& title,
215      const InitiateUploadNewFileOptions& options,
216      const InitiateUploadCallback& callback) OVERRIDE {
217    base::MessageLoop::current()->PostTask(FROM_HERE,
218        base::Bind(callback, GDATA_NO_CONNECTION, GURL()));
219    return CancelCallback();
220  }
221
222  virtual CancelCallback InitiateUploadExistingFile(
223      const std::string& content_type,
224      int64 content_length,
225      const std::string& resource_id,
226      const InitiateUploadExistingFileOptions& options,
227      const InitiateUploadCallback& callback) OVERRIDE {
228    base::MessageLoop::current()->PostTask(FROM_HERE,
229        base::Bind(callback, GDATA_NO_CONNECTION, GURL()));
230    return CancelCallback();
231  }
232
233  // Should not be used.
234  virtual CancelCallback ResumeUpload(
235      const GURL& upload_url,
236      int64 start_position,
237      int64 end_position,
238      int64 content_length,
239      const std::string& content_type,
240      const base::FilePath& local_file_path,
241      const UploadRangeCallback& callback,
242      const ProgressCallback& progress_callback) OVERRIDE {
243    NOTREACHED();
244    return CancelCallback();
245  }
246};
247
248// Mock DriveService that returns a failure at ResumeUpload().
249class MockDriveServiceNoConnectionAtResume : public DummyDriveService {
250  // Succeeds and returns an upload location URL.
251  virtual CancelCallback InitiateUploadNewFile(
252      const std::string& content_type,
253      int64 content_length,
254      const std::string& parent_resource_id,
255      const std::string& title,
256      const InitiateUploadNewFileOptions& options,
257      const InitiateUploadCallback& callback) OVERRIDE {
258    base::MessageLoop::current()->PostTask(FROM_HERE,
259        base::Bind(callback, HTTP_SUCCESS, GURL(kTestUploadNewFileURL)));
260    return CancelCallback();
261  }
262
263  virtual CancelCallback InitiateUploadExistingFile(
264      const std::string& content_type,
265      int64 content_length,
266      const std::string& resource_id,
267      const InitiateUploadExistingFileOptions& options,
268      const InitiateUploadCallback& callback) OVERRIDE {
269    base::MessageLoop::current()->PostTask(FROM_HERE,
270        base::Bind(callback, HTTP_SUCCESS, GURL(kTestUploadExistingFileURL)));
271    return CancelCallback();
272  }
273
274  // Returns error.
275  virtual CancelCallback ResumeUpload(
276      const GURL& upload_url,
277      int64 start_position,
278      int64 end_position,
279      int64 content_length,
280      const std::string& content_type,
281      const base::FilePath& local_file_path,
282      const UploadRangeCallback& callback,
283      const ProgressCallback& progress_callback) OVERRIDE {
284    base::MessageLoop::current()->PostTask(FROM_HERE,
285        base::Bind(callback,
286                   UploadRangeResponse(GDATA_NO_CONNECTION, -1, -1),
287                   base::Passed(scoped_ptr<FileResource>())));
288    return CancelCallback();
289  }
290};
291
292// Mock DriveService that returns a failure at GetUploadStatus().
293class MockDriveServiceNoConnectionAtGetUploadStatus : public DummyDriveService {
294  // Returns error.
295  virtual CancelCallback GetUploadStatus(
296      const GURL& upload_url,
297      int64 content_length,
298      const UploadRangeCallback& callback) OVERRIDE {
299    base::MessageLoop::current()->PostTask(FROM_HERE,
300        base::Bind(callback,
301                   UploadRangeResponse(GDATA_NO_CONNECTION, -1, -1),
302                   base::Passed(scoped_ptr<FileResource>())));
303    return CancelCallback();
304  }
305};
306
307class DriveUploaderTest : public testing::Test {
308 public:
309  virtual void SetUp() OVERRIDE {
310    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
311  }
312
313 protected:
314  content::TestBrowserThreadBundle thread_bundle_;
315  base::ScopedTempDir temp_dir_;
316};
317
318}  // namespace
319
320TEST_F(DriveUploaderTest, UploadExisting0KB) {
321  base::FilePath local_path;
322  std::string data;
323  ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize(
324      temp_dir_.path(), 0, &local_path, &data));
325
326  GDataErrorCode error = GDATA_OTHER_ERROR;
327  GURL upload_location;
328  scoped_ptr<FileResource> entry;
329
330  MockDriveServiceWithUploadExpectation mock_service(local_path, data.size());
331  DriveUploader uploader(&mock_service,
332                         base::MessageLoopProxy::current().get());
333  std::vector<test_util::ProgressInfo> upload_progress_values;
334  uploader.UploadExistingFile(
335      kTestInitiateUploadResourceId,
336      local_path,
337      kTestMimeType,
338      DriveUploader::UploadExistingFileOptions(),
339      test_util::CreateCopyResultCallback(
340          &error, &upload_location, &entry),
341      base::Bind(&test_util::AppendProgressCallbackResult,
342                 &upload_progress_values));
343  base::RunLoop().RunUntilIdle();
344
345  EXPECT_EQ(1, mock_service.resume_upload_call_count());
346  EXPECT_EQ(0, mock_service.received_bytes());
347  EXPECT_EQ(HTTP_SUCCESS, error);
348  EXPECT_TRUE(upload_location.is_empty());
349  ASSERT_TRUE(entry);
350  EXPECT_EQ(kTestDummyMd5, entry->md5_checksum());
351  ASSERT_EQ(1U, upload_progress_values.size());
352  EXPECT_EQ(test_util::ProgressInfo(0, 0), upload_progress_values[0]);
353}
354
355TEST_F(DriveUploaderTest, UploadExisting512KB) {
356  base::FilePath local_path;
357  std::string data;
358  ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize(
359      temp_dir_.path(), 512 * 1024, &local_path, &data));
360
361  GDataErrorCode error = GDATA_OTHER_ERROR;
362  GURL upload_location;
363  scoped_ptr<FileResource> entry;
364
365  MockDriveServiceWithUploadExpectation mock_service(local_path, data.size());
366  DriveUploader uploader(&mock_service,
367                         base::MessageLoopProxy::current().get());
368  std::vector<test_util::ProgressInfo> upload_progress_values;
369  uploader.UploadExistingFile(
370      kTestInitiateUploadResourceId,
371      local_path,
372      kTestMimeType,
373      DriveUploader::UploadExistingFileOptions(),
374      test_util::CreateCopyResultCallback(
375          &error, &upload_location, &entry),
376      base::Bind(&test_util::AppendProgressCallbackResult,
377                 &upload_progress_values));
378  base::RunLoop().RunUntilIdle();
379
380  // 512KB upload should not be split into multiple chunks.
381  EXPECT_EQ(1, mock_service.resume_upload_call_count());
382  EXPECT_EQ(512 * 1024, mock_service.received_bytes());
383  EXPECT_EQ(HTTP_SUCCESS, error);
384  EXPECT_TRUE(upload_location.is_empty());
385  ASSERT_TRUE(entry);
386  EXPECT_EQ(kTestDummyMd5, entry->md5_checksum());
387  ASSERT_EQ(1U, upload_progress_values.size());
388  EXPECT_EQ(test_util::ProgressInfo(512 * 1024, 512 * 1024),
389            upload_progress_values[0]);
390}
391
392TEST_F(DriveUploaderTest, InitiateUploadFail) {
393  base::FilePath local_path;
394  std::string data;
395  ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize(
396      temp_dir_.path(), 512 * 1024, &local_path, &data));
397
398  GDataErrorCode error = HTTP_SUCCESS;
399  GURL upload_location;
400  scoped_ptr<FileResource> entry;
401
402  MockDriveServiceNoConnectionAtInitiate mock_service;
403  DriveUploader uploader(&mock_service,
404                         base::MessageLoopProxy::current().get());
405  uploader.UploadExistingFile(kTestInitiateUploadResourceId,
406                              local_path,
407                              kTestMimeType,
408                              DriveUploader::UploadExistingFileOptions(),
409                              test_util::CreateCopyResultCallback(
410                                  &error, &upload_location, &entry),
411                              google_apis::ProgressCallback());
412  base::RunLoop().RunUntilIdle();
413
414  EXPECT_EQ(GDATA_NO_CONNECTION, error);
415  EXPECT_TRUE(upload_location.is_empty());
416  EXPECT_FALSE(entry);
417}
418
419TEST_F(DriveUploaderTest, InitiateUploadNoConflict) {
420  base::FilePath local_path;
421  std::string data;
422  ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize(
423      temp_dir_.path(), 512 * 1024, &local_path, &data));
424
425  GDataErrorCode error = GDATA_OTHER_ERROR;
426  GURL upload_location;
427  scoped_ptr<FileResource> entry;
428
429  MockDriveServiceWithUploadExpectation mock_service(local_path, data.size());
430  DriveUploader uploader(&mock_service,
431                         base::MessageLoopProxy::current().get());
432  DriveUploader::UploadExistingFileOptions options;
433  options.etag = kTestETag;
434  uploader.UploadExistingFile(kTestInitiateUploadResourceId,
435                              local_path,
436                              kTestMimeType,
437                              options,
438                              test_util::CreateCopyResultCallback(
439                                  &error, &upload_location, &entry),
440                              google_apis::ProgressCallback());
441  base::RunLoop().RunUntilIdle();
442
443  EXPECT_EQ(HTTP_SUCCESS, error);
444  EXPECT_TRUE(upload_location.is_empty());
445}
446
447TEST_F(DriveUploaderTest, InitiateUploadConflict) {
448  base::FilePath local_path;
449  std::string data;
450  ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize(
451      temp_dir_.path(), 512 * 1024, &local_path, &data));
452  const std::string kDestinationETag("destination_etag");
453
454  GDataErrorCode error = GDATA_OTHER_ERROR;
455  GURL upload_location;
456  scoped_ptr<FileResource> entry;
457
458  MockDriveServiceWithUploadExpectation mock_service(local_path, data.size());
459  DriveUploader uploader(&mock_service,
460                         base::MessageLoopProxy::current().get());
461  DriveUploader::UploadExistingFileOptions options;
462  options.etag = kDestinationETag;
463  uploader.UploadExistingFile(kTestInitiateUploadResourceId,
464                              local_path,
465                              kTestMimeType,
466                              options,
467                              test_util::CreateCopyResultCallback(
468                                  &error, &upload_location, &entry),
469                              google_apis::ProgressCallback());
470  base::RunLoop().RunUntilIdle();
471
472  EXPECT_EQ(HTTP_CONFLICT, error);
473  EXPECT_TRUE(upload_location.is_empty());
474}
475
476TEST_F(DriveUploaderTest, ResumeUploadFail) {
477  base::FilePath local_path;
478  std::string data;
479  ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize(
480      temp_dir_.path(), 512 * 1024, &local_path, &data));
481
482  GDataErrorCode error = HTTP_SUCCESS;
483  GURL upload_location;
484  scoped_ptr<FileResource> entry;
485
486  MockDriveServiceNoConnectionAtResume mock_service;
487  DriveUploader uploader(&mock_service,
488                         base::MessageLoopProxy::current().get());
489  uploader.UploadExistingFile(kTestInitiateUploadResourceId,
490                              local_path,
491                              kTestMimeType,
492                              DriveUploader::UploadExistingFileOptions(),
493                              test_util::CreateCopyResultCallback(
494                                  &error, &upload_location, &entry),
495                              google_apis::ProgressCallback());
496  base::RunLoop().RunUntilIdle();
497
498  EXPECT_EQ(GDATA_NO_CONNECTION, error);
499  EXPECT_EQ(GURL(kTestUploadExistingFileURL), upload_location);
500}
501
502TEST_F(DriveUploaderTest, GetUploadStatusFail) {
503  base::FilePath local_path;
504  std::string data;
505  ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize(
506      temp_dir_.path(), 512 * 1024, &local_path, &data));
507
508  GDataErrorCode error = HTTP_SUCCESS;
509  GURL upload_location;
510  scoped_ptr<FileResource> entry;
511
512  MockDriveServiceNoConnectionAtGetUploadStatus mock_service;
513  DriveUploader uploader(&mock_service,
514                         base::MessageLoopProxy::current().get());
515  uploader.ResumeUploadFile(GURL(kTestUploadExistingFileURL),
516                            local_path,
517                            kTestMimeType,
518                            test_util::CreateCopyResultCallback(
519                                &error, &upload_location, &entry),
520                            google_apis::ProgressCallback());
521  base::RunLoop().RunUntilIdle();
522
523  EXPECT_EQ(GDATA_NO_CONNECTION, error);
524  EXPECT_TRUE(upload_location.is_empty());
525}
526
527TEST_F(DriveUploaderTest, NonExistingSourceFile) {
528  GDataErrorCode error = GDATA_OTHER_ERROR;
529  GURL upload_location;
530  scoped_ptr<FileResource> entry;
531
532  DriveUploader uploader(NULL,  // NULL, the service won't be used.
533                         base::MessageLoopProxy::current().get());
534  uploader.UploadExistingFile(
535      kTestInitiateUploadResourceId,
536      temp_dir_.path().AppendASCII("_this_path_should_not_exist_"),
537      kTestMimeType,
538      DriveUploader::UploadExistingFileOptions(),
539      test_util::CreateCopyResultCallback(
540          &error, &upload_location, &entry),
541      google_apis::ProgressCallback());
542  base::RunLoop().RunUntilIdle();
543
544  // Should return failure without doing any attempt to connect to the server.
545  EXPECT_EQ(HTTP_NOT_FOUND, error);
546  EXPECT_TRUE(upload_location.is_empty());
547}
548
549TEST_F(DriveUploaderTest, ResumeUpload) {
550  base::FilePath local_path;
551  std::string data;
552  ASSERT_TRUE(test_util::CreateFileOfSpecifiedSize(
553      temp_dir_.path(), 1024 * 1024, &local_path, &data));
554
555  GDataErrorCode error = GDATA_OTHER_ERROR;
556  GURL upload_location;
557  scoped_ptr<FileResource> entry;
558
559  MockDriveServiceWithUploadExpectation mock_service(local_path, data.size());
560  DriveUploader uploader(&mock_service,
561                         base::MessageLoopProxy::current().get());
562  // Emulate the situation that the only first part is successfully uploaded,
563  // but not the latter half.
564  mock_service.set_received_bytes(512 * 1024);
565
566  std::vector<test_util::ProgressInfo> upload_progress_values;
567  uploader.ResumeUploadFile(
568      GURL(kTestUploadExistingFileURL),
569      local_path,
570      kTestMimeType,
571      test_util::CreateCopyResultCallback(
572          &error, &upload_location, &entry),
573      base::Bind(&test_util::AppendProgressCallbackResult,
574                 &upload_progress_values));
575  base::RunLoop().RunUntilIdle();
576
577  EXPECT_EQ(1, mock_service.resume_upload_call_count());
578  EXPECT_EQ(1024 * 1024, mock_service.received_bytes());
579  EXPECT_EQ(HTTP_SUCCESS, error);
580  EXPECT_TRUE(upload_location.is_empty());
581  ASSERT_TRUE(entry);
582  EXPECT_EQ(kTestDummyMd5, entry->md5_checksum());
583  ASSERT_EQ(1U, upload_progress_values.size());
584  EXPECT_EQ(test_util::ProgressInfo(1024 * 1024, 1024 * 1024),
585            upload_progress_values[0]);
586}
587
588}  // namespace drive
589