// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stdint.h>

#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_restrictions.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/common/frame_messages.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/mhtml_generation_params.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/base/filename_util.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/WebKit/public/web/WebFindOptions.h"
#include "third_party/WebKit/public/web/WebFrameSerializerCacheControlPolicy.h"

using testing::ContainsRegex;
using testing::HasSubstr;
using testing::Not;

namespace content {

namespace {

// A dummy WebContentsDelegate which tracks the results of a find operation.
class FindTrackingDelegate : public WebContentsDelegate {
 public:
  FindTrackingDelegate(const std::string& search)
      : search_(search), matches_(-1) {}

  // Returns number of results.
  int Wait(WebContents* web_contents) {
    WebContentsDelegate* old_delegate = web_contents->GetDelegate();
    web_contents->SetDelegate(this);

    blink::WebFindOptions options;
    options.matchCase = false;

    web_contents->Find(global_request_id++, base::UTF8ToUTF16(search_),
                       options);
    run_loop_.Run();

    web_contents->SetDelegate(old_delegate);

    return matches_;
  }

  void FindReply(WebContents* web_contents,
                 int request_id,
                 int number_of_matches,
                 const gfx::Rect& selection_rect,
                 int active_match_ordinal,
                 bool final_update) override {
    if (final_update) {
      matches_ = number_of_matches;
      run_loop_.Quit();
    }
  }

  static int global_request_id;

 private:
  std::string search_;
  int matches_;
  base::RunLoop run_loop_;

  DISALLOW_COPY_AND_ASSIGN(FindTrackingDelegate);
};

// static
int FindTrackingDelegate::global_request_id = 0;

}  // namespace

class MHTMLGenerationTest : public ContentBrowserTest {
 public:
  MHTMLGenerationTest() : has_mhtml_callback_run_(false), file_size_(0) {}

 protected:
  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    ASSERT_TRUE(embedded_test_server()->Start());
    ContentBrowserTest::SetUp();
  }

  void GenerateMHTML(const base::FilePath& path, const GURL& url) {
    GenerateMHTML(MHTMLGenerationParams(path), url);
  }

  void GenerateMHTML(const MHTMLGenerationParams& params, const GURL& url) {
    NavigateToURL(shell(), url);
    GenerateMHTMLForCurrentPage(params);
  }

  void GenerateMHTMLForCurrentPage(const MHTMLGenerationParams& params) {
    base::RunLoop run_loop;

    shell()->web_contents()->GenerateMHTML(
        params, base::Bind(&MHTMLGenerationTest::MHTMLGenerated,
                           base::Unretained(this),
                           run_loop.QuitClosure()));

    // Block until the MHTML is generated.
    run_loop.Run();

    EXPECT_TRUE(has_mhtml_callback_run());
  }

  int64_t ReadFileSizeFromDisk(base::FilePath path) {
    base::ThreadRestrictions::ScopedAllowIO allow_io_to_test_file_size;
    int64_t file_size;
    if (!base::GetFileSize(path, &file_size)) return -1;
    return file_size;
  }

  void TestOriginalVsSavedPage(
      const GURL& url,
      const MHTMLGenerationParams params,
      int expected_number_of_frames,
      const std::vector<std::string>& expected_substrings,
      const std::vector<std::string>& forbidden_substrings_in_saved_page,
      bool skip_verification_of_original_page = false) {
    // Navigate to the test page and verify if test expectations
    // are met (this is mostly a sanity check - a failure to meet
    // expectations would probably mean that there is a test bug
    // (i.e. that we got called with wrong expected_foo argument).
    NavigateToURL(shell(), url);
    if (!skip_verification_of_original_page) {
      AssertExpectationsAboutCurrentTab(expected_number_of_frames,
                                        expected_substrings,
                                        std::vector<std::string>());
    }

    GenerateMHTML(params, url);
    ASSERT_FALSE(HasFailure());

    // Stop the test server (to make sure the locally saved page
    // is self-contained / won't try to open original resources).
    ASSERT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());

