chromium/fuchsia_web/webengine/context_provider_impl_unittest.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "fuchsia_web/webengine/context_provider_impl.h"

#include <fuchsia/web/cpp/fidl.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/namespace.h>
#include <lib/fidl/cpp/binding_set.h>
#include <lib/sys/cpp/outgoing_directory.h>
#include <lib/vfs/cpp/pseudo_dir.h>
#include <lib/vfs/cpp/service.h>
#include <zircon/status.h>
#include <zircon/types.h>

#include <map>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/fuchsia/file_utils.h"
#include "base/fuchsia/test_component_context_for_process.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/message_loop/message_pump_type.h"
#include "base/run_loop.h"
#include "base/test/task_environment.h"
#include "base/threading/thread.h"
#include "components/fuchsia_component_support/append_arguments_from_file.h"
#include "components/fuchsia_component_support/mock_realm.h"
#include "fuchsia_web/webengine/switches.h"
#include "services/network/public/cpp/network_switches.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

using ::testing::_;
using ::testing::Not;

constexpr char kTestDataFileIn[] = "DataFileIn";
constexpr char kTestDataFileOut[] = "DataFileOut";

constexpr uint64_t kTestQuotaBytes = 1024;
constexpr char kTestQuotaBytesSwitchValue[] = "1024";

// A fake implementation of fuchsia.component/Realm.
class FakeRealm {
 public:
  class Delegate {
   public:
    virtual ~Delegate() = default;
    virtual void OnChildInstanceCreated(const std::string& name) = 0;

   protected:
    Delegate() = default;
  };

  explicit FakeRealm(Delegate& delegate) : delegate_(delegate) {}
  FakeRealm(const FakeRealm&) = delete;
  FakeRealm& operator=(const FakeRealm&) = delete;
  ~FakeRealm() = default;

  // Returns true if the realm has no child instances.
  bool empty() const { return instances_.empty(); }

  // Sets a callback that will be run when the realm's last child is deleted.
  void set_on_empty_callback(base::OnceClosure on_empty_callback) {
    on_empty_callback_ = std::move(on_empty_callback);
  }

  // Saves copies of `child_decl` and `args` for subsequent validation by tests.
  void CreateChild(const fuchsia::component::decl::Child& child_decl,
                   const fuchsia::component::CreateChildArgs& args) {
    ASSERT_TRUE(!base::Contains(instances_, child_decl.name()));

    auto& child = instances_[child_decl.name()];
    child.decl = std::make_unique<fuchsia::component::decl::Child>();
    ASSERT_EQ(child_decl.Clone(child.decl.get()), ZX_OK);
    child.args = std::make_unique<fuchsia::component::CreateChildArgs>();
    ASSERT_EQ(args.Clone(child.args.get()), ZX_OK);
  }

  // Satisfies a request for `child_ref`'s dir by serving a PseudoDir on the
  // current thread containing entries for the Binder and Context protocols.
  // When the client connects to the last of these protocols, the realm
  // delegate's `OnChildInstanceCreated` method is called with the child's name.
  void OpenExposedDir(
      fuchsia::component::decl::ChildRef child_ref,
      fidl::InterfaceRequest<fuchsia::io::Directory> exposed_dir,
      fidl::InterfaceRequest<fuchsia::component::Binder>& binder_request,
      fidl::InterfaceRequest<fuchsia::web::Context>& context_request) {
    auto it = instances_.find(child_ref.name);
    ASSERT_FALSE(it == instances_.end());

    auto& child = it->second;

    // Publish a fake Binder to the child's exposed directory to capture the
    // host's request so that the test may close it to trigger child
    // destruction.
    child.instances.AddEntry(
        fuchsia::component::Binder::Name_,
        std::make_unique<vfs::Service>([&binder_request](zx::channel request,
                                                         async_dispatcher_t*) {
          binder_request = fidl::InterfaceRequest<fuchsia::component::Binder>(
              std::move(request));
        }));

    // Publish a fake Context that triggers notifying the delegate.
    child.instances.AddEntry(
        fuchsia::web::Context::Name_,
        std::make_unique<vfs::Service>(
            [&context_request, delegate = delegate_, name = child_ref.name](
                zx::channel request, async_dispatcher_t*) {
              context_request = fidl::InterfaceRequest<fuchsia::web::Context>(
                  std::move(request));
              delegate->OnChildInstanceCreated(name);
            }));

    child.instances.Serve(fuchsia::io::OpenFlags::RIGHT_READABLE |
                              fuchsia::io::OpenFlags::RIGHT_WRITABLE,
                          exposed_dir.TakeChannel());
  }

