| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/path_service.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/time/default_clock.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/offline_pages/offline_page_model_factory.h" |
| #include "chrome/browser/offline_pages/offline_page_tab_helper.h" |
| #include "chrome/browser/offline_pages/offline_page_url_loader.h" |
| #include "chrome/browser/profiles/profile_key.h" |
| #include "chrome/browser/renderer_host/chrome_navigation_ui_data.h" |
| #include "chrome/common/chrome_constants.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/test/base/testing_browser_process.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "chrome/test/base/testing_profile_manager.h" |
| #include "components/offline_pages/core/archive_validator.h" |
| #include "components/offline_pages/core/client_namespace_constants.h" |
| #include "components/offline_pages/core/model/offline_page_model_taskified.h" |
| #include "components/offline_pages/core/offline_page_feature.h" |
| #include "components/offline_pages/core/offline_page_metadata_store.h" |
| #include "components/offline_pages/core/offline_page_test_archive_publisher.h" |
| #include "components/offline_pages/core/offline_page_test_archiver.h" |
| #include "components/offline_pages/core/request_header/offline_page_navigation_ui_data.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "mojo/public/cpp/system/wait.h" |
| #include "net/base/filename_util.h" |
| #include "net/base/network_change_notifier.h" |
| #include "net/http/http_request_headers.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| |
| #if BUILDFLAG(IS_ANDROID) |
| #include "components/gcm_driver/instance_id/instance_id_android.h" |
| #include "components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h" |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| namespace offline_pages { |
| |
| namespace { |
| |
| constexpr char kPrivateOfflineFileDir[] = "offline_pages"; |
| constexpr char kPublicOfflineFileDir[] = "public_offline_pages"; |
| |
| const base::FilePath kFilename1(FILE_PATH_LITERAL("hello.mhtml")); |
| const base::FilePath kFilename2(FILE_PATH_LITERAL("welcome.mhtml")); |
| const base::FilePath kNonexistentFilename( |
| FILE_PATH_LITERAL("nonexistent.mhtml")); |
| constexpr int kFileSize1 = 471; // Real size of hello.mhtml. |
| constexpr int kFileSize2 = 461; // Real size of welcome.mhtml. |
| const std::string kDigest1( |
| "\x43\x60\x62\x02\x06\x15\x0f\x3e\x77\x99\x3d\xed\xdc\xd4\xe2\x0d\xbe\xbd" |
| "\x77\x1a\xfb\x32\x00\x51\x7e\x63\x7d\x3b\x2e\x46\x63\xf6", |
| 32); // SHA256 Hash of hello.mhtml. |
| const std::string kDigest2( |
| "\xBD\xD3\x37\x79\xDA\x7F\x4E\x6A\x16\x66\xED\x49\x67\x18\x54\x48\xC6\x8E" |
| "\xA1\x47\x16\xA5\x44\x45\x43\xD0\x0E\x04\x9F\x4C\x45\xDC", |
| 32); // SHA256 Hash of welcome.mhtml. |
| const std::string kMismatchedDigest( |
| "\xff\x64\xF9\x7C\x94\xE5\x9E\x91\x83\x3D\x41\xB0\x36\x90\x0A\xDF\xB3\xB1" |
| "\x5C\x13\xBE\xB8\x35\x8C\xF6\x5B\xC4\xB5\x5A\xFC\x3A\xCC", |
| 32); // Wrong SHA256 Hash. |
| |
| constexpr int kTabId = 1; |
| |
| constexpr int64_t kDownloadId = 42LL; |
| |
| constexpr char kTestUrl[] = "http://test.org/page"; |
| constexpr char kTestUrl2[] = "http://test.org/another"; |
| |
| struct ResponseInfo { |
| explicit ResponseInfo(int request_status) : request_status(request_status) { |
| DCHECK_NE(net::OK, request_status); |
| } |
| ResponseInfo(int request_status, |
| const std::string& mime_type, |
| const std::string& data_received) |
| : request_status(request_status), |
| mime_type(mime_type), |
| data_received(data_received) {} |
| |
| int request_status; |
| std::string mime_type; |
| std::string data_received; |
| }; |
| |
| bool GetTabId(int tab_id_value, |
| content::WebContents* web_content, |
| int* tab_id) { |
| *tab_id = tab_id_value; |
| return true; |
| } |
| |
| class TestNetworkChangeNotifier : public net::NetworkChangeNotifier { |
| public: |
| TestNetworkChangeNotifier() : online_(true) {} |
| |
| TestNetworkChangeNotifier(const TestNetworkChangeNotifier&) = delete; |
| TestNetworkChangeNotifier& operator=(const TestNetworkChangeNotifier&) = |
| delete; |
| |
| ~TestNetworkChangeNotifier() override {} |
| |
| net::NetworkChangeNotifier::ConnectionType GetCurrentConnectionType() |
| const override { |
| return online_ ? net::NetworkChangeNotifier::CONNECTION_UNKNOWN |
| : net::NetworkChangeNotifier::CONNECTION_NONE; |
| } |
| |
| bool online() const { return online_; } |
| void set_online(bool online) { online_ = online; } |
| |
| private: |
| bool online_; |
| }; |
| |
| class TestURLLoaderClient : public network::mojom::URLLoaderClient { |
| public: |
| class Observer { |
| public: |
| virtual void OnReceiveRedirect(const GURL& redirected_url) = 0; |
| virtual void OnReceiveResponse( |
| network::mojom::URLResponseHeadPtr response_head) = 0; |
| virtual void OnComplete() = 0; |
| |
| protected: |
| virtual ~Observer() {} |
| }; |
| |
| explicit TestURLLoaderClient(Observer* observer) : observer_(observer) {} |
| |
| TestURLLoaderClient(const TestURLLoaderClient&) = delete; |
| TestURLLoaderClient& operator=(const TestURLLoaderClient&) = delete; |
| |
| ~TestURLLoaderClient() override {} |
| |
| void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override { |
| } |
| |
| void OnReceiveResponse( |
| network::mojom::URLResponseHeadPtr response_head, |
| mojo::ScopedDataPipeConsumerHandle body, |
| std::optional<mojo_base::BigBuffer> cached_metadata) override { |
| response_body_ = std::move(body); |
| observer_->OnReceiveResponse(std::move(response_head)); |
| } |
| |
| void OnReceiveRedirect( |
| const net::RedirectInfo& redirect_info, |
| network::mojom::URLResponseHeadPtr response_head) override { |
| observer_->OnReceiveRedirect(redirect_info.new_url); |
| } |
| |
| void OnTransferSizeUpdated(int32_t transfer_size_diff) override {} |
| |
| void OnUploadProgress(int64_t current_position, |
| int64_t total_size, |
| OnUploadProgressCallback ack_callback) override {} |
| |
| void OnComplete(const network::URLLoaderCompletionStatus& status) override { |
| completion_status_ = status; |
| observer_->OnComplete(); |
| } |
| |
| mojo::PendingRemote<network::mojom::URLLoaderClient> CreateRemote() { |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client_remote = |
| receiver_.BindNewPipeAndPassRemote(); |
| receiver_.set_disconnect_handler(base::BindOnce( |
| &TestURLLoaderClient::OnMojoDisconnect, base::Unretained(this))); |
| return client_remote; |
| } |
| |
| mojo::DataPipeConsumerHandle response_body() { return response_body_.get(); } |
| |
| const network::URLLoaderCompletionStatus& completion_status() const { |
| return completion_status_; |
| } |
| |
| private: |
| void OnMojoDisconnect() {} |
| |
| raw_ptr<Observer> observer_ = nullptr; |
| mojo::Receiver<network::mojom::URLLoaderClient> receiver_{this}; |
| mojo::ScopedDataPipeConsumerHandle response_body_; |
| network::URLLoaderCompletionStatus completion_status_; |
| }; |
| |
| // Helper function to make a character array filled with |size| bytes of |
| // test content. |
| std::string MakeContentOfSize(int size) { |
| EXPECT_GE(size, 0); |
| std::string result; |
| result.reserve(size); |
| for (int i = 0; i < size; i++) { |
| result.append(1, static_cast<char>(i % 256)); |
| } |
| return result; |
| } |
| |
| static network::ResourceRequest CreateResourceRequest( |
| const GURL& url, |
| const std::string& method, |
| const net::HttpRequestHeaders& extra_headers, |
| bool is_outermost_main_frame) { |
| network::ResourceRequest request; |
| request.method = method; |
| request.headers = extra_headers; |
| request.url = url; |
| request.is_outermost_main_frame = is_outermost_main_frame; |
| return request; |
| } |
| |
| } // namespace |
| |
| class OfflinePageRequestHandlerTest; |
| |
| // Builds an OfflinePageURLLoader to test the request interception with network |
| // service enabled. |
| class OfflinePageURLLoaderBuilder : public TestURLLoaderClient::Observer { |
| public: |
| explicit OfflinePageURLLoaderBuilder(OfflinePageRequestHandlerTest* test); |
| |
| void OnReceiveRedirect(const GURL& redirected_url) override; |
| void OnReceiveResponse( |
| network::mojom::URLResponseHeadPtr response_head) override; |
| void OnComplete() override; |
| |
| void InterceptRequest(const GURL& url, |
| const std::string& method, |
| const net::HttpRequestHeaders& extra_headers, |
| bool is_outermost_main_frame); |
| |
| OfflinePageRequestHandlerTest* test() { return test_; } |
| |
| void Quit() { std::move(quit_closure_).Run(); } |
| |
| private: |
| void OnHandleReady(MojoResult result, const mojo::HandleSignalsState& state); |
| void InterceptRequestInternal(const GURL& url, |
| const std::string& method, |
| const net::HttpRequestHeaders& extra_headers, |
| bool is_outermost_main_frame); |
| void MaybeStartLoader( |
| const network::ResourceRequest& request, |
| content::URLLoaderRequestInterceptor::RequestHandler request_handler); |
| void ReadBody(); |
| void ReadCompleted(const ResponseInfo& response); |
| |
| raw_ptr<OfflinePageRequestHandlerTest> test_; |
| std::unique_ptr<ChromeNavigationUIData> navigation_ui_data_; |
| std::unique_ptr<OfflinePageURLLoader> url_loader_; |
| std::unique_ptr<TestURLLoaderClient> client_; |
| std::unique_ptr<mojo::SimpleWatcher> handle_watcher_; |
| mojo::Remote<network::mojom::URLLoader> loader_; |
| std::string mime_type_; |
| std::string body_; |
| base::OnceClosure quit_closure_; |
| }; |
| |
| class OfflinePageRequestHandlerTest : public testing::Test { |
| public: |
| OfflinePageRequestHandlerTest(); |
| |
| OfflinePageRequestHandlerTest(const OfflinePageRequestHandlerTest&) = delete; |
| OfflinePageRequestHandlerTest& operator=( |
| const OfflinePageRequestHandlerTest&) = delete; |
| |
| ~OfflinePageRequestHandlerTest() override {} |
| |
| void SetUp() override; |
| void TearDown() override; |
| |
| void InterceptRequest(const GURL& url, |
| const std::string& method, |
| const net::HttpRequestHeaders& extra_headers, |
| bool is_outermost_main_frame); |
| void SimulateHasNetworkConnectivity(bool has_connectivity); |
| void RunUntilIdle(); |
| void WaitForAsyncOperation(); |
| |
| base::FilePath CreateFileWithContent(const std::string& content); |
| |
| // Returns an offline id of the saved page. |
| // |file_path| in SavePublicPage and SaveInternalPage can be either absolute |
| // or relative. If relative, |file_path| will be appended to public/internal |
| // archive directory used for the testing. |
| // |file_path| in SavePage should be absolute. |
| int64_t SavePublicPage(const GURL& url, |
| const GURL& original_url, |
| const base::FilePath& file_path, |
| int64_t file_size, |
| const std::string& digest); |
| int64_t SaveInternalPage(const GURL& url, |
| const GURL& original_url, |
| const base::FilePath& file_path, |
| int64_t file_size, |
| const std::string& digest); |
| int64_t SavePage(const GURL& url, |
| const GURL& original_url, |
| const base::FilePath& file_path, |
| int64_t file_size, |
| const std::string& digest); |
| |
| OfflinePageItem GetPage(int64_t offline_id); |
| |
| void LoadPage(const GURL& url); |
| void LoadPageWithHeaders(const GURL& url, |
| const net::HttpRequestHeaders& extra_headers); |
| |
| void ReadCompleted(const ResponseInfo& reponse, |
| bool is_offline_page_set_in_navigation_data); |
| |
| void ExpectNoOfflinePageServed(int64_t offline_id); |
| void ExpectOfflinePageServed(int64_t expected_offline_id, |
| int expected_file_size); |
| |
| // Use the offline header with specific reason and offline_id. Return the |
| // full header string. |
| std::string UseOfflinePageHeader(OfflinePageHeader::Reason reason, |
| int64_t offline_id); |
| std::string UseOfflinePageHeaderForIntent(OfflinePageHeader::Reason reason, |
| int64_t offline_id, |
| const GURL& intent_url); |
| |
| Profile* profile() { return profile_; } |
| content::WebContents* web_contents() const { return web_contents_.get(); } |
| OfflinePageTabHelper* offline_page_tab_helper() const { |
| return offline_page_tab_helper_; |
| } |
| int request_status() const { return response_.request_status; } |
| int bytes_read() const { return response_.data_received.length(); } |
| const std::string& data_received() const { return response_.data_received; } |
| const std::string& mime_type() const { return response_.mime_type; } |
| bool is_offline_page_set_in_navigation_data() const { |
| return is_offline_page_set_in_navigation_data_; |
| } |
| |
| bool is_connected_with_good_network() { |
| return network_change_notifier_->online() && |
| // Exclude flaky network. |
| offline_page_header_.reason != OfflinePageHeader::Reason::NET_ERROR; |
| } |
| |
| private: |
| static std::unique_ptr<KeyedService> BuildTestOfflinePageModel( |
| SimpleFactoryKey* key); |
| |
| // TODO(crbug.com/40561648): The static members below will be removed |
| // once the reference to BuildTestOfflinePageModel in SetUp is converted to a |
| // base::OnceCallback. |
| static base::FilePath private_archives_dir_; |
| static base::FilePath public_archives_dir_; |
| |
| void OnSavePageDone(SavePageResult result, int64_t offline_id); |
| void OnGetPageByOfflineIdDone(const OfflinePageItem* pages); |
| |
| // Runs on IO thread. |
| void CreateFileWithContentOnIO(const std::string& content, |
| base::OnceClosure callback); |
| |
| content::BrowserTaskEnvironment task_environment_; |
| TestingProfileManager profile_manager_; |
| raw_ptr<TestingProfile> profile_; |
| std::unique_ptr<content::WebContents> web_contents_; |
| std::unique_ptr<base::HistogramTester> histogram_tester_; |
| raw_ptr<OfflinePageTabHelper> offline_page_tab_helper_; // Not owned. |
| int64_t last_offline_id_; |
| ResponseInfo response_; |
| bool is_offline_page_set_in_navigation_data_; |
| OfflinePageItem page_; |
| OfflinePageHeader offline_page_header_; |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // OfflinePageTabHelper instantiates PrefetchService which in turn requests a |
| // fresh GCM token automatically. This causes the request to be done |
| // synchronously instead of with a posted task. |
| instance_id::InstanceIDAndroid::ScopedBlockOnAsyncTasksForTesting |
| block_async_; |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| // These are not thread-safe. But they can be used in the pattern that |
| // setting the state is done first from one thread and reading this state |
| // can be from any other thread later. |
| std::unique_ptr<TestNetworkChangeNotifier> network_change_notifier_; |
| |
| // These should only be accessed purely from IO thread. |
| base::ScopedTempDir private_archives_temp_base_dir_; |
| base::ScopedTempDir public_archives_temp_base_dir_; |
| base::ScopedTempDir temp_dir_; |
| base::FilePath temp_file_path_; |
| int file_name_sequence_num_ = 0; |
| |
| bool async_operation_completed_ = false; |
| base::OnceClosure async_operation_completed_callback_; |
| OfflinePageURLLoaderBuilder interceptor_factory_; |
| }; |
| |
| OfflinePageRequestHandlerTest::OfflinePageRequestHandlerTest() |
| : task_environment_(content::BrowserTaskEnvironment::REAL_IO_THREAD), |
| profile_manager_(TestingBrowserProcess::GetGlobal()), |
| last_offline_id_(0), |
| response_(net::ERR_IO_PENDING), |
| is_offline_page_set_in_navigation_data_(false), |
| network_change_notifier_(new TestNetworkChangeNotifier), |
| interceptor_factory_(this) {} |
| |
| void OfflinePageRequestHandlerTest::SetUp() { |
| // Create a test profile. |
| ASSERT_TRUE(profile_manager_.SetUp()); |
| profile_ = profile_manager_.CreateTestingProfile("Profile 1"); |
| |
| // Create a test web contents. |
| |
| web_contents_ = content::WebContents::Create( |
| content::WebContents::CreateParams(profile_)); |
| OfflinePageTabHelper::CreateForWebContents(web_contents_.get()); |
| offline_page_tab_helper_ = |
| OfflinePageTabHelper::FromWebContents(web_contents_.get()); |
| |
| // Set up the factory for testing. |
| // Note: The extra dir into the temp folder is needed so that the helper |
| // dir-copy operation works properly. That operation copies the source dir |
| // final path segment into the destination, and not only its immediate |
| // contents so this same-named path here makes the archive dir variable point |
| // to the correct location. |
| // TODO(romax): add the more recent "temporary" dir here instead of reusing |
| // the private one. |
| ASSERT_TRUE(private_archives_temp_base_dir_.CreateUniqueTempDir()); |
| private_archives_dir_ = private_archives_temp_base_dir_.GetPath().AppendASCII( |
| kPrivateOfflineFileDir); |
| ASSERT_TRUE(public_archives_temp_base_dir_.CreateUniqueTempDir()); |
| public_archives_dir_ = public_archives_temp_base_dir_.GetPath().AppendASCII( |
| kPublicOfflineFileDir); |
| OfflinePageModelFactory::GetInstance()->SetTestingFactoryAndUse( |
| profile()->GetProfileKey(), |
| base::BindRepeating( |
| &OfflinePageRequestHandlerTest::BuildTestOfflinePageModel)); |
| |
| // Initialize OfflinePageModel. |
| OfflinePageModelTaskified* model = static_cast<OfflinePageModelTaskified*>( |
| OfflinePageModelFactory::GetForBrowserContext(profile())); |
| |
| // Skip the logic to clear the original URL if it is same as final URL. |
| // This is needed in order to test that offline page request handler can |
| // omit the redirect under this circumstance, for compatibility with the |
| // metadata already written to the store. |
| model->SetSkipClearingOriginalUrlForTesting(); |
| |
| // Avoid running the model's maintenance tasks. |
| model->DoNotRunMaintenanceTasksForTesting(); |
| |
| // Move test data files into their respective temporary test directories. The |
| // model's maintenance tasks must not be executed in the meantime otherwise |
| // these files will be wiped by consistency checks. |
| base::FilePath test_data_dir_path; |
| base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir_path); |
| base::FilePath test_data_private_archives_dir = |
| test_data_dir_path.AppendASCII(kPrivateOfflineFileDir); |
| ASSERT_TRUE(base::CopyDirectory(test_data_private_archives_dir, |
| private_archives_dir_.DirName(), true)); |
| base::FilePath test_data_public_archives_dir = |
| test_data_dir_path.AppendASCII(kPublicOfflineFileDir); |
| ASSERT_TRUE(base::CopyDirectory(test_data_public_archives_dir, |
| public_archives_dir_.DirName(), true)); |
| |
| histogram_tester_ = std::make_unique<base::HistogramTester>(); |
| } |
| |
| void OfflinePageRequestHandlerTest::TearDown() { |
| EXPECT_TRUE(private_archives_temp_base_dir_.Delete()); |
| EXPECT_TRUE(public_archives_temp_base_dir_.Delete()); |
| } |
| |
| void OfflinePageRequestHandlerTest::InterceptRequest( |
| const GURL& url, |
| const std::string& method, |
| const net::HttpRequestHeaders& extra_headers, |
| bool is_outermost_main_frame) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| interceptor_factory_.InterceptRequest(url, method, extra_headers, |
| is_outermost_main_frame); |
| } |
| |
| void OfflinePageRequestHandlerTest::SimulateHasNetworkConnectivity( |
| bool online) { |
| network_change_notifier_->set_online(online); |
| } |
| |
| void OfflinePageRequestHandlerTest::RunUntilIdle() { |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| void OfflinePageRequestHandlerTest::WaitForAsyncOperation() { |
| // No need to wait if async operation is not needed. |
| if (async_operation_completed_) { |
| return; |
| } |
| base::RunLoop run_loop; |
| async_operation_completed_callback_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| |
| void OfflinePageRequestHandlerTest::CreateFileWithContentOnIO( |
| const std::string& content, |
| base::OnceClosure callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| |
| if (!temp_dir_.IsValid()) { |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| } |
| std::string file_name("test"); |
| file_name += base::NumberToString(file_name_sequence_num_++); |
| file_name += ".mht"; |
| temp_file_path_ = temp_dir_.GetPath().AppendASCII(file_name); |
| ASSERT_TRUE(base::WriteFile(temp_file_path_, content)); |
| std::move(callback).Run(); |
| } |
| |
| base::FilePath OfflinePageRequestHandlerTest::CreateFileWithContent( |
| const std::string& content) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| base::RunLoop run_loop; |
| content::GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, |
| base::BindOnce(&OfflinePageRequestHandlerTest::CreateFileWithContentOnIO, |
| base::Unretained(this), content, run_loop.QuitClosure())); |
| run_loop.Run(); |
| return temp_file_path_; |
| } |
| |
| void OfflinePageRequestHandlerTest::ExpectNoOfflinePageServed( |
| int64_t offline_id) { |
| EXPECT_NE("multipart/related", mime_type()); |
| EXPECT_EQ(0, bytes_read()); |
| EXPECT_FALSE(is_offline_page_set_in_navigation_data()); |
| EXPECT_FALSE(offline_page_tab_helper()->GetOfflinePageForTest()); |
| } |
| |
| void OfflinePageRequestHandlerTest::ExpectOfflinePageServed( |
| int64_t expected_offline_id, |
| int expected_file_size) { |
| EXPECT_EQ(net::OK, request_status()); |
| EXPECT_EQ("multipart/related", mime_type()); |
| EXPECT_EQ(expected_file_size, bytes_read()); |
| EXPECT_TRUE(is_offline_page_set_in_navigation_data()); |
| ASSERT_TRUE(offline_page_tab_helper()->GetOfflinePageForTest()); |
| EXPECT_EQ(expected_offline_id, |
| offline_page_tab_helper()->GetOfflinePageForTest()->offline_id); |
| OfflinePageTrustedState expected_trusted_state = |
| private_archives_dir_.IsParent( |
| offline_page_tab_helper()->GetOfflinePageForTest()->file_path) |
| ? OfflinePageTrustedState::TRUSTED_AS_IN_INTERNAL_DIR |
| : OfflinePageTrustedState::TRUSTED_AS_UNMODIFIED_AND_IN_PUBLIC_DIR; |
| EXPECT_EQ(expected_trusted_state, |
| offline_page_tab_helper()->GetTrustedStateForTest()); |
| } |
| |
| std::string OfflinePageRequestHandlerTest::UseOfflinePageHeader( |
| OfflinePageHeader::Reason reason, |
| int64_t offline_id) { |
| DCHECK_NE(OfflinePageHeader::Reason::NONE, reason); |
| offline_page_header_.reason = reason; |
| if (offline_id) { |
| offline_page_header_.id = base::NumberToString(offline_id); |
| } |
| return offline_page_header_.GetCompleteHeaderString(); |
| } |
| |
| std::string OfflinePageRequestHandlerTest::UseOfflinePageHeaderForIntent( |
| OfflinePageHeader::Reason reason, |
| int64_t offline_id, |
| const GURL& intent_url) { |
| DCHECK_NE(OfflinePageHeader::Reason::NONE, reason); |
| DCHECK(offline_id); |
| offline_page_header_.reason = reason; |
| offline_page_header_.id = base::NumberToString(offline_id); |
| offline_page_header_.intent_url = intent_url; |
| return offline_page_header_.GetCompleteHeaderString(); |
| } |
| |
| int64_t OfflinePageRequestHandlerTest::SavePublicPage( |
| const GURL& url, |
| const GURL& original_url, |
| const base::FilePath& file_path, |
| int64_t file_size, |
| const std::string& digest) { |
| base::FilePath final_path; |
| if (file_path.IsAbsolute()) { |
| final_path = file_path; |
| } else { |
| final_path = public_archives_dir_.Append(file_path); |
| } |
| |
| return SavePage(url, original_url, final_path, file_size, digest); |
| } |
| |
| int64_t OfflinePageRequestHandlerTest::SaveInternalPage( |
| const GURL& url, |
| const GURL& original_url, |
| const base::FilePath& file_path, |
| int64_t file_size, |
| const std::string& digest) { |
| base::FilePath final_path; |
| if (file_path.IsAbsolute()) { |
| final_path = file_path; |
| } else { |
| final_path = private_archives_dir_.Append(file_path); |
| } |
| |
| return SavePage(url, original_url, final_path, file_size, digest); |
| } |
| |
| int64_t OfflinePageRequestHandlerTest::SavePage(const GURL& url, |
| const GURL& original_url, |
| const base::FilePath& file_path, |
| int64_t file_size, |
| const std::string& digest) { |
| DCHECK(file_path.IsAbsolute()); |
| |
| static int item_counter = 0; |
| ++item_counter; |
| |
| auto archiver = std::make_unique<OfflinePageTestArchiver>( |
| nullptr, url, OfflinePageArchiver::ArchiverResult::SUCCESSFULLY_CREATED, |
| std::u16string(), file_size, digest, |
| base::SingleThreadTaskRunner::GetCurrentDefault()); |
| archiver->set_filename(file_path); |
| |
| async_operation_completed_ = false; |
| OfflinePageModel::SavePageParams save_page_params; |
| save_page_params.url = url; |
| save_page_params.client_id = |
| ClientId(kDownloadNamespace, base::NumberToString(item_counter)); |
| save_page_params.original_url = original_url; |
| OfflinePageModelFactory::GetForBrowserContext(profile())->SavePage( |
| save_page_params, std::move(archiver), nullptr, |
| base::BindOnce(&OfflinePageRequestHandlerTest::OnSavePageDone, |
| base::Unretained(this))); |
| WaitForAsyncOperation(); |
| return last_offline_id_; |
| } |
| |
| // static |
| std::unique_ptr<KeyedService> |
| OfflinePageRequestHandlerTest::BuildTestOfflinePageModel( |
| SimpleFactoryKey* key) { |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner = |
| base::SingleThreadTaskRunner::GetCurrentDefault(); |
| |
| base::FilePath store_path = |
| key->GetPath().Append(chrome::kOfflinePageMetadataDirname); |
| std::unique_ptr<OfflinePageMetadataStore> metadata_store( |
| new OfflinePageMetadataStore(task_runner, store_path)); |
| |
| // Since we're not saving page into temporary dir, it's set the same as the |
| // private dir. |
| auto archive_manager = std::make_unique<ArchiveManager>( |
| private_archives_dir_, private_archives_dir_, public_archives_dir_, |
| task_runner); |
| |
| auto archive_publisher = std::make_unique<OfflinePageTestArchivePublisher>( |
| archive_manager.get(), kDownloadId); |
| // TODO(iwells): Figure out how to make use_verbatim_archive_path go away. |
| archive_publisher->use_verbatim_archive_path(true); |
| |
| return std::unique_ptr<KeyedService>(new OfflinePageModelTaskified( |
| std::move(metadata_store), std::move(archive_manager), |
| std::move(archive_publisher), task_runner)); |
| } |
| |
| // static |
| base::FilePath OfflinePageRequestHandlerTest::private_archives_dir_; |
| base::FilePath OfflinePageRequestHandlerTest::public_archives_dir_; |
| |
| void OfflinePageRequestHandlerTest::OnSavePageDone(SavePageResult result, |
| int64_t offline_id) { |
| ASSERT_EQ(SavePageResult::SUCCESS, result); |
| last_offline_id_ = offline_id; |
| |
| async_operation_completed_ = true; |
| if (!async_operation_completed_callback_.is_null()) { |
| std::move(async_operation_completed_callback_).Run(); |
| } |
| } |
| |
| OfflinePageItem OfflinePageRequestHandlerTest::GetPage(int64_t offline_id) { |
| OfflinePageModelFactory::GetForBrowserContext(profile())->GetPageByOfflineId( |
| offline_id, |
| base::BindOnce(&OfflinePageRequestHandlerTest::OnGetPageByOfflineIdDone, |
| base::Unretained(this))); |
| RunUntilIdle(); |
| return page_; |
| } |
| |
| void OfflinePageRequestHandlerTest::OnGetPageByOfflineIdDone( |
| const OfflinePageItem* page) { |
| ASSERT_TRUE(page); |
| page_ = *page; |
| } |
| |
| void OfflinePageRequestHandlerTest::LoadPage(const GURL& url) { |
| InterceptRequest(url, "GET", net::HttpRequestHeaders(), |
| true /* is_outermost_main_frame */); |
| } |
| |
| void OfflinePageRequestHandlerTest::LoadPageWithHeaders( |
| const GURL& url, |
| const net::HttpRequestHeaders& extra_headers) { |
| InterceptRequest(url, "GET", extra_headers, |
| true /* is_outermost_main_frame */); |
| } |
| |
| void OfflinePageRequestHandlerTest::ReadCompleted( |
| const ResponseInfo& response, |
| bool is_offline_page_set_in_navigation_data) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| response_ = response; |
| is_offline_page_set_in_navigation_data_ = |
| is_offline_page_set_in_navigation_data; |
| |
| interceptor_factory_.Quit(); |
| } |
| |
| OfflinePageURLLoaderBuilder::OfflinePageURLLoaderBuilder( |
| OfflinePageRequestHandlerTest* test) |
| : test_(test) { |
| navigation_ui_data_ = std::make_unique<ChromeNavigationUIData>(); |
| } |
| |
| void OfflinePageURLLoaderBuilder::OnReceiveRedirect( |
| const GURL& redirected_url) { |
| InterceptRequestInternal(redirected_url, "GET", net::HttpRequestHeaders(), |
| true); |
| } |
| |
| void OfflinePageURLLoaderBuilder::OnReceiveResponse( |
| network::mojom::URLResponseHeadPtr response_head) { |
| mime_type_ = response_head->mime_type; |
| ReadBody(); |
| } |
| |
| void OfflinePageURLLoaderBuilder::OnComplete() { |
| if (client_->completion_status().error_code != net::OK) { |
| mime_type_.clear(); |
| body_.clear(); |
| } |
| ReadCompleted( |
| ResponseInfo(client_->completion_status().error_code, mime_type_, body_)); |
| // Clear intermediate data in preparation for next potential page loading. |
| mime_type_.clear(); |
| body_.clear(); |
| } |
| |
| void OfflinePageURLLoaderBuilder::InterceptRequestInternal( |
| const GURL& url, |
| const std::string& method, |
| const net::HttpRequestHeaders& extra_headers, |
| bool is_outermost_main_frame) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| client_ = std::make_unique<TestURLLoaderClient>(this); |
| |
| network::ResourceRequest request = CreateResourceRequest( |
| url, method, extra_headers, is_outermost_main_frame); |
| |
| url_loader_ = OfflinePageURLLoader::Create( |
| navigation_ui_data_.get(), |
| test_->web_contents()->GetPrimaryMainFrame()->GetFrameTreeNodeId(), |
| request, |
| base::BindOnce(&OfflinePageURLLoaderBuilder::MaybeStartLoader, |
| base::Unretained(this), request)); |
| |
| // |url_loader_| may not be created. |
| if (!url_loader_) { |
| return; |
| } |
| |
| url_loader_->SetTabIdGetterForTesting(base::BindRepeating(&GetTabId, kTabId)); |
| } |
| |
| void OfflinePageURLLoaderBuilder::InterceptRequest( |
| const GURL& url, |
| const std::string& method, |
| const net::HttpRequestHeaders& extra_headers, |
| bool is_outermost_main_frame) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| base::RunLoop loop; |
| quit_closure_ = loop.QuitWhenIdleClosure(); |
| InterceptRequestInternal(url, method, extra_headers, is_outermost_main_frame); |
| loop.Run(); |
| } |
| |
| void OfflinePageURLLoaderBuilder::MaybeStartLoader( |
| const network::ResourceRequest& request, |
| content::URLLoaderRequestInterceptor::RequestHandler request_handler) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (!request_handler) { |
| ReadCompleted(ResponseInfo(net::ERR_FAILED)); |
| return; |
| } |
| |
| // OfflinePageURLLoader decides to handle the request as offline page. Since |
| // now, OfflinePageURLLoader will own itself and live as long as its URLLoader |
| // and URLLoaderClient are alive. |
| url_loader_.release(); |
| |
| loader_.reset(); |
| std::move(request_handler) |
| .Run(request, loader_.BindNewPipeAndPassReceiver(), |
| client_->CreateRemote()); |
| } |
| |
| void OfflinePageURLLoaderBuilder::ReadBody() { |
| while (true) { |
| MojoHandle consumer = client_->response_body().value(); |
| |
| const void* buffer; |
| uint32_t num_bytes; |
| MojoResult rv = MojoBeginReadData(consumer, nullptr, &buffer, &num_bytes); |
| if (rv == MOJO_RESULT_SHOULD_WAIT) { |
| handle_watcher_ = std::make_unique<mojo::SimpleWatcher>( |
| FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::AUTOMATIC, |
| base::SequencedTaskRunner::GetCurrentDefault()); |
| handle_watcher_->Watch( |
| client_->response_body(), |
| MOJO_HANDLE_SIGNAL_READABLE | MOJO_HANDLE_SIGNAL_PEER_CLOSED, |
| MOJO_WATCH_CONDITION_SATISFIED, |
| base::BindRepeating(&OfflinePageURLLoaderBuilder::OnHandleReady, |
| base::Unretained(this))); |
| return; |
| } |
| |
| // The pipe was closed. |
| if (rv == MOJO_RESULT_FAILED_PRECONDITION) { |
| ReadCompleted(ResponseInfo(net::ERR_FAILED)); |
| return; |
| } |
| |
| CHECK_EQ(rv, MOJO_RESULT_OK); |
| |
| body_.append(static_cast<const char*>(buffer), num_bytes); |
| MojoEndReadData(consumer, num_bytes, nullptr); |
| } |
| } |
| |
| void OfflinePageURLLoaderBuilder::OnHandleReady( |
| MojoResult result, |
| const mojo::HandleSignalsState& state) { |
| if (result != MOJO_RESULT_OK) { |
| ReadCompleted(ResponseInfo(net::ERR_FAILED)); |
| return; |
| } |
| ReadBody(); |
| } |
| |
| void OfflinePageURLLoaderBuilder::ReadCompleted(const ResponseInfo& response) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| handle_watcher_.reset(); |
| client_.reset(); |
| url_loader_.reset(); |
| loader_.reset(); |
| |
| bool is_offline_page_set_in_navigation_data = false; |
| offline_pages::OfflinePageNavigationUIData* offline_page_data = |
| navigation_ui_data_->GetOfflinePageNavigationUIData(); |
| if (offline_page_data && offline_page_data->is_offline_page()) { |
| is_offline_page_set_in_navigation_data = true; |
| } |
| |
| test()->ReadCompleted(response, is_offline_page_set_in_navigation_data); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, FailedToCreateRequestJob) { |
| SimulateHasNetworkConnectivity(false); |
| |
| // Must be http/https URL. |
| InterceptRequest(GURL("ftp://host/doc"), "GET", net::HttpRequestHeaders(), |
| true /* is_outermost_main_frame */); |
| EXPECT_EQ(0, bytes_read()); |
| EXPECT_FALSE(offline_page_tab_helper()->GetOfflinePageForTest()); |
| |
| InterceptRequest(GURL("file:///path/doc"), "GET", net::HttpRequestHeaders(), |
| true /* is_outermost_main_frame */); |
| EXPECT_EQ(0, bytes_read()); |
| EXPECT_FALSE(offline_page_tab_helper()->GetOfflinePageForTest()); |
| |
| // Must be GET method. |
| InterceptRequest(GURL(kTestUrl), "POST", net::HttpRequestHeaders(), |
| true /* is_outermost_main_frame */); |
| EXPECT_EQ(0, bytes_read()); |
| EXPECT_FALSE(offline_page_tab_helper()->GetOfflinePageForTest()); |
| |
| InterceptRequest(GURL(kTestUrl), "HEAD", net::HttpRequestHeaders(), |
| true /* is_outermost_main_frame */); |
| EXPECT_EQ(0, bytes_read()); |
| EXPECT_FALSE(offline_page_tab_helper()->GetOfflinePageForTest()); |
| |
| // Must be main resource. |
| InterceptRequest(GURL(kTestUrl), "POST", net::HttpRequestHeaders(), |
| false /* is_outermost_main_frame */); |
| EXPECT_EQ(0, bytes_read()); |
| EXPECT_FALSE(offline_page_tab_helper()->GetOfflinePageForTest()); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, LoadOfflinePageOnDisconnectedNetwork) { |
| SimulateHasNetworkConnectivity(false); |
| |
| const GURL test_url(kTestUrl); |
| int64_t offline_id = |
| SaveInternalPage(test_url, GURL(), kFilename1, kFileSize1, std::string()); |
| |
| LoadPage(test_url); |
| |
| ExpectOfflinePageServed(offline_id, kFileSize1); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, |
| DoNotLoadOfflinePageOnDisconnectedNetworkWhenNetworkStateLikelyUnknown) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| offline_pages::kOfflinePagesNetworkStateLikelyUnknown); |
| |
| this->SimulateHasNetworkConnectivity(false); |
| |
| const GURL test_url(kTestUrl); |
| int64_t offline_id = this->SaveInternalPage(test_url, GURL(), kFilename1, |
| kFileSize1, std::string()); |
| |
| this->LoadPage(test_url); |
| |
| // When the network is good, we will fall back to the default handling |
| // immediately. So no request result should be reported. Passing |
| // AGGREGATED_REQUEST_RESULT_MAX to skip checking request result in |
| // the helper function. |
| this->ExpectNoOfflinePageServed(offline_id); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, PageNotFoundOnDisconnectedNetwork) { |
| SimulateHasNetworkConnectivity(false); |
| |
| int64_t offline_id = SaveInternalPage(GURL(kTestUrl), GURL(), kFilename1, |
| kFileSize1, std::string()); |
| |
| LoadPage(GURL(kTestUrl2)); |
| |
| ExpectNoOfflinePageServed(offline_id); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, |
| NetErrorPageSuggestionOnDisconnectedNetwork) { |
| SimulateHasNetworkConnectivity(false); |
| |
| const GURL test_url(kTestUrl); |
| int64_t offline_id = |
| SaveInternalPage(test_url, GURL(), kFilename1, kFileSize1, std::string()); |
| |
| net::HttpRequestHeaders extra_headers; |
| extra_headers.AddHeaderFromString( |
| UseOfflinePageHeader(OfflinePageHeader::Reason::NET_ERROR_SUGGESTION, 0)); |
| LoadPageWithHeaders(test_url, extra_headers); |
| |
| ExpectOfflinePageServed(offline_id, kFileSize1); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, LoadOfflinePageOnFlakyNetwork) { |
| SimulateHasNetworkConnectivity(true); |
| |
| const GURL test_url(kTestUrl); |
| int64_t offline_id = |
| SaveInternalPage(test_url, GURL(), kFilename1, kFileSize1, std::string()); |
| |
| // When custom offline header exists and contains "reason=error", it means |
| // that net error is hit in last request due to flaky network. |
| net::HttpRequestHeaders extra_headers; |
| extra_headers.AddHeaderFromString( |
| UseOfflinePageHeader(OfflinePageHeader::Reason::NET_ERROR, 0)); |
| LoadPageWithHeaders(test_url, extra_headers); |
| |
| ExpectOfflinePageServed(offline_id, kFileSize1); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, PageNotFoundOnFlakyNetwork) { |
| SimulateHasNetworkConnectivity(true); |
| |
| int64_t offline_id = SaveInternalPage(GURL(kTestUrl), GURL(), kFilename1, |
| kFileSize1, std::string()); |
| |
| // When custom offline header exists and contains "reason=error", it means |
| // that net error is hit in last request due to flaky network. |
| net::HttpRequestHeaders extra_headers; |
| extra_headers.AddHeaderFromString( |
| UseOfflinePageHeader(OfflinePageHeader::Reason::NET_ERROR, 0)); |
| LoadPageWithHeaders(GURL(kTestUrl2), extra_headers); |
| |
| ExpectNoOfflinePageServed(offline_id); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, ForceLoadOfflinePageOnConnectedNetwork) { |
| SimulateHasNetworkConnectivity(true); |
| |
| const GURL test_url(kTestUrl); |
| int64_t offline_id = |
| SaveInternalPage(test_url, GURL(), kFilename1, kFileSize1, std::string()); |
| |
| // When custom offline header exists and contains value other than |
| // "reason=error", it means that offline page is forced to load. |
| net::HttpRequestHeaders extra_headers; |
| extra_headers.AddHeaderFromString( |
| UseOfflinePageHeader(OfflinePageHeader::Reason::DOWNLOAD, 0)); |
| LoadPageWithHeaders(test_url, extra_headers); |
| |
| ExpectOfflinePageServed(offline_id, kFileSize1); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, PageNotFoundOnConnectedNetwork) { |
| SimulateHasNetworkConnectivity(true); |
| |
| // Save an offline page. |
| int64_t offline_id = SaveInternalPage(GURL(kTestUrl), GURL(), kFilename1, |
| kFileSize1, std::string()); |
| |
| // When custom offline header exists and contains value other than |
| // "reason=error", it means that offline page is forced to load. |
| net::HttpRequestHeaders extra_headers; |
| extra_headers.AddHeaderFromString( |
| UseOfflinePageHeader(OfflinePageHeader::Reason::DOWNLOAD, 0)); |
| LoadPageWithHeaders(GURL(kTestUrl2), extra_headers); |
| |
| ExpectNoOfflinePageServed(offline_id); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, DoNotLoadOfflinePageOnConnectedNetwork) { |
| SimulateHasNetworkConnectivity(true); |
| |
| const GURL test_url(kTestUrl); |
| int64_t offline_id = |
| SaveInternalPage(test_url, GURL(), kFilename1, kFileSize1, std::string()); |
| |
| LoadPage(test_url); |
| |
| // When the network is good, we will fall back to the default handling |
| // immediately. So no request result should be reported. Passing |
| // AGGREGATED_REQUEST_RESULT_MAX to skip checking request result in |
| // the helper function. |
| ExpectNoOfflinePageServed(offline_id); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, LoadMostRecentlyCreatedOfflinePage) { |
| SimulateHasNetworkConnectivity(false); |
| |
| // Save 2 offline pages associated with same online URL, but pointing to |
| // different archive file. |
| const GURL test_url(kTestUrl); |
| int64_t offline_id2 = |
| SaveInternalPage(test_url, GURL(), kFilename2, kFileSize2, std::string()); |
| |
| // Load an URL that matches multiple offline pages. Expect that the most |
| // recently created offline page is fetched. |
| LoadPage(test_url); |
| |
| ExpectOfflinePageServed(offline_id2, kFileSize2); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, LoadOfflinePageByOfflineID) { |
| SimulateHasNetworkConnectivity(true); |
| |
| // Save 2 offline pages associated with same online URL, but pointing to |
| // different archive file. |
| const GURL test_url(kTestUrl); |
| int64_t offline_id1 = |
| SaveInternalPage(test_url, GURL(), kFilename1, kFileSize1, std::string()); |
| |
| // Load an URL with a specific offline ID designated in the custom header. |
| // Expect the offline page matching the offline id is fetched. |
| net::HttpRequestHeaders extra_headers; |
| extra_headers.AddHeaderFromString( |
| UseOfflinePageHeader(OfflinePageHeader::Reason::DOWNLOAD, offline_id1)); |
| LoadPageWithHeaders(test_url, extra_headers); |
| |
| ExpectOfflinePageServed(offline_id1, kFileSize1); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, FailToLoadByOfflineIDOnUrlMismatch) { |
| SimulateHasNetworkConnectivity(true); |
| |
| int64_t offline_id = SaveInternalPage(GURL(kTestUrl), GURL(), kFilename1, |
| kFileSize1, std::string()); |
| |
| // The offline page found with specific offline ID does not match the passed |
| // online URL. Should fall back to find the offline page based on the online |
| // URL. |
| net::HttpRequestHeaders extra_headers; |
| extra_headers.AddHeaderFromString( |
| UseOfflinePageHeader(OfflinePageHeader::Reason::DOWNLOAD, offline_id)); |
| LoadPageWithHeaders(GURL(kTestUrl2), extra_headers); |
| |
| ExpectNoOfflinePageServed(offline_id); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, LoadOfflinePageForUrlWithFragment) { |
| SimulateHasNetworkConnectivity(false); |
| |
| // Save an offline page associated with online URL without fragment. |
| const GURL test_url(kTestUrl); |
| int64_t offline_id1 = |
| SaveInternalPage(test_url, GURL(), kFilename1, kFileSize1, std::string()); |
| |
| // Save another offline page associated with online URL that has a fragment. |
| const GURL test_url2(kTestUrl2); |
| GURL url2_with_fragment(test_url2.spec() + "#ref"); |
| int64_t offline_id2 = SaveInternalPage(url2_with_fragment, GURL(), kFilename2, |
| kFileSize2, std::string()); |
| |
| // Loads an url with fragment, that will match the offline URL without the |
| // fragment. |
| GURL url_with_fragment(test_url.spec() + "#ref"); |
| LoadPage(url_with_fragment); |
| |
| ExpectOfflinePageServed(offline_id1, kFileSize1); |
| |
| // Loads an url without fragment, that will match the offline URL with the |
| // fragment. |
| LoadPage(test_url2); |
| |
| EXPECT_EQ(kFileSize2, bytes_read()); |
| ASSERT_TRUE(offline_page_tab_helper()->GetOfflinePageForTest()); |
| EXPECT_EQ(offline_id2, |
| offline_page_tab_helper()->GetOfflinePageForTest()->offline_id); |
| |
| // Loads an url with fragment, that will match the offline URL with different |
| // fragment. |
| GURL url2_with_different_fragment(test_url2.spec() + "#different_ref"); |
| LoadPage(url2_with_different_fragment); |
| |
| EXPECT_EQ(kFileSize2, bytes_read()); |
| ASSERT_TRUE(offline_page_tab_helper()->GetOfflinePageForTest()); |
| EXPECT_EQ(offline_id2, |
| offline_page_tab_helper()->GetOfflinePageForTest()->offline_id); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, LoadOtherPageOnDigestMismatch) { |
| SimulateHasNetworkConnectivity(false); |
| |
| // Save 2 offline pages associated with same online URL, one in internal |
| // location, while another in public location with mismatched digest. |
| const GURL test_url(kTestUrl); |
| int64_t offline_id1 = |
| SaveInternalPage(test_url, GURL(), kFilename1, kFileSize1, std::string()); |
| |
| // There are 2 offline pages matching |test_url|. The most recently created |
| // one should fail on mistmatched digest. The second most recently created |
| // offline page should work. |
| LoadPage(test_url); |
| |
| ExpectOfflinePageServed(offline_id1, kFileSize1); |
| } |
| |
| // Disabled due to https://crbug.com/917113. |
| TEST_F(OfflinePageRequestHandlerTest, DISABLED_EmptyFile) { |
| SimulateHasNetworkConnectivity(false); |
| |
| const std::string expected_data(""); |
| base::FilePath temp_file_path = CreateFileWithContent(expected_data); |
| ArchiveValidator archive_validator; |
| const std::string expected_digest = archive_validator.Finish(); |
| |
| const GURL test_url(kTestUrl); |
| int64_t offline_id = |
| SavePublicPage(test_url, GURL(), temp_file_path, 0, expected_digest); |
| |
| LoadPage(test_url); |
| |
| ExpectOfflinePageServed(offline_id, 0); |
| EXPECT_EQ(expected_data, data_received()); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, TinyFile) { |
| SimulateHasNetworkConnectivity(false); |
| |
| std::string expected_data("hello world"); |
| base::FilePath temp_file_path = CreateFileWithContent(expected_data); |
| ArchiveValidator archive_validator; |
| archive_validator.Update(expected_data.c_str(), expected_data.length()); |
| std::string expected_digest = archive_validator.Finish(); |
| int expected_size = expected_data.length(); |
| |
| const GURL test_url(kTestUrl); |
| int64_t offline_id = SavePublicPage(test_url, GURL(), temp_file_path, |
| expected_size, expected_digest); |
| |
| LoadPage(test_url); |
| |
| ExpectOfflinePageServed(offline_id, expected_size); |
| EXPECT_EQ(expected_data, data_received()); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, SmallFile) { |
| SimulateHasNetworkConnectivity(false); |
| |
| std::string expected_data(MakeContentOfSize(2 * 1024)); |
| base::FilePath temp_file_path = CreateFileWithContent(expected_data); |
| ArchiveValidator archive_validator; |
| archive_validator.Update(expected_data.c_str(), expected_data.length()); |
| std::string expected_digest = archive_validator.Finish(); |
| int expected_size = expected_data.length(); |
| |
| const GURL test_url(kTestUrl); |
| int64_t offline_id = SavePublicPage(test_url, GURL(), temp_file_path, |
| expected_size, expected_digest); |
| |
| LoadPage(test_url); |
| |
| ExpectOfflinePageServed(offline_id, expected_size); |
| EXPECT_EQ(expected_data, data_received()); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, BigFile) { |
| SimulateHasNetworkConnectivity(false); |
| |
| std::string expected_data(MakeContentOfSize(3 * 1024 * 1024)); |
| base::FilePath temp_file_path = CreateFileWithContent(expected_data); |
| ArchiveValidator archive_validator; |
| archive_validator.Update(expected_data.c_str(), expected_data.length()); |
| std::string expected_digest = archive_validator.Finish(); |
| int expected_size = expected_data.length(); |
| |
| const GURL test_url(kTestUrl); |
| int64_t offline_id = SavePublicPage(test_url, GURL(), temp_file_path, |
| expected_size, expected_digest); |
| |
| LoadPage(test_url); |
| |
| ExpectOfflinePageServed(offline_id, expected_size); |
| EXPECT_EQ(expected_data, data_received()); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, LoadFromFileUrlIntent) { |
| SimulateHasNetworkConnectivity(true); |
| |
| std::string expected_data(MakeContentOfSize(2 * 1024)); |
| ArchiveValidator archive_validator; |
| archive_validator.Update(expected_data.c_str(), expected_data.length()); |
| std::string expected_digest = archive_validator.Finish(); |
| int expected_size = expected_data.length(); |
| |
| // Create a file with unmodified data. The path to this file will be feed |
| // into "intent_url" of extra headers. |
| base::FilePath unmodified_file_path = CreateFileWithContent(expected_data); |
| |
| // Create a file with modified data. An offline page is created to associate |
| // with this file, but with size and digest matching the unmodified version. |
| std::string modified_data(expected_data); |
| modified_data[10] = '@'; |
| base::FilePath modified_file_path = CreateFileWithContent(modified_data); |
| |
| const GURL test_url(kTestUrl); |
| int64_t offline_id = SavePublicPage(test_url, GURL(), modified_file_path, |
| expected_size, expected_digest); |
| |
| // Load an URL with custom header that contains "intent_url" pointing to |
| // unmodified file. Expect the file from the intent URL is fetched. |
| net::HttpRequestHeaders extra_headers; |
| extra_headers.AddHeaderFromString(UseOfflinePageHeaderForIntent( |
| OfflinePageHeader::Reason::FILE_URL_INTENT, offline_id, |
| net::FilePathToFileURL(unmodified_file_path))); |
| LoadPageWithHeaders(test_url, extra_headers); |
| |
| ExpectOfflinePageServed(offline_id, expected_size); |
| EXPECT_EQ(expected_data, data_received()); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, IntentFileNotFound) { |
| SimulateHasNetworkConnectivity(true); |
| |
| std::string expected_data(MakeContentOfSize(2 * 1024)); |
| ArchiveValidator archive_validator; |
| archive_validator.Update(expected_data.c_str(), expected_data.length()); |
| std::string expected_digest = archive_validator.Finish(); |
| int expected_size = expected_data.length(); |
| |
| // Create a file with unmodified data. An offline page is created to associate |
| // with this file. |
| base::FilePath unmodified_file_path = CreateFileWithContent(expected_data); |
| |
| // Get a path pointing to non-existing file. This path will be feed into |
| // "intent_url" of extra headers. |
| base::FilePath nonexistent_file_path = |
| unmodified_file_path.DirName().AppendASCII("nonexistent"); |
| |
| const GURL test_url(kTestUrl); |
| int64_t offline_id = SavePublicPage(test_url, GURL(), unmodified_file_path, |
| expected_size, expected_digest); |
| |
| // Load an URL with custom header that contains "intent_url" pointing to |
| // non-existent file. Expect the request fails. |
| net::HttpRequestHeaders extra_headers; |
| extra_headers.AddHeaderFromString(UseOfflinePageHeaderForIntent( |
| OfflinePageHeader::Reason::FILE_URL_INTENT, offline_id, |
| net::FilePathToFileURL(nonexistent_file_path))); |
| LoadPageWithHeaders(test_url, extra_headers); |
| |
| EXPECT_EQ(net::ERR_FAILED, request_status()); |
| EXPECT_NE("multipart/related", mime_type()); |
| EXPECT_EQ(0, bytes_read()); |
| EXPECT_FALSE(is_offline_page_set_in_navigation_data()); |
| EXPECT_FALSE(offline_page_tab_helper()->GetOfflinePageForTest()); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, IntentFileModifiedInTheMiddle) { |
| SimulateHasNetworkConnectivity(true); |
| |
| std::string expected_data(MakeContentOfSize(2 * 1024)); |
| ArchiveValidator archive_validator; |
| archive_validator.Update(expected_data.c_str(), expected_data.length()); |
| std::string expected_digest = archive_validator.Finish(); |
| int expected_size = expected_data.length(); |
| |
| // Create a file with modified data in the middle. An offline page is created |
| // to associate with this modified file, but with size and digest matching the |
| // unmodified version. |
| std::string modified_data(expected_data); |
| modified_data[10] = '@'; |
| base::FilePath modified_file_path = CreateFileWithContent(modified_data); |
| |
| const GURL test_url(kTestUrl); |
| int64_t offline_id = SavePublicPage(test_url, GURL(), modified_file_path, |
| expected_size, expected_digest); |
| |
| // Load an URL with custom header that contains "intent_url" pointing to |
| // modified file. Expect the request fails. |
| net::HttpRequestHeaders extra_headers; |
| extra_headers.AddHeaderFromString(UseOfflinePageHeaderForIntent( |
| OfflinePageHeader::Reason::FILE_URL_INTENT, offline_id, |
| net::FilePathToFileURL(modified_file_path))); |
| LoadPageWithHeaders(test_url, extra_headers); |
| |
| EXPECT_EQ(net::ERR_FAILED, request_status()); |
| EXPECT_NE("multipart/related", mime_type()); |
| EXPECT_EQ(0, bytes_read()); |
| // Note that the offline bit is not cleared on purpose due to the fact that |
| // other flag, like request status, should already indicate that the offline |
| // page fails to load. |
| EXPECT_TRUE(is_offline_page_set_in_navigation_data()); |
| EXPECT_FALSE(offline_page_tab_helper()->GetOfflinePageForTest()); |
| } |
| |
| TEST_F(OfflinePageRequestHandlerTest, IntentFileModifiedWithMoreDataAppended) { |
| SimulateHasNetworkConnectivity(true); |
| |
| std::string expected_data(MakeContentOfSize(2 * 1024)); |
| ArchiveValidator archive_validator; |
| archive_validator.Update(expected_data.c_str(), expected_data.length()); |
| std::string expected_digest = archive_validator.Finish(); |
| int expected_size = expected_data.length(); |
| |
| // Create a file with more data appended. An offline page is created to |
| // associate with this modified file, but with size and digest matching the |
| // unmodified version. |
| std::string modified_data(expected_data); |
| modified_data += "foo"; |
| base::FilePath modified_file_path = CreateFileWithContent(modified_data); |
| |
| const GURL test_url(kTestUrl); |
| int64_t offline_id = SavePublicPage(test_url, GURL(), modified_file_path, |
| expected_size, expected_digest); |
| |
| // Load an URL with custom header that contains "intent_url" pointing to |
| // modified file. Expect the request fails. |
| net::HttpRequestHeaders extra_headers; |
| extra_headers.AddHeaderFromString(UseOfflinePageHeaderForIntent( |
| OfflinePageHeader::Reason::FILE_URL_INTENT, offline_id, |
| net::FilePathToFileURL(modified_file_path))); |
| LoadPageWithHeaders(test_url, extra_headers); |
| |
| EXPECT_EQ(net::ERR_FAILED, request_status()); |
| EXPECT_NE("multipart/related", mime_type()); |
| EXPECT_EQ(0, bytes_read()); |
| // Note that the offline bit is not cleared on purpose due to the fact that |
| // other flag, like request status, should already indicate that the offline |
| // page fails to load. |
| EXPECT_TRUE(is_offline_page_set_in_navigation_data()); |
| EXPECT_FALSE(offline_page_tab_helper()->GetOfflinePageForTest()); |
| } |
| |
| } // namespace offline_pages |