    // Open the saved page and verify if test expectations are
    // met (i.e. if the same expectations are met for "after"
    // [saved version of the page] as for the "before"
    // [the original version of the page].
    NavigateToURL(shell(), net::FilePathToFileURL(params.file_path));
    AssertExpectationsAboutCurrentTab(expected_number_of_frames,
                                      expected_substrings,
                                      forbidden_substrings_in_saved_page);
  }

  void AssertExpectationsAboutCurrentTab(
      int expected_number_of_frames,
      const std::vector<std::string>& expected_substrings,
      const std::vector<std::string>& forbidden_substrings) {
    int actual_number_of_frames =
        shell()->web_contents()->GetAllFrames().size();
    EXPECT_EQ(expected_number_of_frames, actual_number_of_frames);

    for (const auto& expected_substring : expected_substrings) {
      FindTrackingDelegate delegate(expected_substring);
      int actual_number_of_matches = delegate.Wait(shell()->web_contents());
      EXPECT_EQ(1, actual_number_of_matches)
          << "Verifying that \"" << expected_substring << "\" appears "
          << "exactly once in the text of web contents of "
          << shell()->web_contents()->GetURL().spec();
    }

    for (const auto& forbidden_substring : forbidden_substrings) {
      FindTrackingDelegate delegate(forbidden_substring);
      int actual_number_of_matches = delegate.Wait(shell()->web_contents());
      EXPECT_EQ(0, actual_number_of_matches)
          << "Verifying that \"" << forbidden_substring << "\" doesn't "
          << "appear in the text of web contents of "
          << shell()->web_contents()->GetURL().spec();
    }
  }

  bool has_mhtml_callback_run() const { return has_mhtml_callback_run_; }
  int64_t file_size() const { return file_size_; }

  base::ScopedTempDir temp_dir_;

 private:
  void MHTMLGenerated(base::Closure quit_closure, int64_t size) {
    has_mhtml_callback_run_ = true;
    file_size_ = size;
    quit_closure.Run();
  }

  bool has_mhtml_callback_run_;
  int64_t file_size_;
};

// Tests that generating a MHTML does create contents.
// Note that the actual content of the file is not tested, the purpose of this
// test is to ensure we were successful in creating the MHTML data from the
// renderer.
IN_PROC_BROWSER_TEST_F(MHTMLGenerationTest, GenerateMHTML) {
  base::FilePath path(temp_dir_.GetPath());
  path = path.Append(FILE_PATH_LITERAL("test.mht"));

  GenerateMHTML(path, embedded_test_server()->GetURL("/simple_page.html"));
  ASSERT_FALSE(HasFailure());

  // Make sure the actual generated file has some contents.
  EXPECT_GT(file_size(), 0);  // Verify the size reported by the callback.
  EXPECT_GT(ReadFileSizeFromDisk(path), 100);  // Verify the actual file size.

  {
    base::ThreadRestrictions::ScopedAllowIO allow_io_for_content_verification;
    std::string mhtml;
    ASSERT_TRUE(base::ReadFileToString(path, &mhtml));
    EXPECT_THAT(mhtml,
                HasSubstr("Content-Transfer-Encoding: quoted-printable"));
  }
}

class GenerateMHTMLAndExitRendererMessageFilter : public BrowserMessageFilter {
 public:
  GenerateMHTMLAndExitRendererMessageFilter(
      RenderProcessHostImpl* render_process_host)
      : BrowserMessageFilter(FrameMsgStart),
        render_process_host_(render_process_host) {}

 protected:
  ~GenerateMHTMLAndExitRendererMessageFilter() override {}

