|  | // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include <stack> | 
|  | #include <utility> | 
|  |  | 
|  | #include "base/bind.h" | 
|  | #include "base/bind_helpers.h" | 
|  | #include "base/file_path.h" | 
|  | #include "base/file_util.h" | 
|  | #include "base/memory/ref_counted.h" | 
|  | #include "base/memory/scoped_ptr.h" | 
|  | #include "base/message_loop_proxy.h" | 
|  | #include "base/scoped_temp_dir.h" | 
|  | #include "base/synchronization/waitable_event.h" | 
|  | #include "base/threading/thread.h" | 
|  | #include "base/time.h" | 
|  | #include "net/base/file_stream.h" | 
|  | #include "net/base/io_buffer.h" | 
|  | #include "net/base/net_errors.h" | 
|  | #include "net/http/http_request_headers.h" | 
|  | #include "net/http/http_response_headers.h" | 
|  | #include "net/url_request/url_request.h" | 
|  | #include "net/url_request/url_request_context.h" | 
|  | #include "net/url_request/url_request_error_job.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  | #include "webkit/blob/blob_data.h" | 
|  | #include "webkit/blob/blob_url_request_job.h" | 
|  |  | 
|  | namespace webkit_blob { | 
|  |  | 
|  | static const int kBufferSize = 1024; | 
|  | static const char kTestData1[] = "Hello"; | 
|  | static const char kTestData2[] = "Here it is data."; | 
|  | static const char kTestFileData1[] = "0123456789"; | 
|  | static const char kTestFileData2[] = "This is sample file."; | 
|  | static const char kTestContentType[] = "foo/bar"; | 
|  | static const char kTestContentDisposition[] = "attachment; filename=foo.txt"; | 
|  |  | 
|  | class BlobURLRequestJobTest : public testing::Test { | 
|  | public: | 
|  |  | 
|  | // Test Harness ------------------------------------------------------------- | 
|  | // TODO(jianli): share this test harness with AppCacheURLRequestJobTest | 
|  |  | 
|  | class MockURLRequestDelegate : public net::URLRequest::Delegate { | 
|  | public: | 
|  | explicit MockURLRequestDelegate(BlobURLRequestJobTest* test) | 
|  | : test_(test), | 
|  | received_data_(new net::IOBuffer(kBufferSize)) { | 
|  | } | 
|  |  | 
|  | virtual void OnResponseStarted(net::URLRequest* request) { | 
|  | if (request->status().is_success()) { | 
|  | EXPECT_TRUE(request->response_headers()); | 
|  | ReadSome(request); | 
|  | } else { | 
|  | RequestComplete(); | 
|  | } | 
|  | } | 
|  |  | 
|  | virtual void OnReadCompleted(net::URLRequest* request, int bytes_read) { | 
|  | if (bytes_read > 0) | 
|  | ReceiveData(request, bytes_read); | 
|  | else | 
|  | RequestComplete(); | 
|  | } | 
|  |  | 
|  | const std::string& response_data() const { return response_data_; } | 
|  |  | 
|  | private: | 
|  | void ReadSome(net::URLRequest* request) { | 
|  | if (!request->is_pending()) { | 
|  | RequestComplete(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | int bytes_read = 0; | 
|  | if (!request->Read(received_data_.get(), kBufferSize, &bytes_read)) { | 
|  | if (!request->status().is_io_pending()) { | 
|  | RequestComplete(); | 
|  | } | 
|  | return; | 
|  | } | 
|  |  | 
|  | ReceiveData(request, bytes_read); | 
|  | } | 
|  |  | 
|  | void ReceiveData(net::URLRequest* request, int bytes_read) { | 
|  | if (bytes_read) { | 
|  | response_data_.append(received_data_->data(), | 
|  | static_cast<size_t>(bytes_read)); | 
|  | ReadSome(request); | 
|  | } else { | 
|  | RequestComplete(); | 
|  | } | 
|  | } | 
|  |  | 
|  | void RequestComplete() { | 
|  | test_->ScheduleNextTask(); | 
|  | } | 
|  |  | 
|  | BlobURLRequestJobTest* test_; | 
|  | scoped_refptr<net::IOBuffer> received_data_; | 
|  | std::string response_data_; | 
|  | }; | 
|  |  | 
|  | // Helper class run a test on our io_thread. The io_thread | 
|  | // is spun up once and reused for all tests. | 
|  | template <class Method> | 
|  | void MethodWrapper(Method method) { | 
|  | SetUpTest(); | 
|  | (this->*method)(); | 
|  | } | 
|  |  | 
|  | void SetUp() { | 
|  | ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); | 
|  |  | 
|  | temp_file1_ = temp_dir_.path().AppendASCII("BlobFile1.dat"); | 
|  | ASSERT_EQ(static_cast<int>(arraysize(kTestFileData1) - 1), | 
|  | file_util::WriteFile(temp_file1_, kTestFileData1, | 
|  | arraysize(kTestFileData1) - 1)); | 
|  | base::PlatformFileInfo file_info1; | 
|  | file_util::GetFileInfo(temp_file1_, &file_info1); | 
|  | temp_file_modification_time1_ = file_info1.last_modified; | 
|  |  | 
|  | temp_file2_ = temp_dir_.path().AppendASCII("BlobFile2.dat"); | 
|  | ASSERT_EQ(static_cast<int>(arraysize(kTestFileData2) - 1), | 
|  | file_util::WriteFile(temp_file2_, kTestFileData2, | 
|  | arraysize(kTestFileData2) - 1)); | 
|  | base::PlatformFileInfo file_info2; | 
|  | file_util::GetFileInfo(temp_file2_, &file_info2); | 
|  | temp_file_modification_time2_ = file_info2.last_modified; | 
|  |  | 
|  | io_thread_.reset(new base::Thread("BlobRLRequestJobTest Thread")); | 
|  | base::Thread::Options options(MessageLoop::TYPE_IO, 0); | 
|  | io_thread_->StartWithOptions(options); | 
|  | } | 
|  |  | 
|  | void TearDown() { | 
|  | io_thread_.reset(NULL); | 
|  | } | 
|  |  | 
|  | static net::URLRequestJob* BlobURLRequestJobFactory( | 
|  | net::URLRequest* request, | 
|  | net::NetworkDelegate* network_delegate, | 
|  | const std::string& scheme) { | 
|  | BlobURLRequestJob* temp = blob_url_request_job_; | 
|  | blob_url_request_job_ = NULL; | 
|  | return temp; | 
|  | } | 
|  |  | 
|  | BlobURLRequestJobTest() | 
|  | : expected_status_code_(0) { | 
|  | } | 
|  |  | 
|  | template <class Method> | 
|  | void RunTestOnIOThread(Method method) { | 
|  | test_finished_event_ .reset(new base::WaitableEvent(false, false)); | 
|  | io_thread_->message_loop()->PostTask( | 
|  | FROM_HERE, base::Bind(&BlobURLRequestJobTest::MethodWrapper<Method>, | 
|  | base::Unretained(this), method)); | 
|  | test_finished_event_->Wait(); | 
|  | } | 
|  |  | 
|  | void SetUpTest() { | 
|  | DCHECK(MessageLoop::current() == io_thread_->message_loop()); | 
|  |  | 
|  | net::URLRequest::Deprecated::RegisterProtocolFactory( | 
|  | "blob", &BlobURLRequestJobFactory); | 
|  | url_request_delegate_.reset(new MockURLRequestDelegate(this)); | 
|  | } | 
|  |  | 
|  | void TearDownTest() { | 
|  | DCHECK(MessageLoop::current() == io_thread_->message_loop()); | 
|  |  | 
|  | request_.reset(); | 
|  | url_request_delegate_.reset(); | 
|  |  | 
|  | DCHECK(!blob_url_request_job_); | 
|  | net::URLRequest::Deprecated::RegisterProtocolFactory("blob", NULL); | 
|  | } | 
|  |  | 
|  | void TestFinished() { | 
|  | // We unwind the stack prior to finishing up to let stack | 
|  | // based objects get deleted. | 
|  | DCHECK(MessageLoop::current() == io_thread_->message_loop()); | 
|  | MessageLoop::current()->PostTask( | 
|  | FROM_HERE, | 
|  | base::Bind(&BlobURLRequestJobTest::TestFinishedUnwound, | 
|  | base::Unretained(this))); | 
|  | } | 
|  |  | 
|  | void TestFinishedUnwound() { | 
|  | TearDownTest(); | 
|  | test_finished_event_->Signal(); | 
|  | } | 
|  |  | 
|  | void PushNextTask(const base::Closure& task) { | 
|  | task_stack_.push(std::pair<base::Closure, bool>(task, false)); | 
|  | } | 
|  |  | 
|  | void PushNextTaskAsImmediate(const base::Closure& task) { | 
|  | task_stack_.push(std::pair<base::Closure, bool>(task, true)); | 
|  | } | 
|  |  | 
|  | void ScheduleNextTask() { | 
|  | DCHECK(MessageLoop::current() == io_thread_->message_loop()); | 
|  | if (task_stack_.empty()) { | 
|  | TestFinished(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | base::Closure task = task_stack_.top().first; | 
|  | bool immediate = task_stack_.top().second; | 
|  | task_stack_.pop(); | 
|  | if (immediate) | 
|  | task.Run(); | 
|  | else | 
|  | MessageLoop::current()->PostTask(FROM_HERE, task); | 
|  | } | 
|  |  | 
|  | void TestSuccessRequest(BlobData* blob_data, | 
|  | const std::string& expected_response) { | 
|  | PushNextTask(base::Bind(&BlobURLRequestJobTest::VerifyResponse, | 
|  | base::Unretained(this))); | 
|  | expected_status_code_ = 200; | 
|  | expected_response_ = expected_response; | 
|  | return TestRequest("GET", net::HttpRequestHeaders(), blob_data); | 
|  | } | 
|  |  | 
|  | void TestErrorRequest(BlobData* blob_data, | 
|  | int expected_status_code) { | 
|  | PushNextTask(base::Bind(&BlobURLRequestJobTest::VerifyResponse, | 
|  | base::Unretained(this))); | 
|  | expected_status_code_ = expected_status_code; | 
|  | expected_response_ = ""; | 
|  | return TestRequest("GET", net::HttpRequestHeaders(), blob_data); | 
|  | } | 
|  |  | 
|  | void TestRequest(const std::string& method, | 
|  | const net::HttpRequestHeaders& extra_headers, | 
|  | BlobData* blob_data) { | 
|  | // This test has async steps. | 
|  | request_.reset(empty_context_.CreateRequest( | 
|  | GURL("blob:blah"), url_request_delegate_.get())); | 
|  | request_->set_method(method); | 
|  | blob_url_request_job_ = new BlobURLRequestJob( | 
|  | request_.get(), | 
|  | empty_context_.network_delegate(), | 
|  | blob_data, | 
|  | base::MessageLoopProxy::current()); | 
|  |  | 
|  | // Start the request. | 
|  | if (!extra_headers.IsEmpty()) | 
|  | request_->SetExtraRequestHeaders(extra_headers); | 
|  | request_->Start(); | 
|  |  | 
|  | // Completion is async. | 
|  | } | 
|  |  | 
|  | void VerifyResponse() { | 
|  | EXPECT_TRUE(request_->status().is_success()); | 
|  | EXPECT_EQ(expected_status_code_, | 
|  | request_->response_headers()->response_code()); | 
|  | EXPECT_STREQ(expected_response_.c_str(), | 
|  | url_request_delegate_->response_data().c_str()); | 
|  | TestFinished(); | 
|  | } | 
|  |  | 
|  | // Test Cases --------------------------------------------------------------- | 
|  |  | 
|  | void TestGetSimpleDataRequest() { | 
|  | scoped_refptr<BlobData> blob_data(new BlobData()); | 
|  | blob_data->AppendData(kTestData1); | 
|  | TestSuccessRequest(blob_data, kTestData1); | 
|  | } | 
|  |  | 
|  | void TestGetSimpleFileRequest() { | 
|  | scoped_refptr<BlobData> blob_data(new BlobData()); | 
|  | blob_data->AppendFile(temp_file1_, 0, -1, base::Time()); | 
|  | TestSuccessRequest(blob_data, kTestFileData1); | 
|  | } | 
|  |  | 
|  | void TestGetLargeFileRequest() { | 
|  | scoped_refptr<BlobData> blob_data(new BlobData()); | 
|  | FilePath large_temp_file = temp_dir_.path().AppendASCII("LargeBlob.dat"); | 
|  | std::string large_data; | 
|  | large_data.reserve(kBufferSize * 5); | 
|  | for (int i = 0; i < kBufferSize * 5; ++i) | 
|  | large_data.append(1, static_cast<char>(i % 256)); | 
|  | ASSERT_EQ(static_cast<int>(large_data.size()), | 
|  | file_util::WriteFile(large_temp_file, large_data.data(), | 
|  | large_data.size())); | 
|  | blob_data->AppendFile(large_temp_file, 0, -1, base::Time()); | 
|  | TestSuccessRequest(blob_data, large_data); | 
|  | } | 
|  |  | 
|  | void TestGetNonExistentFileRequest() { | 
|  | FilePath non_existent_file = | 
|  | temp_file1_.InsertBeforeExtension(FILE_PATH_LITERAL("-na")); | 
|  | scoped_refptr<BlobData> blob_data(new BlobData()); | 
|  | blob_data->AppendFile(non_existent_file, 0, -1, base::Time()); | 
|  | TestErrorRequest(blob_data, 404); | 
|  | } | 
|  |  | 
|  | void TestGetChangedFileRequest() { | 
|  | scoped_refptr<BlobData> blob_data(new BlobData()); | 
|  | base::Time old_time = | 
|  | temp_file_modification_time1_ - base::TimeDelta::FromSeconds(10); | 
|  | blob_data->AppendFile(temp_file1_, 0, 3, old_time); | 
|  | TestErrorRequest(blob_data, 404); | 
|  | } | 
|  |  | 
|  | void TestGetSlicedFileRequest() { | 
|  | scoped_refptr<BlobData> blob_data(new BlobData()); | 
|  | blob_data->AppendFile(temp_file1_, 2, 4, temp_file_modification_time1_); | 
|  | std::string result(kTestFileData1 + 2, 4); | 
|  | TestSuccessRequest(blob_data, result); | 
|  | } | 
|  |  | 
|  | scoped_refptr<BlobData> BuildComplicatedData(std::string* expected_result) { | 
|  | scoped_refptr<BlobData> blob_data(new BlobData()); | 
|  | blob_data->AppendData(kTestData1 + 1, 2); | 
|  | blob_data->AppendFile(temp_file1_, 2, 3, temp_file_modification_time1_); | 
|  | blob_data->AppendData(kTestData2 + 3, 4); | 
|  | blob_data->AppendFile(temp_file2_, 4, 5, temp_file_modification_time2_); | 
|  | *expected_result = std::string(kTestData1 + 1, 2); | 
|  | *expected_result += std::string(kTestFileData1 + 2, 3); | 
|  | *expected_result += std::string(kTestData2 + 3, 4); | 
|  | *expected_result += std::string(kTestFileData2 + 4, 5); | 
|  | return blob_data; | 
|  | } | 
|  |  | 
|  | void TestGetComplicatedDataAndFileRequest() { | 
|  | std::string result; | 
|  | scoped_refptr<BlobData> blob_data = BuildComplicatedData(&result); | 
|  | TestSuccessRequest(blob_data, result); | 
|  | } | 
|  |  | 
|  | void TestGetRangeRequest1() { | 
|  | std::string result; | 
|  | scoped_refptr<BlobData> blob_data = BuildComplicatedData(&result); | 
|  | net::HttpRequestHeaders extra_headers; | 
|  | extra_headers.SetHeader(net::HttpRequestHeaders::kRange, "bytes=5-10"); | 
|  | PushNextTask(base::Bind(&BlobURLRequestJobTest::VerifyResponse, | 
|  | base::Unretained(this))); | 
|  | expected_status_code_ = 206; | 
|  | expected_response_ = result.substr(5, 10 - 5 + 1); | 
|  | return TestRequest("GET", extra_headers, blob_data); | 
|  | } | 
|  |  | 
|  | void TestGetRangeRequest2() { | 
|  | std::string result; | 
|  | scoped_refptr<BlobData> blob_data = BuildComplicatedData(&result); | 
|  | net::HttpRequestHeaders extra_headers; | 
|  | extra_headers.SetHeader(net::HttpRequestHeaders::kRange, "bytes=-10"); | 
|  | PushNextTask(base::Bind(&BlobURLRequestJobTest::VerifyResponse, | 
|  | base::Unretained(this))); | 
|  | expected_status_code_ = 206; | 
|  | expected_response_ = result.substr(result.length() - 10); | 
|  | return TestRequest("GET", extra_headers, blob_data); | 
|  | } | 
|  |  | 
|  | void TestExtraHeaders() { | 
|  | scoped_refptr<BlobData> blob_data(new BlobData()); | 
|  | blob_data->set_content_type(kTestContentType); | 
|  | blob_data->set_content_disposition(kTestContentDisposition); | 
|  | blob_data->AppendData(kTestData1); | 
|  | PushNextTask( | 
|  | base::Bind(&BlobURLRequestJobTest::VerifyResponseForTestExtraHeaders, | 
|  | base::Unretained(this))); | 
|  | TestRequest("GET", net::HttpRequestHeaders(), blob_data); | 
|  | } | 
|  |  | 
|  | void VerifyResponseForTestExtraHeaders() { | 
|  | EXPECT_TRUE(request_->status().is_success()); | 
|  | EXPECT_EQ(request_->response_headers()->response_code(), 200); | 
|  | EXPECT_STREQ(url_request_delegate_->response_data().c_str(), kTestData1); | 
|  | std::string content_type; | 
|  | EXPECT_TRUE(request_->response_headers()->GetMimeType(&content_type)); | 
|  | EXPECT_STREQ(content_type.c_str(), kTestContentType); | 
|  | void* iter = NULL; | 
|  | std::string content_disposition; | 
|  | EXPECT_TRUE(request_->response_headers()->EnumerateHeader( | 
|  | &iter, "Content-Disposition", &content_disposition)); | 
|  | EXPECT_STREQ(content_disposition.c_str(), kTestContentDisposition); | 
|  | TestFinished(); | 
|  | } | 
|  |  | 
|  | private: | 
|  | ScopedTempDir temp_dir_; | 
|  | FilePath temp_file1_; | 
|  | FilePath temp_file2_; | 
|  | base::Time temp_file_modification_time1_; | 
|  | base::Time temp_file_modification_time2_; | 
|  | scoped_ptr<base::Thread> io_thread_; | 
|  | static BlobURLRequestJob* blob_url_request_job_; | 
|  |  | 
|  | scoped_ptr<base::WaitableEvent> test_finished_event_; | 
|  | std::stack<std::pair<base::Closure, bool> > task_stack_; | 
|  | net::URLRequestContext empty_context_; | 
|  | scoped_ptr<net::URLRequest> request_; | 
|  | scoped_ptr<MockURLRequestDelegate> url_request_delegate_; | 
|  | int expected_status_code_; | 
|  | std::string expected_response_; | 
|  | }; | 
|  |  | 
|  | // static | 
|  | BlobURLRequestJob* BlobURLRequestJobTest::blob_url_request_job_ = NULL; | 
|  |  | 
|  | TEST_F(BlobURLRequestJobTest, TestGetSimpleDataRequest) { | 
|  | RunTestOnIOThread(&BlobURLRequestJobTest::TestGetSimpleDataRequest); | 
|  | } | 
|  |  | 
|  | TEST_F(BlobURLRequestJobTest, TestGetSimpleFileRequest) { | 
|  | RunTestOnIOThread(&BlobURLRequestJobTest::TestGetSimpleFileRequest); | 
|  | } | 
|  |  | 
|  | TEST_F(BlobURLRequestJobTest, TestGetLargeFileRequest) { | 
|  | RunTestOnIOThread(&BlobURLRequestJobTest::TestGetLargeFileRequest); | 
|  | } | 
|  |  | 
|  | TEST_F(BlobURLRequestJobTest, TestGetSlicedFileRequest) { | 
|  | RunTestOnIOThread(&BlobURLRequestJobTest::TestGetSlicedFileRequest); | 
|  | } | 
|  |  | 
|  | TEST_F(BlobURLRequestJobTest, TestGetNonExistentFileRequest) { | 
|  | RunTestOnIOThread(&BlobURLRequestJobTest::TestGetNonExistentFileRequest); | 
|  | } | 
|  |  | 
|  | TEST_F(BlobURLRequestJobTest, TestGetChangedFileRequest) { | 
|  | RunTestOnIOThread(&BlobURLRequestJobTest::TestGetChangedFileRequest); | 
|  | } | 
|  |  | 
|  | TEST_F(BlobURLRequestJobTest, TestGetComplicatedDataAndFileRequest) { | 
|  | RunTestOnIOThread( | 
|  | &BlobURLRequestJobTest::TestGetComplicatedDataAndFileRequest); | 
|  | } | 
|  |  | 
|  | TEST_F(BlobURLRequestJobTest, TestGetRangeRequest1) { | 
|  | RunTestOnIOThread(&BlobURLRequestJobTest::TestGetRangeRequest1); | 
|  | } | 
|  |  | 
|  | TEST_F(BlobURLRequestJobTest, TestGetRangeRequest2) { | 
|  | RunTestOnIOThread(&BlobURLRequestJobTest::TestGetRangeRequest2); | 
|  | } | 
|  |  | 
|  | TEST_F(BlobURLRequestJobTest, TestExtraHeaders) { | 
|  | RunTestOnIOThread(&BlobURLRequestJobTest::TestExtraHeaders); | 
|  | } | 
|  |  | 
|  | }  // namespace webkit_blob |