  // Destroys the child and runs the `on_empty_callback` if none remain.
  void DestroyChild(fuchsia::component::decl::ChildRef child) {
    ASSERT_EQ(instances_.erase(child.name), 1u);
    if (instances_.empty() && on_empty_callback_) {
      std::move(on_empty_callback_).Run();
    }
  }

  // Returns true if the realm has a child with the same name as `child`.
  bool HasChild(const fuchsia::component::decl::Child& child) {
    return base::Contains(instances_, child.name());
  }

  // Returns true if the realm has a child with the same name as `child`.
  bool HasChild(const fuchsia::component::decl::ChildRef& child) {
    return base::Contains(instances_, child.name);
  }

  // Returns the component declaration used to create the child named `name`.
  const fuchsia::component::decl::Child& GetChildDecl(const std::string& name) {
    return *instances_[name].decl;
  }

  // Returns the args used to create the child named `name`.
  const fuchsia::component::CreateChildArgs& GetChildArgs(
      const std::string& name) {
    return *instances_[name].args;
  }

 private:
  struct Child {
    vfs::PseudoDir instances;
    std::unique_ptr<fuchsia::component::decl::Child> decl;
    std::unique_ptr<fuchsia::component::CreateChildArgs> args;
  };

  const raw_ref<Delegate> delegate_;
  std::map<std::string, Child> instances_;
  base::OnceClosure on_empty_callback_;
};

class MockFakeRealmDelegate : public FakeRealm::Delegate {
 public:
  MOCK_METHOD(void,
              OnChildInstanceCreated,
              (const std::string& name),
              (override));
};

// Returns true if `arg` (a fuchsia::component::decl::CollectionRef) references
// `name`.
MATCHER_P(CollectionNameIs, name, "") {
  return arg.name == name;
}

// Returns true if the URL of `arg` (a fuchsia::component::decl::Child) is
// `url`.
MATCHER_P(UrlIs, url, "") {
  return arg.has_url() && arg.url() == url;
}

// Returns true if `realm` knows `arg`.
MATCHER_P(IsInRealm, realm, "") {
  return realm->HasChild(arg);
}

// Returns true if `arg` (a fuchsia::component::CreateChildArgs) has a dynamic
// directory offer for `name`.
MATCHER_P2(HasDynamicDirectoryOffer, name, rights, "") {
  if (!arg.has_dynamic_offers()) {
    return false;
  }
  for (const auto& offer : arg.dynamic_offers()) {
    if (offer.is_directory() && offer.directory().has_target_name() &&
        offer.directory().target_name() == name &&
        offer.directory().has_rights() &&
        offer.directory().rights() == rights) {
      return true;
    }
  }
  return false;
}

// Returns a primitive `CreateContextParams` configured to pass the test
// component's service directory as the service directory to be used by the
// context.
fuchsia::web::CreateContextParams BuildCreateContextParams() {
  fuchsia::web::CreateContextParams output;
  zx_status_t result = fdio_service_connect(
      base::kServiceDirectoryPath,
      output.mutable_service_directory()->NewRequest().TakeChannel().release());
  EXPECT_EQ(result, ZX_OK) << "Failed to open /svc";
  return output;
}

// Returns a handle to the test component's `/cache` directory.
fidl::InterfaceHandle<fuchsia::io::Directory> OpenCacheDirectory() {
  fidl::InterfaceHandle<fuchsia::io::Directory> cache_handle;
  zx_status_t result =
      fdio_service_connect(base::kPersistedCacheDirectoryPath,
                           cache_handle.NewRequest().TakeChannel().release());
  EXPECT_EQ(result, ZX_OK) << "Failed to open /cache";
  return cache_handle;
}

}  // namespace