 private:
  bool OnMessageReceived(const IPC::Message& message) override {
    if (message.type() == FrameHostMsg_SerializeAsMHTMLResponse::ID) {
      // After |return false| below, this IPC message will be handled by the
      // product code as illustrated below.  (1), (2), (3) depict points in time
      // when product code runs on UI and FILE threads.  (X), (Y), (Z) depict
      // when we want test-injected tasks to run - for the repro, (Z) has to
      // happen between (1) and (3).  (Y?) and (Z?) depict when test tasks can
      // theoretically happen and ruin the repro.
      //
      //     IO thread       UI thread           FILE thread
      //     ---------       ---------           -----------
      //        |                |                     |
      //    WE ARE HERE          |                     |
      //        |                |                     |
      // after |return false|    |                     |
      //        +--------------->+                     |
      //        |                |                     |
      //        |               (X)                    |
      //        |                |                     |
      //        |                |                    (Y?)
      //        |               (Z?)                   |
      //        |                |                     |
      // (1)    |      MHTMLGenerationManager          |
      //        |      ::OnSerializeAsMHTMLResponse    |
      //        |                +-------------------->+
      //        |                |                     |
      //        |                |                    (Y)
      //        |                |                     |
      // (2)    |                |          MHTMLGenerationManager::Job
      //        |                |          ::CloseFileOnFileThread
      //        |                |                     |
      //        |               (Z)                    |
      //        |         test needs to inject         |
      //        |        fast renderer shutdown        |
      //        |      HERE - between (1) and (3)      |
      //        |                |                     |
      //        |                |                     |
      //        |                +<--------------------+
      //        |                |                     |
      // (3)    |      MHTMLGenerationManager          |
      //        |      ::OnFileClosed                  |
      //        |                |                     |
      //
      // We hope that (Z) happens between (1) and (3) by doing the following:
      // - From here post TaskX to UI thread.  (X) is guaranteed to happen
      //   before timepoint (1) (because posting of (1) happens after
      //   |return false| / before we post TaskX below).
      // - From (X) post TaskY to FILE thread.  Because this posting is done
      //   before (1), we can guarantee that (Y) will happen before (2).
      // - From (Y) post TaskZ to UI thread.  Because this posting is done
      //   before (2), we can guarantee that (Z) will happen before (3).
      // - We cannot really guarantee that (Y) and (Z) happen *after* (1) - i.e.
      //   execution at (Y?) and (Z?) instead is possible.  In practice,
      //   bouncing off of UI and FILE thread does mean (Z) happens after (1).
      BrowserThread::PostTask(
          BrowserThread::UI, FROM_HERE, base::Bind(
              &GenerateMHTMLAndExitRendererMessageFilter::TaskX,
              base::Unretained(this)));
    }

    return false;
  };

  void TaskX() {
    BrowserThread::PostTask(
        BrowserThread::FILE, FROM_HERE, base::Bind(
            &GenerateMHTMLAndExitRendererMessageFilter::TaskY,
            base::Unretained(this)));
  }

  void TaskY() {
    BrowserThread::PostTask(
        BrowserThread::UI, FROM_HERE, base::Bind(
            &GenerateMHTMLAndExitRendererMessageFilter::TaskZ,
            base::Unretained(this)));
  }

  void TaskZ() {
    render_process_host_->FastShutdownIfPossible();
  }

  RenderProcessHostImpl* render_process_host_;

  DISALLOW_COPY_AND_ASSIGN(GenerateMHTMLAndExitRendererMessageFilter);
};

// Regression test for the crash/race from https://crbug.com/612098.
IN_PROC_BROWSER_TEST_F(MHTMLGenerationTest, GenerateMHTMLAndExitRenderer) {
  NavigateToURL(shell(), embedded_test_server()->GetURL("/simple_page.html"));

  RenderProcessHostImpl* render_process_host =
      static_cast<RenderProcessHostImpl*>(
          shell()->web_contents()->GetRenderProcessHost());
  scoped_refptr<BrowserMessageFilter> filter =
      new GenerateMHTMLAndExitRendererMessageFilter(render_process_host);
  render_process_host->AddFilter(filter.get());

  base::FilePath path(temp_dir_.GetPath());
  path = path.Append(FILE_PATH_LITERAL("test.mht"));
  GenerateMHTMLForCurrentPage(MHTMLGenerationParams(path));

  EXPECT_GT(ReadFileSizeFromDisk(path), 100);  // Verify the actual file size.
}

IN_PROC_BROWSER_TEST_F(MHTMLGenerationTest, InvalidPath) {
  base::FilePath path(FILE_PATH_LITERAL("/invalid/file/path"));

  GenerateMHTML(path, embedded_test_server()->GetURL(
                          "/download/local-about-blank-subframes.html"));
  ASSERT_FALSE(HasFailure());  // No failures with the invocation itself?

  EXPECT_EQ(file_size(), -1);  // Expecting that the callback reported failure.
}

