chromium/chrome/browser/extensions/api/document_scan/document_scan_apitest.cc

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

#include <string_view>

#include "base/auto_reset.h"
#include "base/check_deref.h"
#include "base/containers/map_util.h"
#include "base/notreached.h"
#include "base/path_service.h"
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/extensions/api/document_scan/document_scan_api_handler.h"
#include "chrome/browser/extensions/api/document_scan/document_scan_test_utils.h"
#include "chrome/browser/extensions/api/document_scan/fake_document_scan_ash.h"
#include "chrome/browser/extensions/api/document_scan/scanner_discovery_runner.h"
#include "chrome/browser/extensions/api/document_scan/start_scan_runner.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/pref_names.h"
#include "chromeos/crosapi/mojom/document_scan.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/extension_registry_observer.h"
#include "extensions/common/constants.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/test/test_extension_dir.h"

namespace extensions {

namespace {

constexpr size_t kRealBackendMinimumReadSize = 32768;

// Enum used to initialize the parameterized test with different types of
// extensions.
enum class ExtensionType {
  kChromeApp,
  kExtensionMV2,
  kExtensionMV3,
};

// Mapping of the different extension types used in the test to the specific
// manifest file names to create an extension of that type. The actual location
// of these files is at //chrome/test/data/extensions/api_test/document_scan/.
static constexpr auto kManifestFileNames =
    base::MakeFixedFlatMap<ExtensionType, std::string_view>(
        {{ExtensionType::kChromeApp, "manifest_chrome_app.json"},
         {ExtensionType::kExtensionMV2, "manifest_extension_v2.json"},
         {ExtensionType::kExtensionMV3, "manifest_extension_v3.json"}});

std::unique_ptr<TestExtensionDir> CreateDocumentScanExtension(
    ExtensionType type) {
  auto extension_dir = std::make_unique<TestExtensionDir>();

  base::FilePath test_data_dir;
  CHECK(base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir));
  base::FilePath document_scan_dir = test_data_dir.AppendASCII("extensions")
                                         .AppendASCII("api_test")
                                         .AppendASCII("document_scan");

  base::ScopedAllowBlockingForTesting allow_blocking;
  base::CopyDirectory(document_scan_dir, extension_dir->UnpackedPath(),
                      /*recursive=*/false);
  extension_dir->CopyFileTo(document_scan_dir.AppendASCII(CHECK_DEREF(
                                base::FindOrNull(kManifestFileNames, type))),
                            extensions::kManifestFilename);

  return extension_dir;
}

// Helper class that automatically adds extension IDs using the documentScan
// permission to the trusted extension list.  This allows tests to set up a
// trusted extension even with the autogenerated extension ID that comes from
// RunExtensionTest.
class AutoTruster : public extensions::ExtensionRegistryObserver {
 public:
  explicit AutoTruster(ExtensionRegistry* registry) {
    extension_registry_observation_.Observe(registry);
  }
  ~AutoTruster() override = default;

  void OnExtensionWillBeInstalled(content::BrowserContext* browser_context,
                                  const Extension* extension,
                                  bool is_update,
                                  const std::string& old_name) override {
    if (extension->permissions_data()->HasAPIPermission("documentScan")) {
      PrefService* prefs =
          Profile::FromBrowserContext(browser_context)->GetPrefs();
      ScopedListPrefUpdate update(prefs,
                                  prefs::kDocumentScanAPITrustedExtensions);
      update->Append(extension->id());
    }
  }

 private:
  base::ScopedObservation<ExtensionRegistry, ExtensionRegistryObserver>
      extension_registry_observation_{this};
  std::unique_ptr<ScopedListPrefUpdate> scoped_pref_update_;
};

}  // namespace