class ContextProviderImplTest : public ::testing::Test {
 protected:
  ContextProviderImplTest()
      : service_thread_("outgoing server"),
        mock_realm_(test_component_context_.additional_services()) {
    provider_.emplace(outgoing_directory_);
    bindings_.AddBinding(&provider_.value(), provider_ptr_.NewRequest());
  }

  static void SetUpTestSuite() {
    // Need to do this once on the main thread before spinning off others.
    base::PlatformThread::SetCurrentThreadType(base::ThreadType::kDefault);
  }

  void SetUp() override {
    // Serve the context provider's outgoing directory on the service thread.
    ASSERT_TRUE(
        service_thread_.StartWithOptions({base::MessagePumpType::IO, 0}));
    fidl::InterfaceHandle<fuchsia::io::Directory> handle;
    service_thread_.task_runner()->PostTask(
        FROM_HERE,
        base::BindOnce(
            [](sys::OutgoingDirectory* dir,
               fidl::InterfaceRequest<fuchsia::io::Directory> request) {
              dir->Serve(std::move(request));
            },
            base::Unretained(&outgoing_directory_), handle.NewRequest()));
    service_thread_.FlushForTesting();

    // Install the outgoing directory in the test's namespace for inspection.
    fdio_ns_t* ns = nullptr;
    ASSERT_EQ(::fdio_ns_get_installed(&ns), ZX_OK);
    global_namespace_ = ns;
    ASSERT_EQ(::fdio_ns_bind(global_namespace_, kTestOutgoingPath,
                             handle.TakeChannel().release()),
              ZX_OK);
  }

  void TearDown() override {
    // Shut down the ContextProvider.
    provider_ptr_.Unbind();
    provider_.reset();

    // Wait for all children to be destroyed before the mock is destroyed.
    if (!fake_realm_.empty()) {
      base::RunLoop run_loop;
      fake_realm_.set_on_empty_callback(run_loop.QuitClosure());
      run_loop.Run();
    }

    ASSERT_EQ(::fdio_ns_unbind(global_namespace_, kTestOutgoingPath), ZX_OK);
  }

  // Add expectations to the MockRealm for creation and destruction of a single
  // child web_instance, invoking the respective methods on the test's
  // FakeRealm. `binder_request` and `context_request` capture the request
  // channels for the Binder and Context protocols, respectively.
  void ExpectChildInstance(
      fidl::InterfaceRequest<fuchsia::component::Binder>& binder_request,
      fidl::InterfaceRequest<fuchsia::web::Context>& context_request) {
    ::testing::InSequence sequence;

    EXPECT_CALL(mock_realm_, CreateChild(CollectionNameIs("web_instances"),
                                         Not(IsInRealm(&fake_realm_)), _, _))
        .WillOnce([this](const auto& collection, const auto& decl,
                         const auto& args, auto callback) {
          fake_realm_.CreateChild(decl, args);
          callback(
              fuchsia::component::Realm_CreateChild_Result::WithResponse({}));
        })
        .RetiresOnSaturation();

    EXPECT_CALL(mock_realm_, OpenExposedDir(IsInRealm(&fake_realm_), _, _))
        .WillOnce([this, &binder_request, &context_request](
                      const auto& child, auto dir_request, auto callback) {
          fake_realm_.OpenExposedDir(child, std::move(dir_request),
                                     binder_request, context_request);
          callback(
              fuchsia::component::Realm_OpenExposedDir_Result::WithResponse(
                  {}));
        })
        .RetiresOnSaturation();

    EXPECT_CALL(mock_realm_, DestroyChild(IsInRealm(&fake_realm_), _))
        .WillOnce([this](const auto& child, auto callback) {
          fake_realm_.DestroyChild(child);
          callback(
              fuchsia::component::Realm_DestroyChild_Result::WithResponse({}));
        })
        .RetiresOnSaturation();
  }

