chromium/chrome/browser/offline_pages/offline_page_request_handler_unittest.cc

// 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