// Tests that MHTML generated using the default 'quoted-printable' encoding does
// not contain the 'binary' Content-Transfer-Encoding header, and generates
// base64 encoding for the image part.
IN_PROC_BROWSER_TEST_F(MHTMLGenerationTest, GenerateNonBinaryMHTMLWithImage) {
  base::FilePath path(temp_dir_.GetPath());
  path = path.Append(FILE_PATH_LITERAL("test_binary.mht"));

  GURL url(embedded_test_server()->GetURL("/page_with_image.html"));
  GenerateMHTML(path, url);
  ASSERT_FALSE(HasFailure());
  EXPECT_GT(file_size(), 0);  // Verify the size reported by the callback.
  EXPECT_GT(ReadFileSizeFromDisk(path), 100);  // Verify the actual file size.

  {
    base::ThreadRestrictions::ScopedAllowIO allow_io_for_content_verification;
    std::string mhtml;
    ASSERT_TRUE(base::ReadFileToString(path, &mhtml));
    EXPECT_THAT(mhtml, HasSubstr("Content-Transfer-Encoding: base64"));
    EXPECT_THAT(mhtml, Not(HasSubstr("Content-Transfer-Encoding: binary")));
    EXPECT_THAT(mhtml, ContainsRegex("Content-Location:.*blank.jpg"));
  }
}

// Tests that MHTML generated using the binary encoding contains the 'binary'
// Content-Transfer-Encoding header, and does not contain any base64 encoded
// parts.
IN_PROC_BROWSER_TEST_F(MHTMLGenerationTest, GenerateBinaryMHTMLWithImage) {
  base::FilePath path(temp_dir_.GetPath());
  path = path.Append(FILE_PATH_LITERAL("test_binary.mht"));

  GURL url(embedded_test_server()->GetURL("/page_with_image.html"));
  MHTMLGenerationParams params(path);
  params.use_binary_encoding = true;

  GenerateMHTML(params, url);
  ASSERT_FALSE(HasFailure());
  EXPECT_GT(file_size(), 0);  // Verify the size reported by the callback.
  EXPECT_GT(ReadFileSizeFromDisk(path), 100);  // Verify the actual file size.

  {
    base::ThreadRestrictions::ScopedAllowIO allow_io_for_content_verification;
    std::string mhtml;
    ASSERT_TRUE(base::ReadFileToString(path, &mhtml));
    EXPECT_THAT(mhtml, HasSubstr("Content-Transfer-Encoding: binary"));
    EXPECT_THAT(mhtml, Not(HasSubstr("Content-Transfer-Encoding: base64")));
    EXPECT_THAT(mhtml, ContainsRegex("Content-Location:.*blank.jpg"));
  }
}

IN_PROC_BROWSER_TEST_F(MHTMLGenerationTest, GenerateMHTMLIgnoreNoStore) {
  base::FilePath path(temp_dir_.GetPath());
  path = path.Append(FILE_PATH_LITERAL("test.mht"));

  GURL url(embedded_test_server()->GetURL("/nostore.html"));

  // Generate MHTML without specifying the FailForNoStoreMainFrame policy.
  GenerateMHTML(path, url);

  // We expect that there wasn't an error (file size -1 indicates an error.)
  ASSERT_FALSE(HasFailure());

  std::string mhtml;
  {
    base::ThreadRestrictions::ScopedAllowIO allow_io_for_content_verification;
    ASSERT_TRUE(base::ReadFileToString(path, &mhtml));
  }

  // Make sure the contents of the body are present.
  EXPECT_THAT(mhtml, HasSubstr("test body"));

  // Make sure that URL of the content is present.
  EXPECT_THAT(mhtml, ContainsRegex("Content-Location:.*/nostore.html"));
}

IN_PROC_BROWSER_TEST_F(MHTMLGenerationTest, GenerateMHTMLObeyNoStoreMainFrame) {
  base::FilePath path(temp_dir_.GetPath());
  path = path.Append(FILE_PATH_LITERAL("test.mht"));

  GURL url(embedded_test_server()->GetURL("/nostore.html"));

  // Generate MHTML, specifying the FailForNoStoreMainFrame policy.
  MHTMLGenerationParams params(path);
  params.cache_control_policy =
      blink::WebFrameSerializerCacheControlPolicy::FailForNoStoreMainFrame;

  GenerateMHTML(params, url);
  // We expect that there was an error (file size -1 indicates an error.)
  EXPECT_EQ(-1, file_size());

  std::string mhtml;
  {
    base::ThreadRestrictions::ScopedAllowIO allow_io_for_content_verification;
    ASSERT_TRUE(base::ReadFileToString(path, &mhtml));
  }

  // Make sure the contents are missing.
  EXPECT_THAT(mhtml, Not(HasSubstr("test body")));
}

