Index: content/browser/download/url_downloader_unittest.cc |
diff --git a/content/browser/download/url_downloader_unittest.cc b/content/browser/download/url_downloader_unittest.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..fc53237b9162867e11d05c93e2e9d82a581f9442 |
--- /dev/null |
+++ b/content/browser/download/url_downloader_unittest.cc |
@@ -0,0 +1,397 @@ |
+// Copyright 2013 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 <vector> |
+ |
+#include "base/bind.h" |
+#include "base/memory/ref_counted.h" |
+#include "base/memory/scoped_ptr.h" |
+#include "base/run_loop.h" |
+#include "base/strings/stringprintf.h" |
+#include "content/browser/byte_stream.h" |
+#include "content/browser/download/download_create_info.h" |
+#include "content/browser/download/download_interrupt_reasons_impl.h" |
+#include "content/browser/download/url_downloader.h" |
+#include "content/public/browser/browser_thread.h" |
+#include "content/public/browser/download_interrupt_reasons.h" |
+#include "content/public/browser/download_url_parameters.h" |
+#include "content/public/test/test_browser_context.h" |
+#include "content/public/test/test_browser_thread_bundle.h" |
+#include "net/base/net_errors.h" |
+#include "net/test/embedded_test_server/embedded_test_server.h" |
+#include "net/test/embedded_test_server/http_request.h" |
+#include "net/test/embedded_test_server/http_response.h" |
+#include "testing/gmock/include/gmock/gmock.h" |
+#include "testing/gtest/include/gtest/gtest.h" |
+ |
+using ::testing::AllOf; |
+using ::testing::Return; |
+using ::testing::_; |
+ |
+namespace content { |
+namespace { |
+ |
+class ByteStreamSlurper { |
+ public: |
+ typedef base::Callback<void(scoped_ptr<ByteStreamSlurper>)> |
+ CompletionCallback; |
+ |
+ ByteStreamSlurper(scoped_ptr<ByteStreamReader> reader, |
+ const CompletionCallback& callback); |
+ |
+ bool is_done() const { return is_done_; } |
+ int state() const { return state_; } |
+ const std::string& content() const { return content_; } |
+ |
+ void Start(); |
+ |
+ private: |
+ void Slurp(); |
+ |
+ scoped_ptr<ByteStreamReader> reader_; |
+ CompletionCallback callback_; |
+ bool is_done_; |
+ int state_; |
+ std::string content_; |
+}; |
+ |
+ByteStreamSlurper::ByteStreamSlurper(scoped_ptr<ByteStreamReader> reader, |
+ const CompletionCallback& callback) |
+ : reader_(reader.Pass()), |
+ callback_(callback), |
+ is_done_(false), |
+ state_(0) { |
+} |
+ |
+void ByteStreamSlurper::Start() { |
+ reader_->RegisterCallback( |
+ base::Bind(&ByteStreamSlurper::Slurp, |
+ base::Unretained(this))); |
+ // Initial slurp. |
+ Slurp(); |
+} |
+ |
+void ByteStreamSlurper::Slurp() { |
+ while (!is_done_) { |
+ scoped_refptr<net::IOBuffer> buffer; |
+ size_t length = 0; |
+ ByteStreamReader::StreamState state = reader_->Read(&buffer, &length); |
+ switch (state) { |
+ case ByteStreamReader::STREAM_EMPTY: |
+ return; // We should get called when there's data available. |
+ |
+ case ByteStreamReader::STREAM_COMPLETE: { |
+ DCHECK_EQ(0u, length); |
+ is_done_ = true; |
+ state_ = reader_->GetStatus(); |
+ scoped_ptr<ByteStreamSlurper> owned_this(this); |
+ BrowserThread::PostTask( |
+ BrowserThread::UI, |
+ FROM_HERE, |
+ base::Bind(callback_, base::Passed(&owned_this))); |
+ break; |
+ } |
+ |
+ case ByteStreamReader::STREAM_HAS_DATA: |
+ content_.append(buffer->data(), length); |
+ break; |
+ } |
+ } |
+} |
+ |
+class Response : public net::test_server::HttpResponse { |
+ public: |
+ Response() : data_(new Data()) {} |
+ Response(const Response& that) : data_(that.data_) {} |
+ Response& Code(net::HttpStatusCode code) { |
+ data_->response_.set_code(code); |
+ return *this; |
+ } |
+ Response& Header(const char* name, const char* value) { |
+ data_->response_.AddCustomHeader(name, value); |
+ return *this; |
+ } |
+ Response& Content(const char* content) { |
+ data_->response_.set_content(content); |
+ return *this; |
+ } |
+ Response& ContentType(const char* content_type) { |
+ data_->response_.set_content_type(content_type); |
+ return *this; |
+ } |
+ |
+ virtual std::string ToResponseString() const OVERRIDE { |
+ return data_->response_.ToResponseString(); |
+ } |
+ |
+ private: |
+ class Data : public base::RefCounted<Data> { |
+ public: |
+ Data() {} |
+ net::test_server::BasicHttpResponse response_; |
+ |
+ private: |
+ friend class base::RefCounted<Data>; |
+ virtual ~Data() {} |
+ }; |
+ |
+ scoped_refptr<Data> data_; |
+}; |
+ |
+class RequestHandler { |
+ public: |
+ net::test_server::EmbeddedTestServer::HandleRequestCallback GetCallback() { |
+ return base::Bind(&RequestHandler::HandleRequest, base::Unretained(this)); |
+ } |
+ |
+ MOCK_METHOD3(Request, |
+ Response(const std::string&, |
+ const std::map<std::string, std::string>&, |
+ const std::string&)); |
+ |
+ private: |
+ scoped_ptr<net::test_server::HttpResponse> HandleRequest( |
+ const net::test_server::HttpRequest& request); |
+}; |
+ |
+MATCHER_P2(HasHeader, name, value, "") { |
+ return arg.find(name) != arg.end() && arg.find(name)->second == value; |
+} |
+ |
+scoped_ptr<net::test_server::HttpResponse> RequestHandler::HandleRequest( |
+ const net::test_server::HttpRequest& request) { |
+ scoped_ptr<Response> response(new Response( |
+ Request(request.relative_url, request.headers, request.content))); |
+ return response.PassAs<net::test_server::HttpResponse>(); |
+} |
+ |
+class UrlDownloaderTest : public testing::Test { |
+ public: |
+ UrlDownloaderTest() {} |
+ virtual ~UrlDownloaderTest() {} |
+ |
+ virtual void SetUp() OVERRIDE { |
+ test_server_.reset(new net::test_server::EmbeddedTestServer( |
+ BrowserThread::GetMessageLoopProxyForThread(BrowserThread::IO))); |
+ test_server_->RegisterRequestHandler(handler_.GetCallback()); |
+ } |
+ |
+ virtual void TearDown() OVERRIDE { |
+ ASSERT_TRUE(test_server_->ShutdownAndWaitUntilComplete()); |
+ base::RunLoop run_loop; |
+ run_loop.RunUntilIdle(); |
+ } |
+ |
+ DownloadInterruptReason StartRequestAndWaitForCallback( |
+ DownloadRequestHandle* request_handle, |
+ scoped_ptr<DownloadCreateInfo>* info) { |
+ base::RunLoop run_loop; |
+ DownloadInterruptReason result = DOWNLOAD_INTERRUPT_REASON_NONE; |
+ request_handle->Start(base::Bind(&UrlDownloaderTest::StartRequestCallback, |
+ &result, |
+ info, |
+ run_loop.QuitClosure())); |
+ run_loop.Run(); |
+ return result; |
+ } |
+ |
+ DownloadInterruptReason SlurpByteStream( |
+ scoped_ptr<ByteStreamReader> stream_reader, |
+ std::string* content) { |
+ int status = 0; |
+ base::RunLoop run_loop; |
+ scoped_ptr<ByteStreamSlurper> slurper( |
+ new ByteStreamSlurper(stream_reader.Pass(), |
+ base::Bind(&UrlDownloaderTest::SlurpDoneCallback, |
+ &status, |
+ content, |
+ run_loop.QuitClosure()))); |
+ BrowserThread::PostTask(BrowserThread::IO, |
+ FROM_HERE, |
+ base::Bind(&ByteStreamSlurper::Start, |
+ base::Unretained(slurper.release()))); |
+ run_loop.Run(); |
+ return static_cast<DownloadInterruptReason>(status); |
+ } |
+ |
+ TestBrowserContext* test_browser_context() { return &test_browser_context_; } |
+ |
+ RequestHandler* request_handler() { return &handler_; } |
+ |
+ net::test_server::EmbeddedTestServer* test_server() { |
+ return test_server_.get(); |
+ } |
+ |
+ private: |
+ static void StartRequestCallback(DownloadInterruptReason* store_result, |
+ scoped_ptr<DownloadCreateInfo>* store_info, |
+ const base::Closure& closure, |
+ DownloadInterruptReason result, |
+ scoped_ptr<DownloadCreateInfo> create_info) { |
+ *store_result = result; |
+ store_info->swap(create_info); |
+ closure.Run(); |
+ } |
+ |
+ static void SlurpDoneCallback(int* status, |
+ std::string* content, |
+ const base::Closure& closure, |
+ scoped_ptr<ByteStreamSlurper> slurper) { |
+ *status = slurper->state(); |
+ *content = slurper->content(); |
+ BrowserThread::DeleteSoon(BrowserThread::IO, FROM_HERE, slurper.release()); |
+ closure.Run(); |
+ } |
+ |
+ TestBrowserThreadBundle thread_bundle_; |
+ TestBrowserContext test_browser_context_; |
+ scoped_ptr<net::test_server::EmbeddedTestServer> test_server_; |
+ RequestHandler handler_; |
+}; |
+ |
+const char* const kDummyURL = "/foo"; |
+const char* const kTextContentType = "text/plain"; |
+const char* const kDummyContent = "Hello world"; |
+ |
+TEST_F(UrlDownloaderTest, UrlDownloader_Start) { |
+ const char* kETag = "abcd"; |
+ ASSERT_TRUE(test_server()->InitializeAndWaitUntilReady()); |
+ GURL url = test_server()->GetURL(kDummyURL); |
+ scoped_ptr<DownloadUrlParameters> params(new DownloadUrlParameters( |
+ url, -1, -1, test_browser_context()->GetResourceContext())); |
+ scoped_ptr<DownloadRequestHandle> request_handle( |
+ UrlDownloader::CreateDownloadRequest(params.Pass())); |
+ |
+ EXPECT_CALL(*request_handler(), Request(kDummyURL, _, _)) |
+ .WillOnce(Return(Response() |
+ .Code(net::HTTP_OK) |
+ .Header("etag", kETag) |
+ .ContentType(kTextContentType) |
+ .Content(kDummyContent))); |
+ scoped_ptr<DownloadCreateInfo> info; |
+ DownloadInterruptReason result = |
+ StartRequestAndWaitForCallback(request_handle.get(), &info); |
+ ASSERT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE, result); |
+ ASSERT_TRUE(info); |
+ EXPECT_STREQ(kETag, info->etag.c_str()); |
+ EXPECT_STREQ(kTextContentType, info->mime_type.c_str()); |
+ |
+ std::string content; |
+ ASSERT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE, |
+ SlurpByteStream(info->stream_reader.Pass(), &content)); |
+ EXPECT_EQ(kDummyContent, content); |
+} |
+ |
+TEST_F(UrlDownloaderTest, UrlDownloader_ValidRedirects) { |
+ ASSERT_TRUE(test_server()->InitializeAndWaitUntilReady()); |
+ static const char* const kURL0 = "/foo"; |
+ static const char* const kURL1 = "/bar"; |
+ static const char* const kURL2 = "/baz"; |
+ std::vector<GURL> urls; |
+ urls.push_back(test_server()->GetURL(kURL0)); |
+ urls.push_back(test_server()->GetURL(kURL1)); |
+ urls.push_back(test_server()->GetURL(kURL2)); |
+ |
+ scoped_ptr<DownloadUrlParameters> params( |
+ new DownloadUrlParameters(urls[0], -1, -1, |
+ test_browser_context()->GetResourceContext())); |
+ scoped_ptr<DownloadRequestHandle> request_handle( |
+ UrlDownloader::CreateDownloadRequest(params.Pass())); |
+ |
+ EXPECT_CALL(*request_handler(), Request(kURL0, _, _)) |
+ .WillOnce(Return(Response() |
+ .Code(net::HTTP_TEMPORARY_REDIRECT) |
+ .Header("location", urls[1].spec().c_str()))); |
+ EXPECT_CALL(*request_handler(), Request(kURL1, _, _)) |
+ .WillOnce(Return(Response() |
+ .Code(net::HTTP_TEMPORARY_REDIRECT) |
+ .Header("location", urls[2].spec().c_str()))); |
+ EXPECT_CALL(*request_handler(), Request(kURL2, _, _)) |
+ .WillOnce(Return(Response() |
+ .Code(net::HTTP_OK) |
+ .ContentType(kTextContentType) |
+ .Content(kDummyContent))); |
+ scoped_ptr<DownloadCreateInfo> info; |
+ DownloadInterruptReason result = |
+ StartRequestAndWaitForCallback(request_handle.get(), &info); |
+ ASSERT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE, result); |
+ ASSERT_TRUE(info); |
+ EXPECT_EQ(urls, info->url_chain); |
+} |
+ |
+TEST_F(UrlDownloaderTest, UrlDownloader_InvalidRedirects) { |
+ ASSERT_TRUE(test_server()->InitializeAndWaitUntilReady()); |
+ static const char* const kURL0 = "/foo"; |
+ static const char* const kURL1 = "/bar"; |
+ static const char* const kURL2 = "notavalidscheme://baz"; |
+ std::vector<GURL> urls; |
+ urls.push_back(test_server()->GetURL(kURL0)); |
+ urls.push_back(test_server()->GetURL(kURL1)); |
+ urls.push_back(GURL(kURL2)); |
+ |
+ scoped_ptr<DownloadUrlParameters> params( |
+ new DownloadUrlParameters(urls[0], -1, -1, |
+ test_browser_context()->GetResourceContext())); |
+ scoped_ptr<DownloadRequestHandle> request_handle( |
+ UrlDownloader::CreateDownloadRequest(params.Pass())); |
+ |
+ EXPECT_CALL(*request_handler(), Request(kURL0, _, _)) |
+ .WillOnce(Return(Response() |
+ .Code(net::HTTP_TEMPORARY_REDIRECT) |
+ .Header("location", urls[1].spec().c_str()))); |
+ EXPECT_CALL(*request_handler(), Request(kURL1, _, _)) |
+ .WillOnce(Return(Response() |
+ .Code(net::HTTP_TEMPORARY_REDIRECT) |
+ .Header("location", urls[2].spec().c_str()))); |
+ scoped_ptr<DownloadCreateInfo> info; |
+ DownloadInterruptReason result = |
+ StartRequestAndWaitForCallback(request_handle.get(), &info); |
+ EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST, result); |
+ EXPECT_FALSE(info); |
+} |
+ |
+TEST_F(UrlDownloaderTest, UrlDownloader_404Handling) { |
+ ASSERT_TRUE(test_server()->InitializeAndWaitUntilReady()); |
+ scoped_ptr<DownloadUrlParameters> params( |
+ new DownloadUrlParameters(test_server()->GetURL(kDummyURL), |
+ -1, |
+ -1, |
+ test_browser_context()->GetResourceContext())); |
+ scoped_ptr<DownloadRequestHandle> request_handle( |
+ UrlDownloader::CreateDownloadRequest(params.Pass())); |
+ EXPECT_CALL(*request_handler(), Request(kDummyURL, _, _)) |
+ .WillOnce(Return(Response().Code(net::HTTP_NOT_FOUND))); |
+ scoped_ptr<DownloadCreateInfo> info; |
+ DownloadInterruptReason reason = |
+ StartRequestAndWaitForCallback(request_handle.get(), &info); |
+ EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE, reason); |
+ EXPECT_TRUE(info); |
+ |
+ std::string content; |
+ EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_SERVER_BAD_CONTENT, |
+ SlurpByteStream(info->stream_reader.Pass(), &content)); |
+ EXPECT_TRUE(content.empty()); |
+} |
+ |
+TEST_F(UrlDownloaderTest, UrlDownloader_InvalidScheme) { |
+ GURL url("notarealscheme://foo/bar"); |
+ scoped_ptr<DownloadUrlParameters> params(new DownloadUrlParameters( |
+ url, -1, -1, test_browser_context()->GetResourceContext())); |
+ scoped_ptr<DownloadRequestHandle> request_handle( |
+ UrlDownloader::CreateDownloadRequest(params.Pass())); |
+ scoped_ptr<DownloadCreateInfo> info; |
+ DownloadInterruptReason reason = |
+ StartRequestAndWaitForCallback(request_handle.get(), &info); |
+ EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST, reason); |
+ EXPECT_FALSE(info); |
+} |
+ |
+// TODO(asanka): Test that back pressure is commuted correctly. |
+// TODO(asanka): Test that a successful response results in a |
+// DownloadCreateInfo and that the response data is available. |
+// TODO(asanka): Test that responses with errors are handled correctly. |
+// TODO(asanka): Test that request headers are set correctly. |
+// TODO(asanka): Test that pausing and resuming work correctly. |
+} // namespace |
+} // namespace content |