class DocumentScanApiTest : public ExtensionApiTest,
                            public testing::WithParamInterface<ExtensionType> {
 public:
  void SetUpOnMainThread() override {
    ExtensionApiTest::SetUpOnMainThread();

    // Replace the production DocumentScanAsh with a mock for testing.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
    chromeos::LacrosService::Get()->InjectRemoteForTesting(
        receiver_.BindNewPipeAndPassRemote());
#else
    DocumentScanAPIHandler::Get(browser()->profile())
        ->SetDocumentScanForTesting(&document_scan_ash_);
#endif
    document_scan()->SetSmallestMaxReadSize(kRealBackendMinimumReadSize);
  }

 protected:
  ExtensionType GetExtensionType() const { return GetParam(); }

  void RunTest(const char* html_test_page) {
    auto dir = CreateDocumentScanExtension(GetExtensionType());
    auto run_options = GetExtensionType() == ExtensionType::kChromeApp
                           ? RunOptions{.custom_arg = html_test_page,
                                        .launch_as_platform_app = true}
                           : RunOptions({.extension_url = html_test_page});
    ASSERT_TRUE(RunExtensionTest(dir->UnpackedPath(), run_options, {}));
  }

  FakeDocumentScanAsh* document_scan() { return &document_scan_ash_; }

 private:
  FakeDocumentScanAsh document_scan_ash_;
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  mojo::Receiver<crosapi::mojom::DocumentScan> receiver_{&document_scan_ash_};
#endif
};

IN_PROC_BROWSER_TEST_P(DocumentScanApiTest, TestLoadPermissions) {
  // This test simply checks to see if we have the correct permissions to load
  // the extension.
  RunTest("load_permissions.html");
}

IN_PROC_BROWSER_TEST_P(DocumentScanApiTest, GetScannerList_DiscoveryDenied) {
  ScannerDiscoveryRunner::SetDiscoveryConfirmationResultForTesting(false);
  RunTest("get_scanner_list_denied.html");
}

IN_PROC_BROWSER_TEST_P(DocumentScanApiTest, StartScan_PermissionDenied) {
  // There is a check for a valid scanner handle before the check for the
  // permission from the user.  Even though this tests the permission denied
  // case it still needs a valid scanner handle, so set the discovery
  // confirmation result.
  ScannerDiscoveryRunner::SetDiscoveryConfirmationResultForTesting(true);
  document_scan()->AddScanner(CreateTestScannerInfo());
  base::AutoReset<std::optional<bool>> testing_scope =
      StartScanRunner::SetStartScanConfirmationResultForTesting(false);
  RunTest("start_scan_denied.html");
}

IN_PROC_BROWSER_TEST_P(DocumentScanApiTest, PerformScan_PermissionAllowed) {
  ScannerDiscoveryRunner::SetDiscoveryConfirmationResultForTesting(true);
  base::AutoReset<std::optional<bool>> testing_scope =
      StartScanRunner::SetStartScanConfirmationResultForTesting(true);
  document_scan()->AddScanner(CreateTestScannerInfo());
  RunTest("perform_scan.html");
  // TODO(b/313494616): Load a second extension to verify (lack of)
  // cross-extension handle sharing.
}

IN_PROC_BROWSER_TEST_P(DocumentScanApiTest, PerformScan_ExtensionTrusted) {
  AutoTruster extension_truster(extension_registry());
  // Confirmation would fail, but it doesn't matter because the extension is
  // trusted.
  ScannerDiscoveryRunner::SetDiscoveryConfirmationResultForTesting(false);
  base::AutoReset<std::optional<bool>> testing_scope =
      StartScanRunner::SetStartScanConfirmationResultForTesting(false);
  document_scan()->AddScanner(CreateTestScannerInfo());
  RunTest("perform_scan.html");
}

INSTANTIATE_TEST_SUITE_P(/**/,
                         DocumentScanApiTest,
                         testing::Values(ExtensionType::kChromeApp,
                                         ExtensionType::kExtensionMV2,
                                         ExtensionType::kExtensionMV3));
}  // namespace extensions