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