  // Calls on `provider` to create a new web instance and waits for its
  // creation. Returns the name of the newly-created instance.
  std::string CreateAndWaitForInstance(
      fuchsia::web::ContextProvider& provider,
      fuchsia::web::CreateContextParams create_params,
      fuchsia::web::ContextPtr& context_ptr) {
    base::RunLoop run_loop;

    // Terminate the loop if there's an error reported via the Context pointer.
    context_ptr.set_error_handler(
        [quit_loop = run_loop.QuitClosure()](zx_status_t error_status) {
          ADD_FAILURE() << "Context unexpectedly closed while waiting for "
                           "instance: "
                        << zx_status_get_string(error_status);
          quit_loop.Run();
        });

    provider.Create(std::move(create_params), context_ptr.NewRequest());

    // Pump events until the child instance is created and capture its name.
    std::string instance_name;
    EXPECT_CALL(mock_fake_realm_delegate_, OnChildInstanceCreated(_))
        .WillOnce([&instance_name, quit_loop = run_loop.QuitClosure()](
                      const std::string& name) {
          instance_name = name;
          quit_loop.Run();
        });
    run_loop.Run();
    ::testing::Mock::VerifyAndClearExpectations(&mock_fake_realm_delegate_);

    // Clear the error handler since the loop is no longer running.
    context_ptr.set_error_handler({});

    return instance_name;
  }

  // Waits for the Context channel to be closed and returns the associated
  // error or Epitaph.
  zx_status_t WaitForContextClosedStatus(
      fidl::InterfacePtr<fuchsia::web::Context>& context) {
    zx_status_t status = ZX_OK;
    base::RunLoop run_loop;
    context.set_error_handler(
        [&status, quit_loop = run_loop.QuitClosure()](zx_status_t error) {
          status = error;
          quit_loop.Run();
        });
    run_loop.Run();
    return status;
  }

  // Returns the component declaration used to create the child named `name`.
  const fuchsia::component::decl::Child& GetInstanceDecl(
      const std::string& name) {
    return fake_realm_.GetChildDecl(name);
  }

  // Returns the args used to create the child named `name`.
  const fuchsia::component::CreateChildArgs& GetInstanceArgs(
      const std::string& name) {
    return fake_realm_.GetChildArgs(name);
  }

  // Returns the path to the directory in the test's namespace holding dynamic
  // directory offers for the named instance.
  static base::FilePath GetInstanceDirectory(const std::string& name) {
    return base::FilePath(kTestOutgoingPath)
        .AppendASCII("web_instances")
        .AppendASCII(name);
  }

  // Returns the command line passed to the named instance.
  static base::CommandLine GetInstanceCommandLine(const std::string& name) {
    base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
    EXPECT_TRUE(fuchsia_component_support::AppendArgumentsFromFile(
        GetInstanceDirectory(name)
            .AppendASCII("command-line-config")
            .AppendASCII("argv.json"),
        command_line));
    return command_line;
  }

  fuchsia::web::ContextProvider& context_provider() { return *provider_ptr_; }

  void BindContextProvider(
      fidl::InterfaceRequest<fuchsia::web::ContextProvider> request) {
    bindings_.AddBinding(&provider_.value(), std::move(request));
  }

 private:
  // The path in the test's namespace where the outgoing directory given to
  // ContextProvider is bound for the sake of analysis.
  static constexpr char kTestOutgoingPath[] = "/test_outgoing_path";

  base::test::SingleThreadTaskEnvironment task_environment_{
      base::test::SingleThreadTaskEnvironment::MainThreadType::IO};

  raw_ptr<fdio_ns_t> global_namespace_ = nullptr;
  sys::OutgoingDirectory outgoing_directory_;
  // The thread serving `outgoing_directory_` must be destroyed before the
  // directory itself.
  base::Thread service_thread_;

  ::testing::StrictMock<MockFakeRealmDelegate> mock_fake_realm_delegate_;
  FakeRealm fake_realm_{mock_fake_realm_delegate_};

  // Used to replace the process component context with one providing a fake
  // fuchsia.component.Realm.
  base::TestComponentContextForProcess test_component_context_;

