|  | // 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 "components/update_client/crx_downloader.h" | 
|  |  | 
|  | #include <memory> | 
|  | #include <utility> | 
|  |  | 
|  | #include "base/bind.h" | 
|  | #include "base/files/file_path.h" | 
|  | #include "base/files/file_util.h" | 
|  | #include "base/memory/ref_counted.h" | 
|  | #include "base/path_service.h" | 
|  | #include "base/run_loop.h" | 
|  | #include "base/test/bind_test_util.h" | 
|  | #include "base/test/task_environment.h" | 
|  | #include "base/threading/thread_task_runner_handle.h" | 
|  | #include "build/build_config.h" | 
|  | #include "components/update_client/net/network_chromium.h" | 
|  | #include "components/update_client/update_client_errors.h" | 
|  | #include "components/update_client/utils.h" | 
|  | #include "net/base/net_errors.h" | 
|  | #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" | 
|  | #include "services/network/test/test_url_loader_factory.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  |  | 
|  | using base::ContentsEqual; | 
|  |  | 
|  | namespace update_client { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | const char kTestFileName[] = "jebgalgnebhfojomionfpkfelancnnkf.crx"; | 
|  |  | 
|  | const char hash_jebg[] = | 
|  | "7ab32f071cd9b5ef8e0d7913be161f532d98b3e9fa284a7cd8059c3409ce0498"; | 
|  |  | 
|  | base::FilePath MakeTestFilePath(const char* file) { | 
|  | base::FilePath path; | 
|  | base::PathService::Get(base::DIR_SOURCE_ROOT, &path); | 
|  | return path.AppendASCII("components/test/data/update_client") | 
|  | .AppendASCII(file); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | class CrxDownloaderTest : public testing::Test { | 
|  | public: | 
|  | CrxDownloaderTest(); | 
|  | ~CrxDownloaderTest() override; | 
|  |  | 
|  | // Overrides from testing::Test. | 
|  | void SetUp() override; | 
|  | void TearDown() override; | 
|  |  | 
|  | void Quit(); | 
|  | void RunThreads(); | 
|  | void RunThreadsUntilIdle(); | 
|  |  | 
|  | void DownloadComplete(int crx_context, const CrxDownloader::Result& result); | 
|  |  | 
|  | void DownloadProgress(int crx_context); | 
|  |  | 
|  | int GetInterceptorCount() { return interceptor_count_; } | 
|  |  | 
|  | void AddResponse(const GURL& url, | 
|  | const base::FilePath& file_path, | 
|  | int net_error); | 
|  |  | 
|  | protected: | 
|  | std::unique_ptr<CrxDownloader> crx_downloader_; | 
|  |  | 
|  | network::TestURLLoaderFactory test_url_loader_factory_; | 
|  |  | 
|  | CrxDownloader::DownloadCallback callback_; | 
|  | CrxDownloader::ProgressCallback progress_callback_; | 
|  |  | 
|  | int crx_context_; | 
|  |  | 
|  | int num_download_complete_calls_; | 
|  | CrxDownloader::Result download_complete_result_; | 
|  |  | 
|  | // These members are updated by DownloadProgress. | 
|  | int num_progress_calls_; | 
|  |  | 
|  | // Accumulates the number of loads triggered. | 
|  | int interceptor_count_ = 0; | 
|  |  | 
|  | // A magic value for the context to be used in the tests. | 
|  | static const int kExpectedContext = 0xaabb; | 
|  |  | 
|  | private: | 
|  | base::test::TaskEnvironment task_environment_; | 
|  | scoped_refptr<network::SharedURLLoaderFactory> | 
|  | test_shared_url_loader_factory_; | 
|  | base::OnceClosure quit_closure_; | 
|  | }; | 
|  |  | 
|  | const int CrxDownloaderTest::kExpectedContext; | 
|  |  | 
|  | CrxDownloaderTest::CrxDownloaderTest() | 
|  | : callback_(base::BindOnce(&CrxDownloaderTest::DownloadComplete, | 
|  | base::Unretained(this), | 
|  | kExpectedContext)), | 
|  | progress_callback_( | 
|  | base::BindRepeating(&CrxDownloaderTest::DownloadProgress, | 
|  | base::Unretained(this), | 
|  | kExpectedContext)), | 
|  | crx_context_(0), | 
|  | num_download_complete_calls_(0), | 
|  | num_progress_calls_(0), | 
|  | task_environment_(base::test::TaskEnvironment::MainThreadType::IO), | 
|  | test_shared_url_loader_factory_( | 
|  | base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>( | 
|  | &test_url_loader_factory_)) {} | 
|  |  | 
|  | CrxDownloaderTest::~CrxDownloaderTest() {} | 
|  |  | 
|  | void CrxDownloaderTest::SetUp() { | 
|  | num_download_complete_calls_ = 0; | 
|  | download_complete_result_ = CrxDownloader::Result(); | 
|  | num_progress_calls_ = 0; | 
|  |  | 
|  | // Do not use the background downloader in these tests. | 
|  | crx_downloader_ = CrxDownloader::Create( | 
|  | false, base::MakeRefCounted<NetworkFetcherChromiumFactory>( | 
|  | test_shared_url_loader_factory_, | 
|  | base::BindRepeating([](const GURL& url) { return false; }))); | 
|  | crx_downloader_->set_progress_callback(progress_callback_); | 
|  |  | 
|  | test_url_loader_factory_.SetInterceptor(base::BindLambdaForTesting( | 
|  | [&](const network::ResourceRequest& request) { interceptor_count_++; })); | 
|  | } | 
|  |  | 
|  | void CrxDownloaderTest::TearDown() { | 
|  | crx_downloader_.reset(); | 
|  | } | 
|  |  | 
|  | void CrxDownloaderTest::Quit() { | 
|  | if (!quit_closure_.is_null()) | 
|  | std::move(quit_closure_).Run(); | 
|  | } | 
|  |  | 
|  | void CrxDownloaderTest::DownloadComplete(int crx_context, | 
|  | const CrxDownloader::Result& result) { | 
|  | ++num_download_complete_calls_; | 
|  | crx_context_ = crx_context; | 
|  | download_complete_result_ = result; | 
|  | Quit(); | 
|  | } | 
|  |  | 
|  | void CrxDownloaderTest::DownloadProgress(int crx_context) { | 
|  | ++num_progress_calls_; | 
|  | } | 
|  |  | 
|  | void CrxDownloaderTest::AddResponse(const GURL& url, | 
|  | const base::FilePath& file_path, | 
|  | int net_error) { | 
|  | if (net_error == net::OK) { | 
|  | std::string data; | 
|  | EXPECT_TRUE(base::ReadFileToString(file_path, &data)); | 
|  | auto head = network::mojom::URLResponseHead::New(); | 
|  | head->content_length = data.size(); | 
|  | network::URLLoaderCompletionStatus status(net_error); | 
|  | status.decoded_body_length = data.size(); | 
|  | test_url_loader_factory_.AddResponse(url, std::move(head), data, status); | 
|  | return; | 
|  | } | 
|  |  | 
|  | EXPECT_NE(net_error, net::OK); | 
|  | test_url_loader_factory_.AddResponse( | 
|  | url, network::mojom::URLResponseHead::New(), std::string(), | 
|  | network::URLLoaderCompletionStatus(net_error)); | 
|  | } | 
|  |  | 
|  | void CrxDownloaderTest::RunThreads() { | 
|  | base::RunLoop runloop; | 
|  | quit_closure_ = runloop.QuitClosure(); | 
|  | runloop.Run(); | 
|  |  | 
|  | // Since some tests need to drain currently enqueued tasks such as network | 
|  | // intercepts on the IO thread, run the threads until they are | 
|  | // idle. The component updater service won't loop again until the loop count | 
|  | // is set and the service is started. | 
|  | RunThreadsUntilIdle(); | 
|  | } | 
|  |  | 
|  | void CrxDownloaderTest::RunThreadsUntilIdle() { | 
|  | task_environment_.RunUntilIdle(); | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | } | 
|  |  | 
|  | // Tests that starting a download without a url results in an error. | 
|  | TEST_F(CrxDownloaderTest, NoUrl) { | 
|  | std::vector<GURL> urls; | 
|  | crx_downloader_->StartDownload(urls, std::string("abcd"), | 
|  | std::move(callback_)); | 
|  | RunThreadsUntilIdle(); | 
|  |  | 
|  | EXPECT_EQ(1, num_download_complete_calls_); | 
|  | EXPECT_EQ(kExpectedContext, crx_context_); | 
|  | EXPECT_EQ(static_cast<int>(CrxDownloaderError::NO_URL), | 
|  | download_complete_result_.error); | 
|  | EXPECT_TRUE(download_complete_result_.response.empty()); | 
|  | EXPECT_EQ(0, num_progress_calls_); | 
|  | } | 
|  |  | 
|  | // Tests that starting a download without providing a hash results in an error. | 
|  | TEST_F(CrxDownloaderTest, NoHash) { | 
|  | std::vector<GURL> urls(1, GURL("http://somehost/somefile")); | 
|  |  | 
|  | crx_downloader_->StartDownload(urls, std::string(), std::move(callback_)); | 
|  | RunThreadsUntilIdle(); | 
|  |  | 
|  | EXPECT_EQ(1, num_download_complete_calls_); | 
|  | EXPECT_EQ(kExpectedContext, crx_context_); | 
|  | EXPECT_EQ(static_cast<int>(CrxDownloaderError::NO_HASH), | 
|  | download_complete_result_.error); | 
|  | EXPECT_TRUE(download_complete_result_.response.empty()); | 
|  | EXPECT_EQ(0, num_progress_calls_); | 
|  | } | 
|  |  | 
|  | // Tests that downloading from one url is successful. | 
|  | TEST_F(CrxDownloaderTest, OneUrl) { | 
|  | const GURL expected_crx_url = | 
|  | GURL("http://localhost/download/jebgalgnebhfojomionfpkfelancnnkf.crx"); | 
|  |  | 
|  | const base::FilePath test_file(MakeTestFilePath(kTestFileName)); | 
|  | AddResponse(expected_crx_url, test_file, net::OK); | 
|  |  | 
|  | crx_downloader_->StartDownloadFromUrl( | 
|  | expected_crx_url, std::string(hash_jebg), std::move(callback_)); | 
|  | RunThreads(); | 
|  |  | 
|  | EXPECT_EQ(1, GetInterceptorCount()); | 
|  |  | 
|  | EXPECT_EQ(1, num_download_complete_calls_); | 
|  | EXPECT_EQ(kExpectedContext, crx_context_); | 
|  | EXPECT_EQ(0, download_complete_result_.error); | 
|  | EXPECT_TRUE(ContentsEqual(download_complete_result_.response, test_file)); | 
|  |  | 
|  | EXPECT_TRUE( | 
|  | DeleteFileAndEmptyParentDirectory(download_complete_result_.response)); | 
|  |  | 
|  | EXPECT_LE(1, num_progress_calls_); | 
|  | } | 
|  |  | 
|  | // Tests that downloading from one url fails if the actual hash of the file | 
|  | // does not match the expected hash. | 
|  | TEST_F(CrxDownloaderTest, OneUrlBadHash) { | 
|  | const GURL expected_crx_url = | 
|  | GURL("http://localhost/download/jebgalgnebhfojomionfpkfelancnnkf.crx"); | 
|  |  | 
|  | const base::FilePath test_file(MakeTestFilePath(kTestFileName)); | 
|  | AddResponse(expected_crx_url, test_file, net::OK); | 
|  |  | 
|  | crx_downloader_->StartDownloadFromUrl( | 
|  | expected_crx_url, | 
|  | std::string( | 
|  | "813c59747e139a608b3b5fc49633affc6db574373f309f156ea6d27229c0b3f9"), | 
|  | std::move(callback_)); | 
|  | RunThreads(); | 
|  |  | 
|  | EXPECT_EQ(1, GetInterceptorCount()); | 
|  |  | 
|  | EXPECT_EQ(1, num_download_complete_calls_); | 
|  | EXPECT_EQ(kExpectedContext, crx_context_); | 
|  | EXPECT_EQ(static_cast<int>(CrxDownloaderError::BAD_HASH), | 
|  | download_complete_result_.error); | 
|  | EXPECT_TRUE(download_complete_result_.response.empty()); | 
|  |  | 
|  | EXPECT_LE(1, num_progress_calls_); | 
|  | } | 
|  |  | 
|  | // Tests that specifying two urls has no side effects. Expect a successful | 
|  | // download, and only one download request be made. | 
|  | TEST_F(CrxDownloaderTest, TwoUrls) { | 
|  | const GURL expected_crx_url = | 
|  | GURL("http://localhost/download/jebgalgnebhfojomionfpkfelancnnkf.crx"); | 
|  |  | 
|  | const base::FilePath test_file(MakeTestFilePath(kTestFileName)); | 
|  | AddResponse(expected_crx_url, test_file, net::OK); | 
|  |  | 
|  | std::vector<GURL> urls; | 
|  | urls.push_back(expected_crx_url); | 
|  | urls.push_back(expected_crx_url); | 
|  |  | 
|  | crx_downloader_->StartDownload(urls, std::string(hash_jebg), | 
|  | std::move(callback_)); | 
|  | RunThreads(); | 
|  |  | 
|  | EXPECT_EQ(1, GetInterceptorCount()); | 
|  |  | 
|  | EXPECT_EQ(1, num_download_complete_calls_); | 
|  | EXPECT_EQ(kExpectedContext, crx_context_); | 
|  | EXPECT_EQ(0, download_complete_result_.error); | 
|  | EXPECT_TRUE(ContentsEqual(download_complete_result_.response, test_file)); | 
|  |  | 
|  | EXPECT_TRUE( | 
|  | DeleteFileAndEmptyParentDirectory(download_complete_result_.response)); | 
|  |  | 
|  | EXPECT_LE(1, num_progress_calls_); | 
|  | } | 
|  |  | 
|  | // Tests that the fallback to a valid url is successful. | 
|  | TEST_F(CrxDownloaderTest, TwoUrls_FirstInvalid) { | 
|  | const GURL expected_crx_url = | 
|  | GURL("http://localhost/download/jebgalgnebhfojomionfpkfelancnnkf.crx"); | 
|  | const GURL no_file_url = | 
|  | GURL("http://localhost/download/ihfokbkgjpifnbbojhneepfflplebdkc.crx"); | 
|  |  | 
|  | const base::FilePath test_file(MakeTestFilePath(kTestFileName)); | 
|  | AddResponse(expected_crx_url, test_file, net::OK); | 
|  | AddResponse(no_file_url, base::FilePath(), net::ERR_FILE_NOT_FOUND); | 
|  |  | 
|  | std::vector<GURL> urls; | 
|  | urls.push_back(no_file_url); | 
|  | urls.push_back(expected_crx_url); | 
|  |  | 
|  | crx_downloader_->StartDownload(urls, std::string(hash_jebg), | 
|  | std::move(callback_)); | 
|  | RunThreads(); | 
|  |  | 
|  | EXPECT_EQ(2, GetInterceptorCount()); | 
|  |  | 
|  | EXPECT_EQ(1, num_download_complete_calls_); | 
|  | EXPECT_EQ(kExpectedContext, crx_context_); | 
|  | EXPECT_EQ(0, download_complete_result_.error); | 
|  | EXPECT_TRUE(ContentsEqual(download_complete_result_.response, test_file)); | 
|  |  | 
|  | EXPECT_TRUE( | 
|  | DeleteFileAndEmptyParentDirectory(download_complete_result_.response)); | 
|  |  | 
|  | // Expect at least some progress reported by the loader. | 
|  | EXPECT_LE(1, num_progress_calls_); | 
|  |  | 
|  | const auto download_metrics = crx_downloader_->download_metrics(); | 
|  | ASSERT_EQ(2u, download_metrics.size()); | 
|  | EXPECT_EQ(no_file_url, download_metrics[0].url); | 
|  | EXPECT_EQ(net::ERR_FILE_NOT_FOUND, download_metrics[0].error); | 
|  | EXPECT_EQ(-1, download_metrics[0].downloaded_bytes); | 
|  | EXPECT_EQ(-1, download_metrics[0].total_bytes); | 
|  | EXPECT_EQ(expected_crx_url, download_metrics[1].url); | 
|  | EXPECT_EQ(0, download_metrics[1].error); | 
|  | EXPECT_EQ(1015, download_metrics[1].downloaded_bytes); | 
|  | EXPECT_EQ(1015, download_metrics[1].total_bytes); | 
|  | } | 
|  |  | 
|  | // Tests that the download succeeds if the first url is correct and the | 
|  | // second bad url does not have a side-effect. | 
|  | TEST_F(CrxDownloaderTest, TwoUrls_SecondInvalid) { | 
|  | const GURL expected_crx_url = | 
|  | GURL("http://localhost/download/jebgalgnebhfojomionfpkfelancnnkf.crx"); | 
|  | const GURL no_file_url = | 
|  | GURL("http://localhost/download/ihfokbkgjpifnbbojhneepfflplebdkc.crx"); | 
|  |  | 
|  | const base::FilePath test_file(MakeTestFilePath(kTestFileName)); | 
|  | AddResponse(expected_crx_url, test_file, net::OK); | 
|  | AddResponse(no_file_url, base::FilePath(), net::ERR_FILE_NOT_FOUND); | 
|  |  | 
|  | std::vector<GURL> urls; | 
|  | urls.push_back(expected_crx_url); | 
|  | urls.push_back(no_file_url); | 
|  |  | 
|  | crx_downloader_->StartDownload(urls, std::string(hash_jebg), | 
|  | std::move(callback_)); | 
|  | RunThreads(); | 
|  |  | 
|  | EXPECT_EQ(1, GetInterceptorCount()); | 
|  |  | 
|  | EXPECT_EQ(1, num_download_complete_calls_); | 
|  | EXPECT_EQ(kExpectedContext, crx_context_); | 
|  | EXPECT_EQ(0, download_complete_result_.error); | 
|  | EXPECT_TRUE(ContentsEqual(download_complete_result_.response, test_file)); | 
|  |  | 
|  | EXPECT_TRUE( | 
|  | DeleteFileAndEmptyParentDirectory(download_complete_result_.response)); | 
|  |  | 
|  | EXPECT_LE(1, num_progress_calls_); | 
|  |  | 
|  | EXPECT_EQ(1u, crx_downloader_->download_metrics().size()); | 
|  | } | 
|  |  | 
|  | // Tests that the download fails if both urls don't serve content. | 
|  | TEST_F(CrxDownloaderTest, TwoUrls_BothInvalid) { | 
|  | const GURL expected_crx_url = | 
|  | GURL("http://localhost/download/jebgalgnebhfojomionfpkfelancnnkf.crx"); | 
|  |  | 
|  | AddResponse(expected_crx_url, base::FilePath(), net::ERR_FILE_NOT_FOUND); | 
|  |  | 
|  | std::vector<GURL> urls; | 
|  | urls.push_back(expected_crx_url); | 
|  | urls.push_back(expected_crx_url); | 
|  |  | 
|  | crx_downloader_->StartDownload(urls, std::string(hash_jebg), | 
|  | std::move(callback_)); | 
|  | RunThreads(); | 
|  |  | 
|  | EXPECT_EQ(2, GetInterceptorCount()); | 
|  |  | 
|  | EXPECT_EQ(1, num_download_complete_calls_); | 
|  | EXPECT_EQ(kExpectedContext, crx_context_); | 
|  | EXPECT_NE(0, download_complete_result_.error); | 
|  | EXPECT_TRUE(download_complete_result_.response.empty()); | 
|  |  | 
|  | const auto download_metrics = crx_downloader_->download_metrics(); | 
|  | ASSERT_EQ(2u, download_metrics.size()); | 
|  | EXPECT_EQ(expected_crx_url, download_metrics[0].url); | 
|  | EXPECT_EQ(net::ERR_FILE_NOT_FOUND, download_metrics[0].error); | 
|  | EXPECT_EQ(-1, download_metrics[0].downloaded_bytes); | 
|  | EXPECT_EQ(-1, download_metrics[0].total_bytes); | 
|  | EXPECT_EQ(expected_crx_url, download_metrics[1].url); | 
|  | EXPECT_EQ(net::ERR_FILE_NOT_FOUND, download_metrics[1].error); | 
|  | EXPECT_EQ(-1, download_metrics[1].downloaded_bytes); | 
|  | EXPECT_EQ(-1, download_metrics[1].total_bytes); | 
|  | } | 
|  |  | 
|  | }  // namespace update_client |