| // Copyright 2020 The Chromium Authors | 
 | // Use of this source code is governed by a BSD-style license that can be | 
 | // found in the LICENSE file. | 
 |  | 
 | #ifndef CHROME_BROWSER_POLICY_EXTENSION_FORCE_INSTALL_MIXIN_H_ | 
 | #define CHROME_BROWSER_POLICY_EXTENSION_FORCE_INSTALL_MIXIN_H_ | 
 |  | 
 | #include <atomic> | 
 | #include <map> | 
 | #include <optional> | 
 | #include <string> | 
 |  | 
 | #include "base/files/file_path.h" | 
 | #include "base/files/scoped_temp_dir.h" | 
 | #include "base/memory/raw_ptr.h" | 
 | #include "build/build_config.h" | 
 | #include "chrome/test/base/mixin_based_in_process_browser_test.h" | 
 | #include "components/policy/core/common/cloud/test/policy_builder.h" | 
 | #include "extensions/common/extension_id.h" | 
 | #include "net/test/embedded_test_server/embedded_test_server.h" | 
 |  | 
 | class GURL; | 
 | class Profile; | 
 |  | 
 | namespace base { | 
 | class Version; | 
 | }  // namespace base | 
 |  | 
 | namespace extensions { | 
 | class Extension; | 
 | }  // namespace extensions | 
 |  | 
 | namespace policy { | 
 | class MockConfigurationPolicyProvider; | 
 | }  // namespace policy | 
 |  | 
 | #if BUILDFLAG(IS_CHROMEOS) | 
 |  | 
 | namespace ash { | 
 | class DeviceStateMixin; | 
 | class EmbeddedPolicyTestServerMixin; | 
 | }  // namespace ash | 
 |  | 
 | namespace policy { | 
 | class DevicePolicyCrosTestHelper; | 
 | }  // namespace policy | 
 |  | 
 | #endif  // BUILDFLAG(IS_CHROMEOS) | 
 |  | 
 | // A mixin that allows to force-install an extension/app via user or device | 
 | // policy. | 
 | // | 
 | // Encapsulates the following operations: | 
 | // * generating a CRX file, | 
 | // * generating an update manifest, | 
 | // * hosting the update manifest and the CRX via an embedded test server, | 
 | // * configuring the force installation in the user/device policy. | 
 | // | 
 | // Example usage (for force-installing using the user-level policy): | 
 | // | 
 | //   class MyTestFixture : ... { | 
 | //    protected: | 
 | //     void SetUpOnMainThread() override { | 
 | //       ... | 
 | //       force_install_mixin_.InitWithMockPolicyProvider(...); | 
 | //     } | 
 | //     ExtensionForceInstallMixin force_install_mixin_{&mixin_host_}; | 
 | //   }; | 
 | //   IN_PROC_BROWSER_TEST_F(...) { | 
 | //     EXPECT_TRUE(force_install_mixin_.ForceInstallFromCrx(...)); | 
 | //   } | 
 | // | 
 | // Internally, the mixin owns an embedded test server that hosts files needed | 
 | // for the forced installation: | 
 | // * "/<extension_id>.xml" - update manifests referred to by policies, | 
 | // * "/<extension_id>-<version>.crx" - CRX packages referred to by the update | 
 | //   manifests. | 
 | class ExtensionForceInstallMixin final : public InProcessBrowserTestMixin { | 
 |  public: | 
 |   // The type of the waiting mode for the force installation operation. | 
 |   enum class WaitMode { | 
 |     // Don't wait, and return immediately. | 
 |     kNone, | 
 |     // Wait until the force-installation pref is updated. | 
 |     kPrefSet, | 
 |     // Wait until the extension is loaded. | 
 |     kLoad, | 
 |     // Wait until the extension's background page is loaded for the first time. | 
 |     kBackgroundPageFirstLoad, | 
 |     // Wait until the extension is loaded and its (presumably javascript | 
 |     // typescript) code sends the hard-coded message 'ready'. The extension | 
 |     // needs to send a message via `chrome.test.sendMessage('ready')`. | 
 |     kReadyMessageReceived, | 
 |   }; | 
 |  | 
 |   // The type of the waiting mode for the force-installed extension update. | 
 |   enum class UpdateWaitMode { | 
 |     // Don't wait, and return immediately. | 
 |     kNone, | 
 |     // TODO(crbug.com/40697472): Add other wait modes as necessary. | 
 |   }; | 
 |  | 
 |   // The type of the server error that should be simulated. | 
 |   enum class ServerErrorMode { | 
 |     // No error - network requests will succeed. | 
 |     kNone, | 
 |     // Don't respond to any network request at all (a rough equivalent of an | 
 |     // absent network). | 
 |     kHung, | 
 |     // Respond with the HTTP 500 Internal Server Error. | 
 |     kInternalError, | 
 |   }; | 
 |  | 
 |   explicit ExtensionForceInstallMixin(InProcessBrowserTestMixinHost* host); | 
 |   ExtensionForceInstallMixin(const ExtensionForceInstallMixin&) = delete; | 
 |   ExtensionForceInstallMixin& operator=(const ExtensionForceInstallMixin&) = | 
 |       delete; | 
 |   ~ExtensionForceInstallMixin() override; | 
 |  | 
 |   // Use one of the Init*() methods below to initialize the object before | 
 |   // calling any other method. | 
 |   // Note: The |profile| argument is optional; if it's null, only the `kNone` | 
 |   // waiting mode is allowed, and Get...Extension() methods are fobidden. | 
 |  | 
 |   void InitWithMockPolicyProvider( | 
 |       Profile* profile, | 
 |       policy::MockConfigurationPolicyProvider* mock_policy_provider); | 
 |  | 
 | #if BUILDFLAG(IS_CHROMEOS) | 
 |   void InitWithDeviceStateMixin(Profile* profile, | 
 |                                 ash::DeviceStateMixin* device_state_mixin); | 
 |   void InitWithDevicePolicyCrosTestHelper( | 
 |       Profile* profile, | 
 |       policy::DevicePolicyCrosTestHelper* device_policy_cros_test_helper); | 
 |   void InitWithEmbeddedPolicyMixin( | 
 |       Profile* profile, | 
 |       ash::EmbeddedPolicyTestServerMixin* policy_test_server_mixin, | 
 |       policy::UserPolicyBuilder* user_policy_builder, | 
 |       const std::string& account_id, | 
 |       const std::string& policy_type); | 
 | #endif  // BUILDFLAG(IS_CHROMEOS) | 
 |  | 
 |   // Force-installs the CRX file |crx_path|; under the hood, generates an update | 
 |   // manifest and serves it and the CRX file by the embedded test server. | 
 |   // |extension_id| - if non-null, will be set to the installed extension ID. | 
 |   // |extension_version| - if non-null, will be set to the installed extension | 
 |   // version. | 
 |   [[nodiscard]] bool ForceInstallFromCrx( | 
 |       const base::FilePath& crx_path, | 
 |       WaitMode wait_mode, | 
 |       extensions::ExtensionId* extension_id = nullptr, | 
 |       base::Version* extension_version = nullptr); | 
 |   // Force-installs the extension from the given source directory (which should | 
 |   // contain the manifest.json file and all other files of the extension). | 
 |   // Under the hood, packs the directory into a CRX file and serves it like | 
 |   // ForceInstallFromCrx(). | 
 |   // |pem_path| - if non-empty, will be used to load the private key for packing | 
 |   // the extension; when empty, a random key will be generated. | 
 |   // |extension_id| - if non-null, will be set to the installed extension ID. | 
 |   // |extension_version| - if non-null, will be set to the installed extension | 
 |   // version. | 
 |   [[nodiscard]] bool ForceInstallFromSourceDir( | 
 |       const base::FilePath& extension_dir_path, | 
 |       const std::optional<base::FilePath>& pem_path, | 
 |       WaitMode wait_mode, | 
 |       extensions::ExtensionId* extension_id = nullptr, | 
 |       base::Version* extension_version = nullptr); | 
 |  | 
 |   // Updates the served extension to the new version from |crx_path|. It's | 
 |   // expected that a ForceInstallFromCrx() call was done previously for this | 
 |   // extension. | 
 |   // |extension_version| - if non-null, will be set to the CRX'es version. | 
 |   [[nodiscard]] bool UpdateFromCrx(const base::FilePath& crx_path, | 
 |                                    UpdateWaitMode wait_mode, | 
 |                                    base::Version* extension_version = nullptr); | 
 |   // Updates the served |extension_id| extension to the new version from | 
 |   // |extension_dir_path|. It's expected that a ForceInstallFromSourceDir() call | 
 |   // was done previously for this extension. | 
 |   // |extension_version| - if non-null, will be set to the extension's version. | 
 |   [[nodiscard]] bool UpdateFromSourceDir( | 
 |       const base::FilePath& extension_dir_path, | 
 |       const extensions::ExtensionId& extension_id, | 
 |       UpdateWaitMode wait_mode, | 
 |       base::Version* extension_version = nullptr); | 
 |  | 
 |   // Returns the extension, or null if it's not installed yet. | 
 |   const extensions::Extension* GetInstalledExtension( | 
 |       const extensions::ExtensionId& extension_id) const; | 
 |   // Returns the extension, or null if it's not installed or not enabled yet. | 
 |   const extensions::Extension* GetEnabledExtension( | 
 |       const extensions::ExtensionId& extension_id) const; | 
 |  | 
 |   // Changes the embedded test server's error mode, allowing to simulate | 
 |   // network unavailability and errors. | 
 |   void SetServerErrorMode(ServerErrorMode server_error_mode); | 
 |  | 
 |   // InProcessBrowserTestMixin: | 
 |   void SetUpOnMainThread() override; | 
 |  | 
 |   bool initialized() const { return initialized_; } | 
 |  | 
 |  private: | 
 |   // Returns the path to the file that is served by the embedded test server | 
 |   // under the given name. | 
 |   base::FilePath GetPathInServedDir(const std::string& file_name) const; | 
 |   // Returns the URL of the update manifest pointing to the embedded test | 
 |   // server. | 
 |   GURL GetServedUpdateManifestUrl( | 
 |       const extensions::ExtensionId& extension_id) const; | 
 |   // Returns the URL of the CRX file pointing to the embedded test server. | 
 |   GURL GetServedCrxUrl(const extensions::ExtensionId& extension_id, | 
 |                        const base::Version& extension_version) const; | 
 |   // Makes the given |source_crx_path| file served by the embedded test server. | 
 |   bool ServeExistingCrx(const base::FilePath& source_crx_path, | 
 |                         const extensions::ExtensionId& extension_id, | 
 |                         const base::Version& extension_version); | 
 |   // Packs the given |extension_dir_path| (using the |pem_path| if provided or a | 
 |   // random key otherwise) and makes the produced CRX file served by the | 
 |   // embedded test server. | 
 |   bool CreateAndServeCrx(const base::FilePath& extension_dir_path, | 
 |                          const std::optional<base::FilePath>& pem_path, | 
 |                          const base::Version& extension_version, | 
 |                          extensions::ExtensionId* extension_id); | 
 |   // Force-installs the CRX file served by the embedded test server. | 
 |   bool ForceInstallFromServedCrx(const extensions::ExtensionId& extension_id, | 
 |                                  const base::Version& extension_version, | 
 |                                  WaitMode wait_mode); | 
 |   // Creates an update manifest with the CRX URL pointing to the embedded test | 
 |   // server. | 
 |   bool CreateAndServeUpdateManifestFile( | 
 |       const extensions::ExtensionId& extension_id, | 
 |       const base::Version& extension_version); | 
 |   // Sets the policy to force-install the given extension from the given update | 
 |   // manifest URL. | 
 |   bool UpdatePolicy(const extensions::ExtensionId& extension_id, | 
 |                     const GURL& update_manifest_url); | 
 |   // Waits until the extension's given version is installed and gets into the | 
 |   // requested mode. Does nothing if |wait_mode| is |kNone|. | 
 |   bool WaitForExtensionUpdate(const extensions::ExtensionId& extension_id, | 
 |                               const base::Version& extension_version, | 
 |                               UpdateWaitMode wait_mode); | 
 |  | 
 |   base::ScopedTempDir temp_dir_; | 
 |   net::EmbeddedTestServer embedded_test_server_; | 
 |   bool initialized_ = false; | 
 |   raw_ptr<Profile, DanglingUntriaged> profile_ = nullptr; | 
 |   raw_ptr<policy::MockConfigurationPolicyProvider> mock_policy_provider_ = | 
 |       nullptr; | 
 | #if BUILDFLAG(IS_CHROMEOS) | 
 |   raw_ptr<ash::DeviceStateMixin> device_state_mixin_ = nullptr; | 
 |   raw_ptr<policy::DevicePolicyCrosTestHelper> device_policy_cros_test_helper_ = | 
 |       nullptr; | 
 |   raw_ptr<ash::EmbeddedPolicyTestServerMixin> policy_test_server_mixin_ = | 
 |       nullptr; | 
 |   raw_ptr<policy::UserPolicyBuilder> user_policy_builder_ = nullptr; | 
 |   // |account_id_| and |policy_type_| are only used with | 
 |   // |policy_test_server_mixin_|. | 
 |   std::string account_id_; | 
 |   std::string policy_type_; | 
 | #endif | 
 |   // Mapping from the extension ID to the PEM file (the supplied or a randomly | 
 |   // generated one). It's not populated for extensions installed from CRX files, | 
 |   // since there's no PEM file available in that case. | 
 |   std::map<extensions::ExtensionId, base::FilePath> extension_id_to_pem_path_; | 
 |   // The current error mode. Stored in an atomic variable, as the server's | 
 |   // request handlers are reading from it on IO thread. | 
 |   std::atomic<ServerErrorMode> server_error_mode_{ServerErrorMode::kNone}; | 
 | }; | 
 |  | 
 | #endif  // CHROME_BROWSER_POLICY_EXTENSION_FORCE_INSTALL_MIXIN_H_ |