| // 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/files/file_util.h" |
| #include "base/notreached.h" |
| #include "base/path_service.h" |
| #include "chrome/browser/ash/scanning/fake_lorgnette_scanner_manager.h" |
| #include "chrome/browser/ash/scanning/lorgnette_scanner_manager_factory.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 "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(); |
| |
| DocumentScanAPIHandler::Get(profile())->SetDocumentScanForTesting( |
| &document_scan_ash_); |
| |
| document_scan()->SetSmallestMaxReadSize(kRealBackendMinimumReadSize); |
| } |
| |
| void SetUpBrowserContextKeyedServices( |
| content::BrowserContext* context) override { |
| ExtensionApiTest::SetUpBrowserContextKeyedServices(context); |
| ash::LorgnetteScannerManagerFactory::GetInstance()->SetTestingFactory( |
| context, base::BindRepeating([](content::BrowserContext* context) |
| -> std::unique_ptr<KeyedService> { |
| return std::make_unique<ash::FakeLorgnetteScannerManager>(); |
| })); |
| } |
| |
| void SetScannerInfoList(std::vector<lorgnette::ScannerInfo> scanners) { |
| auto* scanner_manager = static_cast<ash::FakeLorgnetteScannerManager*>( |
| ash::LorgnetteScannerManagerFactory::GetForBrowserContext(profile())); |
| |
| lorgnette::ListScannersResponse response; |
| response.set_result(lorgnette::OPERATION_RESULT_SUCCESS); |
| for (auto& scanner : scanners) { |
| auto open_response = crosapi::mojom::OpenScannerResponse::New(); |
| open_response->result = crosapi::mojom::ScannerOperationResult::kSuccess; |
| open_response->scanner_id = scanner.name(); |
| open_response->scanner_handle = scanner.name() + "-handle"; |
| open_response->options.emplace(); |
| open_response->options.value()["option1"] = |
| CreateTestScannerOption("option1", 5); |
| document_scan()->SetOpenScannerResponse(scanner.name(), |
| std::move(open_response)); |
| |
| response.mutable_scanners()->Add(std::move(scanner)); |
| } |
| scanner_manager->SetGetScannerInfoListResponse(response); |
| } |
| |
| 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_; |
| }; |
| |
| 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); |
| SetScannerInfoList({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); |
| SetScannerInfoList({CreateTestScannerInfo()}); |
| const std::vector<std::string> scan_data = {"img", "data", "img", "data", ""}; |
| document_scan()->SetReadScanDataResponses( |
| scan_data, crosapi::mojom::ScannerOperationResult::kEndOfData); |
| 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); |
| SetScannerInfoList({CreateTestScannerInfo()}); |
| const std::vector<std::string> scan_data = {"img", "data", "img", "data", ""}; |
| document_scan()->SetReadScanDataResponses( |
| scan_data, crosapi::mojom::ScannerOperationResult::kEndOfData); |
| RunTest("perform_scan.html"); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(/**/, |
| DocumentScanApiTest, |
| testing::Values(ExtensionType::kChromeApp, |
| ExtensionType::kExtensionMV2, |
| ExtensionType::kExtensionMV3)); |
| } // namespace extensions |