IN_PROC_BROWSER_TEST_F(MHTMLGenerationTest,
                       GenerateMHTMLIgnoreNoStoreSubFrame) {
  base::FilePath path(temp_dir_.GetPath());
  path = path.Append(FILE_PATH_LITERAL("test.mht"));

  GURL url(embedded_test_server()->GetURL("/page_with_nostore_iframe.html"));

  // Generate MHTML, specifying the FailForNoStoreMainFrame policy.
  MHTMLGenerationParams params(path);
  params.cache_control_policy =
      blink::WebFrameSerializerCacheControlPolicy::FailForNoStoreMainFrame;

  GenerateMHTML(params, url);
  // We expect that there was no error (file size -1 indicates an error.)
  EXPECT_LT(0, file_size());

  std::string mhtml;
  {
    base::ThreadRestrictions::ScopedAllowIO allow_io_for_content_verification;
    ASSERT_TRUE(base::ReadFileToString(path, &mhtml));
  }

  EXPECT_THAT(mhtml, HasSubstr("Main Frame"));
  // Make sure that no-store subresources exist in this mode.
  EXPECT_THAT(mhtml, HasSubstr("no-store test body"));
  EXPECT_THAT(mhtml, ContainsRegex("Content-Location:.*nostore.jpg"));
}

IN_PROC_BROWSER_TEST_F(MHTMLGenerationTest, GenerateMHTMLObeyNoStoreSubFrame) {
  base::FilePath path(temp_dir_.GetPath());
  path = path.Append(FILE_PATH_LITERAL("test.mht"));

  GURL url(embedded_test_server()->GetURL("/page_with_nostore_iframe.html"));

  // Generate MHTML, specifying the FailForNoStoreMainFrame policy.
  MHTMLGenerationParams params(path);
  params.cache_control_policy = blink::WebFrameSerializerCacheControlPolicy::
      SkipAnyFrameOrResourceMarkedNoStore;

  GenerateMHTML(params, url);
  // We expect that there was no error (file size -1 indicates an error.)
  EXPECT_LT(0, file_size());

  std::string mhtml;
  {
    base::ThreadRestrictions::ScopedAllowIO allow_io_for_content_verification;
    ASSERT_TRUE(base::ReadFileToString(path, &mhtml));
  }

  EXPECT_THAT(mhtml, HasSubstr("Main Frame"));
  // Make sure the contents are missing.
  EXPECT_THAT(mhtml, Not(HasSubstr("no-store test body")));
  // This image comes from a resource marked no-store.
  EXPECT_THAT(mhtml, Not(ContainsRegex("Content-Location:.*nostore.jpg")));
}

// TODO(crbug.com/615291): These fail on Android under some circumstances.
#if defined(OS_ANDROID)
#define MAYBE_ViewedMHTMLContainsNoStoreContentIfNoCacheControlPolicy \
    DISABLED_ViewedMHTMLContainsNoStoreContentIfNoCacheControlPolicy
#define MAYBE_ViewedMHTMLDoesNotContainNoStoreContent \
    DISABLED_ViewedMHTMLDoesNotContainNoStoreContent
#else
#define MAYBE_ViewedMHTMLContainsNoStoreContentIfNoCacheControlPolicy \
    ViewedMHTMLContainsNoStoreContentIfNoCacheControlPolicy
#define MAYBE_ViewedMHTMLDoesNotContainNoStoreContent \
    ViewedMHTMLDoesNotContainNoStoreContent
#endif