  // A mock fuchsia::component/Realm used to bridge to `fake_realm_`.
  ::testing::StrictMock<fuchsia_component_support::MockRealm> mock_realm_;
  fidl::BindingSet<fuchsia::web::ContextProvider> bindings_;
  std::optional<ContextProviderImpl> provider_;
  fuchsia::web::ContextProviderPtr provider_ptr_;
};

TEST_F(ContextProviderImplTest, CanCreateContextWithServiceDirectory) {
  fidl::InterfaceRequest<fuchsia::component::Binder> binder_request;
  fidl::InterfaceRequest<fuchsia::web::Context> context_request;
  ExpectChildInstance(binder_request, context_request);

  fuchsia::web::ContextPtr context;
  const std::string instance_name = CreateAndWaitForInstance(
      context_provider(), BuildCreateContextParams(), context);
  ASSERT_FALSE(instance_name.empty());

  // Requests for both interfaces should have been made.
  ASSERT_TRUE(binder_request);
  ASSERT_TRUE(context_request);

  const auto& child = GetInstanceDecl(instance_name);
  const auto& create_child_args = GetInstanceArgs(instance_name);

  ASSERT_THAT(child, UrlIs("#meta/web_instance_with_svc_directory.cm"));
  ASSERT_THAT(
      create_child_args,
      HasDynamicDirectoryOffer("svc", fuchsia::io::Operations::CONNECT |
                                          fuchsia::io::Operations::ENUMERATE |
                                          fuchsia::io::Operations::TRAVERSE));
  ASSERT_PRED1(base::PathExists,
               GetInstanceDirectory(instance_name).AppendASCII("svc"));
}

TEST_F(ContextProviderImplTest, CreateContextWithoutServiceDirectoryFails) {
  fuchsia::web::ContextPtr context_ptr;
  context_provider().Create({}, context_ptr.NewRequest());
  ASSERT_EQ(WaitForContextClosedStatus(context_ptr), ZX_ERR_INVALID_ARGS);
}

TEST_F(ContextProviderImplTest, CreateValidatesDataDirectory) {
  // Deliberately supply the wrong kind of object as the data-directory.
  fuchsia::web::CreateContextParams create_params = BuildCreateContextParams();
  zx::socket socket1, socket2;
  ASSERT_EQ(zx::socket::create(0, &socket1, &socket2), ZX_OK);
  create_params.set_data_directory(
      fidl::InterfaceHandle<fuchsia::io::Directory>(
          zx::channel(socket1.release())));

  fidl::InterfacePtr<fuchsia::web::Context> context_ptr;
  context_provider().Create(std::move(create_params), context_ptr.NewRequest());
  ASSERT_EQ(WaitForContextClosedStatus(context_ptr), ZX_ERR_PEER_CLOSED);
}

// Request Widevine DRM but do not enable VULKAN.
TEST_F(ContextProviderImplTest, CreateValidatesWidevineWithoutVulkan) {
  fuchsia::web::CreateContextParams create_params = BuildCreateContextParams();
  *create_params.mutable_features() =
      fuchsia::web::ContextFeatureFlags::WIDEVINE_CDM;
  *create_params.mutable_cdm_data_directory() = OpenCacheDirectory();

  fidl::InterfacePtr<fuchsia::web::Context> context;
  context_provider().Create(std::move(create_params), context.NewRequest());
  ASSERT_EQ(WaitForContextClosedStatus(context), ZX_ERR_NOT_SUPPORTED);
}

// Request PlayReady DRM but do not enable VULKAN.
TEST_F(ContextProviderImplTest, CreateValidatesPlayReadyWithoutVulkan) {
  fuchsia::web::CreateContextParams create_params = BuildCreateContextParams();
  create_params.set_playready_key_system("foo");
  *create_params.mutable_cdm_data_directory() = OpenCacheDirectory();

  fidl::InterfacePtr<fuchsia::web::Context> context;
  context_provider().Create(std::move(create_params), context.NewRequest());
  ASSERT_EQ(WaitForContextClosedStatus(context), ZX_ERR_NOT_SUPPORTED);
}

// Requesting DRM without VULKAN is acceptable for HEADLESS Contexts.
TEST_F(ContextProviderImplTest, CreateHeadlessDrmWithoutVulkan) {
  fidl::InterfaceRequest<fuchsia::component::Binder> binder_request;
  fidl::InterfaceRequest<fuchsia::web::Context> context_request;
  ExpectChildInstance(binder_request, context_request);

  fuchsia::web::ContextPtr context;
  fuchsia::web::CreateContextParams create_params = BuildCreateContextParams();
  *create_params.mutable_features() =
      fuchsia::web::ContextFeatureFlags::WIDEVINE_CDM |
      fuchsia::web::ContextFeatureFlags::HEADLESS;
  *create_params.mutable_cdm_data_directory() = OpenCacheDirectory();
  const std::string instance_name = CreateAndWaitForInstance(
      context_provider(), std::move(create_params), context);
  ASSERT_FALSE(instance_name.empty());

  // Requests for both interfaces should have been made.
  ASSERT_TRUE(binder_request);
  ASSERT_TRUE(context_request);

  const auto& child = GetInstanceDecl(instance_name);
  const auto& create_child_args = GetInstanceArgs(instance_name);

  ASSERT_THAT(child, UrlIs("#meta/web_instance_with_svc_directory.cm"));
  ASSERT_THAT(create_child_args,
              HasDynamicDirectoryOffer("cdm_data", fuchsia::io::RW_STAR_DIR));
  ASSERT_THAT(
      create_child_args,
      HasDynamicDirectoryOffer("svc", fuchsia::io::Operations::CONNECT |
                                          fuchsia::io::Operations::ENUMERATE |
                                          fuchsia::io::Operations::TRAVERSE));
  ASSERT_PRED1(base::PathExists,
               GetInstanceDirectory(instance_name).AppendASCII("cdm_data"));
  ASSERT_PRED1(base::PathExists,
               GetInstanceDirectory(instance_name).AppendASCII("svc"));
}

TEST_F(ContextProviderImplTest, MultipleConcurrentClients) {
  fidl::InterfaceRequest<fuchsia::component::Binder> binder_request_1;
  fidl::InterfaceRequest<fuchsia::web::Context> context_request_1;
  ExpectChildInstance(binder_request_1, context_request_1);
  fidl::InterfaceRequest<fuchsia::component::Binder> binder_request_2;
  fidl::InterfaceRequest<fuchsia::web::Context> context_request_2;
  ExpectChildInstance(binder_request_2, context_request_2);
  fidl::InterfaceRequest<fuchsia::component::Binder> binder_request_3;
  fidl::InterfaceRequest<fuchsia::web::Context> context_request_3;
  ExpectChildInstance(binder_request_3, context_request_3);

  // Create a Context via the pre-bound Provider pointer.
  fuchsia::web::ContextPtr context_1;
  const std::string instance_1_name = CreateAndWaitForInstance(
      context_provider(), BuildCreateContextParams(), context_1);
  ASSERT_FALSE(instance_1_name.empty());

  // Bind a second Provider pointer and create a Context via it.
  fuchsia::web::ContextProviderPtr provider_2_ptr;
  BindContextProvider(provider_2_ptr.NewRequest());
  fuchsia::web::ContextPtr context_2;
  const std::string instance_2_name = CreateAndWaitForInstance(
      *provider_2_ptr, BuildCreateContextParams(), context_2);
  ASSERT_FALSE(instance_2_name.empty());

  // Ensure that the initial ContextProvider connection is still usable, by
  // creating and verifying another Context from it.
  fuchsia::web::ContextPtr context_3;
  const std::string instance_3_name = CreateAndWaitForInstance(
      context_provider(), BuildCreateContextParams(), context_3);
  ASSERT_FALSE(instance_3_name.empty());
}