IN_PROC_BROWSER_TEST_F(
    MHTMLGenerationTest,
    MAYBE_ViewedMHTMLContainsNoStoreContentIfNoCacheControlPolicy) {
  // Generate MHTML, specifying the FailForNoStoreMainFrame policy.
  base::FilePath path(temp_dir_.GetPath());
  path = path.Append(FILE_PATH_LITERAL("test.mht"));
  MHTMLGenerationParams params(path);

  // No special cache control options so we should see both frames.
  std::vector<std::string> expectations = {
      "Main Frame, normal headers.", "Cache-Control: no-store test body",
  };
  std::vector<std::string> forbidden;
  TestOriginalVsSavedPage(
      embedded_test_server()->GetURL("/page_with_nostore_iframe.html"), params,
      2 /* expected number of frames */, expectations, forbidden);

  std::string mhtml;
  {
    base::ThreadRestrictions::ScopedAllowIO allow_io_for_content_verification;
    ASSERT_TRUE(base::ReadFileToString(params.file_path, &mhtml));
  }
}

IN_PROC_BROWSER_TEST_F(MHTMLGenerationTest,
                       MAYBE_ViewedMHTMLDoesNotContainNoStoreContent) {
  // Generate MHTML, specifying the FailForNoStoreMainFrame policy.
  base::FilePath path(temp_dir_.GetPath());
  path = path.Append(FILE_PATH_LITERAL("test.mht"));
  MHTMLGenerationParams params(path);
  params.cache_control_policy = blink::WebFrameSerializerCacheControlPolicy::
      SkipAnyFrameOrResourceMarkedNoStore;

  // No special cache control options so we should see both frames.
  std::vector<std::string> expectations = {
      "Main Frame, normal headers.",
  };
  std::vector<std::string> forbidden = {
      "Cache-Control: no-store test body",
  };
  TestOriginalVsSavedPage(
      embedded_test_server()->GetURL("/page_with_nostore_iframe.html"), params,
      2 /* expected number of frames */, expectations, forbidden);

  std::string mhtml;
  {
    base::ThreadRestrictions::ScopedAllowIO allow_io_for_content_verification;
    ASSERT_TRUE(base::ReadFileToString(params.file_path, &mhtml));
  }
}

// Test suite that allows testing --site-per-process against cross-site frames.
// See http://dev.chromium.org/developers/design-documents/site-isolation.
class MHTMLGenerationSitePerProcessTest : public MHTMLGenerationTest {
 public:
  MHTMLGenerationSitePerProcessTest() {}

 protected:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    MHTMLGenerationTest::SetUpCommandLine(command_line);

    // Append --site-per-process flag.
    content::IsolateAllSitesForTesting(command_line);
  }

  void SetUpOnMainThread() override {
    MHTMLGenerationTest::SetUpOnMainThread();

    host_resolver()->AddRule("*", "127.0.0.1");
    ASSERT_TRUE(embedded_test_server()->Started());
    content::SetupCrossSiteRedirector(embedded_test_server());
  }

 private:
  DISALLOW_COPY_AND_ASSIGN(MHTMLGenerationSitePerProcessTest);
};

// Test for crbug.com/538766.
IN_PROC_BROWSER_TEST_F(MHTMLGenerationSitePerProcessTest, GenerateMHTML) {
  base::FilePath path(temp_dir_.GetPath());
  path = path.Append(FILE_PATH_LITERAL("test.mht"));

  GURL url(embedded_test_server()->GetURL(
      "a.com", "/frame_tree/page_with_one_frame.html"));
  GenerateMHTML(path, url);
  ASSERT_FALSE(HasFailure());

  std::string mhtml;
  {
    base::ThreadRestrictions::ScopedAllowIO allow_io_for_content_verification;
    ASSERT_TRUE(base::ReadFileToString(path, &mhtml));
  }

  // Make sure the contents of both frames are present.
  EXPECT_THAT(mhtml, HasSubstr("This page has one cross-site iframe"));
  EXPECT_THAT(mhtml, HasSubstr("This page has no title"));  // From title1.html.

  // Make sure that URLs of both frames are present
  // (note that these are single-line regexes).
  EXPECT_THAT(
      mhtml,
      ContainsRegex("Content-Location:.*/frame_tree/page_with_one_frame.html"));
  EXPECT_THAT(mhtml, ContainsRegex("Content-Location:.*/title1.html"));
}

}  // namespace content