TEST_F(ContextProviderImplTest, WithProfileDir) {
  base::ScopedTempDir profile_temp_dir;

  // Setup data dir.
  ASSERT_TRUE(profile_temp_dir.CreateUniqueTempDir());
  ASSERT_TRUE(
      base::WriteFile(profile_temp_dir.GetPath().AppendASCII(kTestDataFileIn),
                      std::string_view()));

  fidl::InterfaceRequest<fuchsia::component::Binder> binder_request;
  fidl::InterfaceRequest<fuchsia::web::Context> context_request;
  ExpectChildInstance(binder_request, context_request);

  // Pass a handle data dir to the context.
  fuchsia::web::CreateContextParams create_params = BuildCreateContextParams();
  create_params.set_data_directory(
      base::OpenDirectoryHandle(profile_temp_dir.GetPath()));

  fuchsia::web::ContextPtr context;
  const std::string instance_name = CreateAndWaitForInstance(
      context_provider(), std::move(create_params), context);
  ASSERT_FALSE(instance_name.empty());

  // Requests for both interfaces should have been made.
  ASSERT_TRUE(binder_request);
  ASSERT_TRUE(context_request);

  const auto& child = GetInstanceDecl(instance_name);
  const auto& create_child_args = GetInstanceArgs(instance_name);
  const base::CommandLine command = GetInstanceCommandLine(instance_name);

  ASSERT_THAT(child, UrlIs("#meta/web_instance_with_svc_directory.cm"));
  ASSERT_THAT(create_child_args,
              HasDynamicDirectoryOffer("data", fuchsia::io::RW_STAR_DIR));
  EXPECT_FALSE(command.HasSwitch(switches::kIncognito));

  auto data_dir = GetInstanceDirectory(instance_name).AppendASCII("data");
  ASSERT_PRED1(base::PathExists, data_dir);

  // Make sure the input file can be seen in the mounted dir.
  ASSERT_PRED1(base::PathExists, data_dir.AppendASCII(kTestDataFileIn));

  // Make sure that the mapped dir can be written to.
  ASSERT_TRUE(base::WriteFile(data_dir.AppendASCII(kTestDataFileOut),
                              std::string_view()));
  ASSERT_PRED1(base::PathExists,
               profile_temp_dir.GetPath().AppendASCII(kTestDataFileOut));
}

// Verify that creation fails when passing in a file rather than a directory.
TEST_F(ContextProviderImplTest, FailsDataDirectoryIsFile) {
  base::ScopedTempDir profile_temp_dir;
  ASSERT_TRUE(profile_temp_dir.CreateUniqueTempDir());

  base::FilePath temp_file_path;
  ASSERT_TRUE(base::CreateTemporaryFileInDir(profile_temp_dir.GetPath(),
                                             &temp_file_path));

  fuchsia::web::CreateContextParams create_params = BuildCreateContextParams();
  create_params.set_data_directory(base::OpenDirectoryHandle(temp_file_path));

  fidl::InterfacePtr<fuchsia::web::Context> context;
  context_provider().Create(std::move(create_params), context.NewRequest());
  ASSERT_EQ(WaitForContextClosedStatus(context), ZX_ERR_PEER_CLOSED);
}

// Tests that unsafely_treat_insecure_origins_as_secure properly adds the right
// command-line arguments to the Context process.
TEST_F(ContextProviderImplTest, WithInsecureOriginsAsSecure) {
  fidl::InterfaceRequest<fuchsia::component::Binder> binder_request;
  fidl::InterfaceRequest<fuchsia::web::Context> context_request;
  ExpectChildInstance(binder_request, context_request);

  fuchsia::web::CreateContextParams create_params = BuildCreateContextParams();
  create_params.set_unsafely_treat_insecure_origins_as_secure(
      {"allow-running-insecure-content", "disable-mixed-content-autoupgrade",
       "http://example.com", "http://example.net"});

  fuchsia::web::ContextPtr context;
  const std::string instance_name = CreateAndWaitForInstance(
      context_provider(), std::move(create_params), context);
  ASSERT_FALSE(instance_name.empty());

  // Requests for both interfaces should have been made.
  ASSERT_TRUE(binder_request);
  ASSERT_TRUE(context_request);

  static constexpr char kAllowRunningInsecureContent[] =
      "allow-running-insecure-content";

  const base::CommandLine command = GetInstanceCommandLine(instance_name);

  EXPECT_TRUE(command.HasSwitch(
      network::switches::kUnsafelyTreatInsecureOriginAsSecure));
#if BUILDFLAG(ENABLE_CAST_RECEIVER)
  ASSERT_STREQ(kAllowRunningInsecureContent,
               switches::kAllowRunningInsecureContent);
  EXPECT_TRUE(command.HasSwitch(kAllowRunningInsecureContent));
  EXPECT_THAT(command.GetSwitchValueASCII(switches::kDisableFeatures),
              ::testing::HasSubstr("AutoupgradeMixedContent"));
  EXPECT_EQ(command.GetSwitchValueASCII(
                network::switches::kUnsafelyTreatInsecureOriginAsSecure),
            "http://example.com,http://example.net");
#else   // BUILDFLAG(ENABLE_CAST_RECEIVER)
  EXPECT_FALSE(command.HasSwitch(kAllowRunningInsecureContent));
  EXPECT_FALSE(command.HasSwitch(switches::kDisableFeatures));

  // The unrecognized values are passed on as origins.
  EXPECT_EQ(command.GetSwitchValueASCII(
                network::switches::kUnsafelyTreatInsecureOriginAsSecure),
            "allow-running-insecure-content,"
            "disable-mixed-content-autoupgrade,"
            "http://example.com,http://example.net");
#endif  // BUILDFLAG(ENABLE_CAST_RECEIVER)
}

TEST_F(ContextProviderImplTest, WithDataQuotaBytes) {
  base::ScopedTempDir profile_temp_dir;
  ASSERT_TRUE(profile_temp_dir.CreateUniqueTempDir());

  fidl::InterfaceRequest<fuchsia::component::Binder> binder_request;
  fidl::InterfaceRequest<fuchsia::web::Context> context_request;
  ExpectChildInstance(binder_request, context_request);

  fuchsia::web::CreateContextParams create_params = BuildCreateContextParams();
  create_params.set_data_directory(
      base::OpenDirectoryHandle(profile_temp_dir.GetPath()));
  create_params.set_data_quota_bytes(kTestQuotaBytes);

  fuchsia::web::ContextPtr context;
  const std::string instance_name = CreateAndWaitForInstance(
      context_provider(), std::move(create_params), context);
  ASSERT_FALSE(instance_name.empty());

  // Requests for both interfaces should have been made.
  ASSERT_TRUE(binder_request);
  ASSERT_TRUE(context_request);

  const base::CommandLine command = GetInstanceCommandLine(instance_name);
  EXPECT_EQ(command.GetSwitchValueASCII("data-quota-bytes"),
            kTestQuotaBytesSwitchValue);
}

TEST_F(ContextProviderImplTest, WithCdmDataQuotaBytes) {
  base::ScopedTempDir profile_temp_dir;
  ASSERT_TRUE(profile_temp_dir.CreateUniqueTempDir());

  fidl::InterfaceRequest<fuchsia::component::Binder> binder_request;
  fidl::InterfaceRequest<fuchsia::web::Context> context_request;
  ExpectChildInstance(binder_request, context_request);

  fuchsia::web::CreateContextParams create_params = BuildCreateContextParams();
  create_params.set_cdm_data_directory(
      base::OpenDirectoryHandle(profile_temp_dir.GetPath()));
  create_params.set_features(fuchsia::web::ContextFeatureFlags::HEADLESS |
                             fuchsia::web::ContextFeatureFlags::WIDEVINE_CDM);
  create_params.set_cdm_data_quota_bytes(kTestQuotaBytes);

  fuchsia::web::ContextPtr context;
  const std::string instance_name = CreateAndWaitForInstance(
      context_provider(), std::move(create_params), context);
  ASSERT_FALSE(instance_name.empty());

  // Requests for both interfaces should have been made.
  ASSERT_TRUE(binder_request);
  ASSERT_TRUE(context_request);

  const base::CommandLine command = GetInstanceCommandLine(instance_name);
  EXPECT_EQ(command.GetSwitchValueASCII("cdm-data-quota-bytes"),
            kTestQuotaBytesSwitchValue);
}