|  | // 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. | 
|  |  | 
|  | #include "chrome/browser/ash/scanning/scan_service.h" | 
|  |  | 
|  | #include <cstdint> | 
|  | #include <map> | 
|  | #include <memory> | 
|  | #include <string> | 
|  | #include <vector> | 
|  |  | 
|  | #include "ash/webui/scanning/mojom/scanning.mojom-test-utils.h" | 
|  | #include "ash/webui/scanning/mojom/scanning.mojom.h" | 
|  | #include "ash/webui/scanning/scanning_uma.h" | 
|  | #include "base/containers/flat_set.h" | 
|  | #include "base/files/file_path.h" | 
|  | #include "base/files/file_util.h" | 
|  | #include "base/files/scoped_temp_dir.h" | 
|  | #include "base/memory/raw_ptr.h" | 
|  | #include "base/strings/stringprintf.h" | 
|  | #include "base/strings/utf_string_conversions.h" | 
|  | #include "base/test/metrics/histogram_tester.h" | 
|  | #include "base/test/task_environment.h" | 
|  | #include "base/time/time.h" | 
|  | #include "base/unguessable_token.h" | 
|  | #include "chrome/browser/ash/login/users/fake_chrome_user_manager.h" | 
|  | #include "chrome/browser/ash/profiles/profile_helper.h" | 
|  | #include "chrome/browser/ash/scanning/fake_lorgnette_scanner_manager.h" | 
|  | #include "chrome/browser/ui/ash/holding_space/holding_space_keyed_service.h" | 
|  | #include "chrome/browser/ui/ash/holding_space/holding_space_keyed_service_factory.h" | 
|  | #include "chrome/browser/ui/ash/holding_space/scoped_test_mount_point.h" | 
|  | #include "chrome/browser/ui/ash/test_session_controller.h" | 
|  | #include "chrome/test/base/testing_browser_process.h" | 
|  | #include "chrome/test/base/testing_profile.h" | 
|  | #include "chrome/test/base/testing_profile_manager.h" | 
|  | #include "chromeos/ash/components/dbus/lorgnette/lorgnette_service.pb.h" | 
|  | #include "components/account_id/account_id.h" | 
|  | #include "components/user_manager/scoped_user_manager.h" | 
|  | #include "content/public/test/browser_task_environment.h" | 
|  | #include "mojo/public/cpp/bindings/pending_remote.h" | 
|  | #include "mojo/public/cpp/bindings/receiver.h" | 
|  | #include "mojo/public/cpp/bindings/remote.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  | #include "third_party/abseil-cpp/absl/types/optional.h" | 
|  | #include "third_party/skia/include/core/SkBitmap.h" | 
|  | #include "ui/gfx/codec/png_codec.h" | 
|  |  | 
|  | namespace ash { | 
|  |  | 
|  | using holding_space::ScopedTestMountPoint; | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | namespace mojo_ipc = scanning::mojom; | 
|  |  | 
|  | using ProtoScanFailureMode = lorgnette::ScanFailureMode; | 
|  |  | 
|  | // Path to the user's "My files" folder. | 
|  | constexpr char kMyFilesPath[] = "/home/chronos/user/MyFiles"; | 
|  |  | 
|  | // Scanner names used for tests. | 
|  | constexpr char kFirstTestScannerName[] = "Test Scanner 1"; | 
|  | constexpr char16_t kFirstTestScannerName16[] = u"Test Scanner 1"; | 
|  | constexpr char kSecondTestScannerName[] = "Test Scanner 2"; | 
|  | constexpr char16_t kSecondTestScannerName16[] = u"Test Scanner 2"; | 
|  | constexpr char kEpsonTestName[] = | 
|  | "airscan:escl:EPSON XP-7100 Series:http://100.107.108.190:443/eSCL/"; | 
|  |  | 
|  | // Document source name used for tests. | 
|  | constexpr char kDocumentSourceName[] = "Flatbed"; | 
|  | constexpr char kAdfSourceName[] = "ADF Duplex"; | 
|  |  | 
|  | // Resolutions used for tests. | 
|  | constexpr uint32_t kFirstResolution = 75; | 
|  | constexpr uint32_t kSecondResolution = 300; | 
|  |  | 
|  | // Email used for test profile. | 
|  | constexpr char kUserEmail[] = "user@email.com"; | 
|  |  | 
|  | // Translation from file type to saved file extension. | 
|  | const std::map<mojo_ipc::FileType, std::string> kFileTypes = { | 
|  | {mojo_ipc::FileType::kJpg, "jpg"}, | 
|  | {mojo_ipc::FileType::kPdf, "pdf"}, | 
|  | {mojo_ipc::FileType::kPng, "png"}}; | 
|  |  | 
|  | // Returns a DocumentSource object. | 
|  | lorgnette::DocumentSource CreateLorgnetteDocumentSource() { | 
|  | lorgnette::DocumentSource source; | 
|  | source.set_type(lorgnette::SOURCE_PLATEN); | 
|  | source.set_name(kDocumentSourceName); | 
|  | source.add_color_modes(lorgnette::MODE_COLOR); | 
|  | source.add_resolutions(kFirstResolution); | 
|  | source.add_resolutions(kSecondResolution); | 
|  | return source; | 
|  | } | 
|  |  | 
|  | // Returns an ADF Duplex DocumentSource object. | 
|  | lorgnette::DocumentSource CreateAdfDuplexDocumentSource() { | 
|  | lorgnette::DocumentSource source; | 
|  | source.set_type(lorgnette::SOURCE_ADF_DUPLEX); | 
|  | source.set_name(kAdfSourceName); | 
|  | return source; | 
|  | } | 
|  |  | 
|  | // Returns a ScannerCapabilities object. | 
|  | lorgnette::ScannerCapabilities CreateLorgnetteScannerCapabilities() { | 
|  | lorgnette::ScannerCapabilities caps; | 
|  | *caps.add_sources() = CreateLorgnetteDocumentSource(); | 
|  | return caps; | 
|  | } | 
|  |  | 
|  | // Returns a ScannerCapabilities object used for testing a scanner | 
|  | // that flips alternate pages.. | 
|  | lorgnette::ScannerCapabilities CreateEpsonScannerCapabilities() { | 
|  | lorgnette::ScannerCapabilities caps; | 
|  | *caps.add_sources() = CreateAdfDuplexDocumentSource(); | 
|  | caps.add_color_modes(lorgnette::MODE_COLOR); | 
|  | caps.add_resolutions(kFirstResolution); | 
|  | caps.add_resolutions(kSecondResolution); | 
|  | return caps; | 
|  | } | 
|  |  | 
|  | // Returns single FilePath to mimic saved PDF format scan. | 
|  | base::FilePath CreateSavedPdfScanPath(const base::FilePath& dir, | 
|  | const base::Time::Exploded& scan_time) { | 
|  | return dir.Append(base::StringPrintf("scan_%02d%02d%02d-%02d%02d%02d.pdf", | 
|  | scan_time.year, scan_time.month, | 
|  | scan_time.day_of_month, scan_time.hour, | 
|  | scan_time.minute, scan_time.second)); | 
|  | } | 
|  |  | 
|  | // Returns a vector of FilePaths to mimic saved scans. | 
|  | std::vector<base::FilePath> CreateSavedScanPaths( | 
|  | const base::FilePath& dir, | 
|  | const base::Time::Exploded& scan_time, | 
|  | const mojo_ipc::FileType& file_type, | 
|  | int num_pages_to_scan) { | 
|  | const auto typeAndExtension = kFileTypes.find(file_type); | 
|  | EXPECT_NE(typeAndExtension, kFileTypes.cend()); | 
|  | std::vector<base::FilePath> file_paths; | 
|  | if (file_type == mojo_ipc::FileType::kPdf) { | 
|  | file_paths.reserve(1); | 
|  | file_paths.push_back(CreateSavedPdfScanPath(dir, scan_time)); | 
|  | } else { | 
|  | file_paths.reserve(num_pages_to_scan); | 
|  | for (int i = 1; i <= num_pages_to_scan; i++) { | 
|  | file_paths.push_back(dir.Append(base::StringPrintf( | 
|  | "scan_%02d%02d%02d-%02d%02d%02d_%d.%s", scan_time.year, | 
|  | scan_time.month, scan_time.day_of_month, scan_time.hour, | 
|  | scan_time.minute, scan_time.second, i, | 
|  | typeAndExtension->second.c_str()))); | 
|  | } | 
|  | } | 
|  | return file_paths; | 
|  | } | 
|  |  | 
|  | // Returns a manually generated PNG image. |alpha| is used to make unique PNGs. | 
|  | std::string CreatePng(const int alpha = 255) { | 
|  | DCHECK(alpha >= 0 && alpha <= 255); | 
|  |  | 
|  | SkBitmap bitmap; | 
|  | bitmap.allocN32Pixels(100, 100); | 
|  | bitmap.eraseARGB(alpha, 0, 255, 0); | 
|  | std::vector<unsigned char> bytes; | 
|  | gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, false, &bytes); | 
|  | return std::string(bytes.begin(), bytes.end()); | 
|  | } | 
|  |  | 
|  | // Returns scan settings with the given path and file type. | 
|  | mojo_ipc::ScanSettings CreateScanSettings(const base::FilePath& scan_to_path, | 
|  | const mojo_ipc::FileType& file_type, | 
|  | const std::string source = "") { | 
|  | mojo_ipc::ScanSettings settings; | 
|  | settings.scan_to_path = scan_to_path; | 
|  | settings.file_type = file_type; | 
|  | settings.source_name = source; | 
|  | return settings; | 
|  | } | 
|  |  | 
|  | // Returns a profile manager set up to generate testing profiles. | 
|  | std::unique_ptr<TestingProfileManager> CreateTestingProfileManager() { | 
|  | auto profile_manager = std::make_unique<TestingProfileManager>( | 
|  | TestingBrowserProcess::GetGlobal()); | 
|  | EXPECT_TRUE(profile_manager->SetUp()); | 
|  | return profile_manager; | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | class FakeScanJobObserver : public mojo_ipc::ScanJobObserver { | 
|  | public: | 
|  | FakeScanJobObserver() = default; | 
|  | ~FakeScanJobObserver() override = default; | 
|  |  | 
|  | FakeScanJobObserver(const FakeScanJobObserver&) = delete; | 
|  | FakeScanJobObserver& operator=(const FakeScanJobObserver&) = delete; | 
|  |  | 
|  | // mojo_ipc::ScanJobObserver: | 
|  | void OnPageProgress(uint32_t page_number, | 
|  | uint32_t progress_percent) override { | 
|  | progress_ = progress_percent; | 
|  | } | 
|  |  | 
|  | void OnPageComplete(const std::vector<uint8_t>& page_data, | 
|  | const uint32_t new_page_index) override { | 
|  | page_complete_ = true; | 
|  | new_page_index_ = new_page_index; | 
|  | } | 
|  |  | 
|  | void OnScanComplete( | 
|  | ProtoScanFailureMode result, | 
|  | const std::vector<base::FilePath>& scanned_file_paths) override { | 
|  | scan_result_ = result; | 
|  | scanned_file_paths_ = scanned_file_paths; | 
|  | } | 
|  |  | 
|  | void OnCancelComplete(bool success) override { | 
|  | cancel_scan_success_ = success; | 
|  | } | 
|  |  | 
|  | void OnMultiPageScanFail(ProtoScanFailureMode result) override { | 
|  | multi_page_scan_result_ = result; | 
|  | } | 
|  |  | 
|  | // Creates a pending remote that can be passed in calls to | 
|  | // ScanService::StartScan(). | 
|  | mojo::PendingRemote<mojo_ipc::ScanJobObserver> GenerateRemote() { | 
|  | if (receiver_.is_bound()) | 
|  | receiver_.reset(); | 
|  |  | 
|  | mojo::PendingRemote<mojo_ipc::ScanJobObserver> remote; | 
|  | receiver_.Bind(remote.InitWithNewPipeAndPassReceiver()); | 
|  | return remote; | 
|  | } | 
|  |  | 
|  | // Returns true if the scan completed successfully. | 
|  | bool scan_success() const { | 
|  | return progress_ == 100 && page_complete_ && | 
|  | scan_result_ == ProtoScanFailureMode::SCAN_FAILURE_MODE_NO_FAILURE; | 
|  | } | 
|  |  | 
|  | // Returns true if the cancel scan request completed successfully. | 
|  | bool cancel_scan_success() const { return cancel_scan_success_; } | 
|  |  | 
|  | uint32_t new_page_index() const { return new_page_index_; } | 
|  |  | 
|  | // Returns the result of the scan job. | 
|  | ProtoScanFailureMode scan_result() const { return scan_result_; } | 
|  |  | 
|  | // Returns the result of the multi-page scan job. | 
|  | ProtoScanFailureMode multi_page_scan_result() const { | 
|  | return multi_page_scan_result_; | 
|  | } | 
|  |  | 
|  | // Returns file paths of the saved scan files. | 
|  | std::vector<base::FilePath> scanned_file_paths() const { | 
|  | return scanned_file_paths_; | 
|  | } | 
|  |  | 
|  | private: | 
|  | uint32_t progress_ = 0; | 
|  | bool page_complete_ = false; | 
|  | uint32_t new_page_index_ = UINT32_MAX; | 
|  | ProtoScanFailureMode scan_result_ = | 
|  | ProtoScanFailureMode::SCAN_FAILURE_MODE_UNKNOWN; | 
|  | ProtoScanFailureMode multi_page_scan_result_ = | 
|  | ProtoScanFailureMode::SCAN_FAILURE_MODE_UNKNOWN; | 
|  | bool cancel_scan_success_ = false; | 
|  | std::vector<base::FilePath> scanned_file_paths_; | 
|  | mojo::Receiver<mojo_ipc::ScanJobObserver> receiver_{this}; | 
|  | }; | 
|  |  | 
|  | class ScanServiceTest : public testing::Test { | 
|  | public: | 
|  | ScanServiceTest() | 
|  | : profile_manager_(CreateTestingProfileManager()), | 
|  | profile_(profile_manager_->CreateTestingProfile(kUserEmail)), | 
|  | scanned_files_mount_( | 
|  | ScopedTestMountPoint::CreateAndMountDownloads(profile_)), | 
|  | session_controller_(std::make_unique<TestSessionController>()), | 
|  | user_manager_(new ash::FakeChromeUserManager), | 
|  | user_manager_owner_(base::WrapUnique(user_manager_.get())) { | 
|  | DCHECK(scanned_files_mount_->IsValid()); | 
|  | const AccountId account_id(AccountId::FromUserEmail(kUserEmail)); | 
|  | user_manager_->AddUser(account_id); | 
|  | user_manager_->LoginUser(account_id); | 
|  | SetupScanService(scanned_files_mount_->GetRootPath(), | 
|  | base::FilePath("/google/drive")); | 
|  | } | 
|  |  | 
|  | void SetupScanService(base::FilePath my_files_path, | 
|  | base::FilePath google_drive_path) { | 
|  | scan_service_ = std::make_unique<ScanService>( | 
|  | &fake_lorgnette_scanner_manager_, my_files_path, google_drive_path, | 
|  | profile_); | 
|  | if (scan_service_remote_.is_bound()) { | 
|  | scan_service_remote_.reset(); | 
|  | } | 
|  | scan_service_->BindInterface( | 
|  | scan_service_remote_.BindNewPipeAndPassReceiver()); | 
|  | } | 
|  |  | 
|  | // Gets scanners by calling ScanService::GetScanners() via the mojo::Remote. | 
|  | std::vector<mojo_ipc::ScannerPtr> GetScanners() { | 
|  | std::vector<mojo_ipc::ScannerPtr> scanners; | 
|  | mojo_ipc::ScanServiceAsyncWaiter(scan_service_remote_.get()) | 
|  | .GetScanners(&scanners); | 
|  | return scanners; | 
|  | } | 
|  |  | 
|  | // Gets scanner capabilities for the scanner identified by |scanner_id| by | 
|  | // calling ScanService::GetScannerCapabilities() via the mojo::Remote. | 
|  | mojo_ipc::ScannerCapabilitiesPtr GetScannerCapabilities( | 
|  | const base::UnguessableToken& scanner_id) { | 
|  | mojo_ipc::ScannerCapabilitiesPtr caps = | 
|  | mojo_ipc::ScannerCapabilities::New(); | 
|  | mojo_ipc::ScanServiceAsyncWaiter(scan_service_remote_.get()) | 
|  | .GetScannerCapabilities(scanner_id, &caps); | 
|  | return caps; | 
|  | } | 
|  |  | 
|  | // Starts a scan with the scanner identified by |scanner_id| with the given | 
|  | // |settings| by calling ScanService::StartScan() via the mojo::Remote. | 
|  | bool StartScan(const base::UnguessableToken& scanner_id, | 
|  | mojo_ipc::ScanSettingsPtr settings) { | 
|  | bool success; | 
|  | mojo_ipc::ScanServiceAsyncWaiter(scan_service_remote_.get()) | 
|  | .StartScan(scanner_id, std::move(settings), | 
|  | fake_scan_job_observer_.GenerateRemote(), &success); | 
|  | task_environment_.RunUntilIdle(); | 
|  | return success; | 
|  | } | 
|  |  | 
|  | // Starts a multi-page scan with the scanner identified by |scanner_id| with | 
|  | // the given |settings| by calling ScanService::StartMultiPageScan() via the | 
|  | // mojo::Remote. Binds the returned MultiPageScanController | 
|  | // mojo::PendingRemote. | 
|  | bool StartMultiPageScan(const base::UnguessableToken& scanner_id, | 
|  | mojo_ipc::ScanSettingsPtr settings) { | 
|  | mojo::PendingRemote<mojo_ipc::MultiPageScanController> pending_remote; | 
|  | mojo_ipc::ScanServiceAsyncWaiter(scan_service_remote_.get()) | 
|  | .StartMultiPageScan(scanner_id, std::move(settings), | 
|  | fake_scan_job_observer_.GenerateRemote(), | 
|  | &pending_remote); | 
|  | if (!pending_remote.is_valid()) | 
|  | return false; | 
|  |  | 
|  | multi_page_scan_controller_remote_.Bind(std::move(pending_remote)); | 
|  | task_environment_.RunUntilIdle(); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | void ResetMultiPageScanControllerRemote() { | 
|  | multi_page_scan_controller_remote_.reset(); | 
|  | } | 
|  |  | 
|  | bool ScanNextPage(const base::UnguessableToken& scanner_id, | 
|  | mojo_ipc::ScanSettingsPtr settings) { | 
|  | bool success; | 
|  | mojo_ipc::MultiPageScanControllerAsyncWaiter( | 
|  | multi_page_scan_controller_remote_.get()) | 
|  | .ScanNextPage(scanner_id, std::move(settings), &success); | 
|  | task_environment_.RunUntilIdle(); | 
|  | return success; | 
|  | } | 
|  |  | 
|  | // Performs a cancel scan request. | 
|  | void CancelScan() { | 
|  | scan_service_remote_->CancelScan(); | 
|  | task_environment_.RunUntilIdle(); | 
|  | } | 
|  |  | 
|  | void CompleteMultiPageScan() { | 
|  | multi_page_scan_controller_remote_->CompleteMultiPageScan(); | 
|  | task_environment_.RunUntilIdle(); | 
|  | } | 
|  |  | 
|  | void RemovePage(const uint32_t page_index) { | 
|  | multi_page_scan_controller_remote_->RemovePage(page_index); | 
|  | task_environment_.RunUntilIdle(); | 
|  | } | 
|  |  | 
|  | bool RescanPage(const base::UnguessableToken& scanner_id, | 
|  | mojo_ipc::ScanSettingsPtr settings, | 
|  | const uint32_t page_index) { | 
|  | bool success; | 
|  | mojo_ipc::MultiPageScanControllerAsyncWaiter( | 
|  | multi_page_scan_controller_remote_.get()) | 
|  | .RescanPage(scanner_id, std::move(settings), page_index, &success); | 
|  | task_environment_.RunUntilIdle(); | 
|  | return success; | 
|  | } | 
|  |  | 
|  | protected: | 
|  | // A `BrowserTaskEnvironment` allows the test to create a `TestingProfile`. | 
|  | content::BrowserTaskEnvironment task_environment_{ | 
|  | base::test::TaskEnvironment::TimeSource::MOCK_TIME}; | 
|  | FakeLorgnetteScannerManager fake_lorgnette_scanner_manager_; | 
|  | FakeScanJobObserver fake_scan_job_observer_; | 
|  | std::unique_ptr<TestingProfileManager> profile_manager_; | 
|  | const raw_ptr<TestingProfile, ExperimentalAsh> profile_; | 
|  | std::unique_ptr<ScopedTestMountPoint> scanned_files_mount_; | 
|  | std::unique_ptr<TestSessionController> session_controller_; | 
|  | const raw_ptr<ash::FakeChromeUserManager, ExperimentalAsh> user_manager_; | 
|  | user_manager::ScopedUserManager user_manager_owner_; | 
|  | std::unique_ptr<ScanService> scan_service_; | 
|  |  | 
|  | private: | 
|  | mojo::Remote<mojo_ipc::ScanService> scan_service_remote_; | 
|  | mojo::Remote<mojo_ipc::MultiPageScanController> | 
|  | multi_page_scan_controller_remote_; | 
|  | }; | 
|  |  | 
|  | // Test that no scanners are returned when there are no scanner names. | 
|  | TEST_F(ScanServiceTest, NoScannerNames) { | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse({}); | 
|  | auto scanners = GetScanners(); | 
|  | EXPECT_TRUE(scanners.empty()); | 
|  | } | 
|  |  | 
|  | // Test that a scanner is returned with the correct display name. | 
|  | TEST_F(ScanServiceTest, GetScanners) { | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  | EXPECT_EQ(scanners[0]->display_name, kFirstTestScannerName16); | 
|  | } | 
|  |  | 
|  | // Test that two returned scanners have unique IDs. | 
|  | TEST_F(ScanServiceTest, UniqueScannerIds) { | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName, kSecondTestScannerName}); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 2u); | 
|  | EXPECT_EQ(scanners[0]->display_name, kFirstTestScannerName16); | 
|  | EXPECT_EQ(scanners[1]->display_name, kSecondTestScannerName16); | 
|  | EXPECT_NE(scanners[0]->id, scanners[1]->id); | 
|  | } | 
|  |  | 
|  | // Test that the number of detected scanners is recorded. | 
|  | TEST_F(ScanServiceTest, RecordNumDetectedScanners) { | 
|  | base::HistogramTester histogram_tester; | 
|  | histogram_tester.ExpectTotalCount("Scanning.NumDetectedScanners", 0); | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName, kSecondTestScannerName}); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 2u); | 
|  | histogram_tester.ExpectUniqueSample("Scanning.NumDetectedScanners", 2, 1); | 
|  | } | 
|  |  | 
|  | // Test that attempting to get capabilities with a scanner ID that doesn't | 
|  | // correspond to a scanner results in obtaining no capabilities. | 
|  | TEST_F(ScanServiceTest, BadScannerId) { | 
|  | auto caps = GetScannerCapabilities(base::UnguessableToken::Create()); | 
|  | EXPECT_TRUE(caps->sources.empty()); | 
|  | } | 
|  |  | 
|  | // Test that failing to obtain capabilities from the LorgnetteScannerManager | 
|  | // results in obtaining no capabilities. | 
|  | TEST_F(ScanServiceTest, NoCapabilities) { | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerCapabilitiesResponse( | 
|  | absl::nullopt); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  | auto caps = GetScannerCapabilities(scanners[0]->id); | 
|  | EXPECT_TRUE(caps->sources.empty()); | 
|  | } | 
|  |  | 
|  | // Test that scanner capabilities can be obtained successfully. | 
|  | TEST_F(ScanServiceTest, GetScannerCapabilities) { | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerCapabilitiesResponse( | 
|  | CreateLorgnetteScannerCapabilities()); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  | auto caps = GetScannerCapabilities(scanners[0]->id); | 
|  | ASSERT_EQ(caps->sources.size(), 1u); | 
|  | EXPECT_EQ(caps->sources[0]->type, mojo_ipc::SourceType::kFlatbed); | 
|  | EXPECT_EQ(caps->sources[0]->name, kDocumentSourceName); | 
|  | ASSERT_EQ(caps->sources[0]->color_modes.size(), 1u); | 
|  | EXPECT_EQ(caps->sources[0]->color_modes[0], mojo_ipc::ColorMode::kColor); | 
|  | ASSERT_EQ(caps->sources[0]->resolutions.size(), 2u); | 
|  | EXPECT_EQ(caps->sources[0]->resolutions[0], kFirstResolution); | 
|  | EXPECT_EQ(caps->sources[0]->resolutions[1], kSecondResolution); | 
|  | } | 
|  |  | 
|  | // Test that attempting to scan with a scanner ID that doesn't correspond to a | 
|  | // scanner results in a failed scan. | 
|  | TEST_F(ScanServiceTest, ScanWithBadScannerId) { | 
|  | EXPECT_FALSE(StartScan(base::UnguessableToken::Create(), | 
|  | mojo_ipc::ScanSettings::New())); | 
|  | } | 
|  |  | 
|  | // Test that attempting to scan with an unsupported file path fails. | 
|  | // Specifically, use a file path with directory navigation (e.g. "..") to verify | 
|  | // it can't be used to save scanned images to an unsupported path. | 
|  | TEST_F(ScanServiceTest, ScanWithUnsupportedFilePath) { | 
|  | const base::FilePath my_files_path(kMyFilesPath); | 
|  | SetupScanService(my_files_path, base::FilePath("/google/drive")); | 
|  |  | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | const std::vector<std::string> scan_data = {"TestData"}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(scan_data); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | const mojo_ipc::ScanSettings settings = CreateScanSettings( | 
|  | my_files_path.Append("../../../var/log"), mojo_ipc::FileType::kPng); | 
|  | EXPECT_FALSE(StartScan(scanners[0]->id, settings.Clone())); | 
|  | } | 
|  |  | 
|  | // Test that a scan can be performed successfully. | 
|  | TEST_F(ScanServiceTest, Scan) { | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | const std::vector<std::string> scan_data = {CreatePng(), CreatePng(), | 
|  | CreatePng()}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(scan_data); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | base::Time::Exploded scan_time; | 
|  | // Since we're using mock time, this is deterministic. | 
|  | base::Time::Now().LocalExplode(&scan_time); | 
|  |  | 
|  | base::HistogramTester histogram_tester; | 
|  | int num_single_file_scans = 0u; | 
|  | int num_multi_file_scans = 0u; | 
|  | for (int type_num = static_cast<int>(mojo_ipc::FileType::kMinValue); | 
|  | type_num <= static_cast<int>(mojo_ipc::FileType::kMaxValue); | 
|  | ++type_num) { | 
|  | auto type = static_cast<mojo_ipc::FileType>(type_num); | 
|  |  | 
|  | const std::vector<base::FilePath> saved_scan_paths = CreateSavedScanPaths( | 
|  | scanned_files_mount_->GetRootPath(), scan_time, type, scan_data.size()); | 
|  | for (const auto& saved_scan_path : saved_scan_paths) | 
|  | EXPECT_FALSE(base::PathExists(saved_scan_path)); | 
|  |  | 
|  | mojo_ipc::ScanSettings settings = | 
|  | CreateScanSettings(scanned_files_mount_->GetRootPath(), type); | 
|  | EXPECT_TRUE(StartScan(scanners[0]->id, settings.Clone())); | 
|  | for (const auto& saved_scan_path : saved_scan_paths) | 
|  | EXPECT_TRUE(base::PathExists(saved_scan_path)); | 
|  |  | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scan_success()); | 
|  | EXPECT_EQ(ProtoScanFailureMode::SCAN_FAILURE_MODE_NO_FAILURE, | 
|  | fake_scan_job_observer_.scan_result()); | 
|  | EXPECT_EQ(scan_data.size() - 1, fake_scan_job_observer_.new_page_index()); | 
|  | EXPECT_EQ(saved_scan_paths, fake_scan_job_observer_.scanned_file_paths()); | 
|  |  | 
|  | // Verify that the histograms have been updated correctly. | 
|  | histogram_tester.ExpectBucketCount( | 
|  | "Scanning.NumFilesCreated", saved_scan_paths.size(), | 
|  | type == mojo_ipc::FileType::kPdf ? ++num_single_file_scans | 
|  | : ++num_multi_file_scans); | 
|  | histogram_tester.ExpectUniqueSample( | 
|  | "Scanning.NumPagesScanned", scan_data.size(), | 
|  | num_single_file_scans + num_multi_file_scans); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Test that an Epson ADF Duplex scan, which produces flipped pages, completes | 
|  | // successfully. | 
|  | TEST_F(ScanServiceTest, RotateEpsonADF) { | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse({kEpsonTestName}); | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerCapabilitiesResponse( | 
|  | CreateEpsonScannerCapabilities()); | 
|  | const std::vector<std::string> scan_data = {CreatePng(), CreatePng(), | 
|  | CreatePng()}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(scan_data); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | base::Time::Exploded scan_time; | 
|  | // Since we're using mock time, this is deterministic. | 
|  | base::Time::Now().LocalExplode(&scan_time); | 
|  |  | 
|  | mojo_ipc::ScanSettings settings = | 
|  | CreateScanSettings(scanned_files_mount_->GetRootPath(), | 
|  | mojo_ipc::FileType::kPdf, "ADF Duplex"); | 
|  | const base::FilePath saved_scan_path = | 
|  | CreateSavedPdfScanPath(scanned_files_mount_->GetRootPath(), scan_time); | 
|  | EXPECT_FALSE(base::PathExists(saved_scan_path)); | 
|  | EXPECT_TRUE(StartScan(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_TRUE(base::PathExists(saved_scan_path)); | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scan_success()); | 
|  | const std::vector<base::FilePath> scanned_file_paths = | 
|  | fake_scan_job_observer_.scanned_file_paths(); | 
|  | EXPECT_EQ(1u, scanned_file_paths.size()); | 
|  | EXPECT_EQ(scan_data.size() - 1, fake_scan_job_observer_.new_page_index()); | 
|  | EXPECT_EQ(saved_scan_path, scanned_file_paths.front()); | 
|  | } | 
|  |  | 
|  | // Test that when a scan fails, the scan job is marked as failed. | 
|  | TEST_F(ScanServiceTest, ScanFails) { | 
|  | // Skip setting the scan data in FakeLorgnetteScannerManager so the scan will | 
|  | // fail. | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | const mojo_ipc::ScanSettings settings = CreateScanSettings( | 
|  | scanned_files_mount_->GetRootPath(), mojo_ipc::FileType::kPng); | 
|  |  | 
|  | EXPECT_TRUE(StartScan(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_FALSE(fake_scan_job_observer_.scan_success()); | 
|  | EXPECT_EQ(ProtoScanFailureMode::SCAN_FAILURE_MODE_DEVICE_BUSY, | 
|  | fake_scan_job_observer_.scan_result()); | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scanned_file_paths().empty()); | 
|  | } | 
|  |  | 
|  | // Tests that a new scan job can succeed after the previous scan failed. | 
|  | TEST_F(ScanServiceTest, ScanAfterFailedScan) { | 
|  | // Skip setting the scan data in FakeLorgnetteScannerManager so the scan will | 
|  | // fail. | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | const mojo_ipc::ScanSettings settings = CreateScanSettings( | 
|  | scanned_files_mount_->GetRootPath(), mojo_ipc::FileType::kPng); | 
|  |  | 
|  | EXPECT_TRUE(StartScan(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_FALSE(fake_scan_job_observer_.scan_success()); | 
|  | EXPECT_EQ(ProtoScanFailureMode::SCAN_FAILURE_MODE_DEVICE_BUSY, | 
|  | fake_scan_job_observer_.scan_result()); | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scanned_file_paths().empty()); | 
|  |  | 
|  | // Set scan data so next scan is successful. | 
|  | const std::vector<std::string> scan_data = {"TestData1", "TestData2", | 
|  | "TestData3"}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(scan_data); | 
|  | base::Time::Exploded scan_time; | 
|  | // Since we're using mock time, this is deterministic. | 
|  | base::Time::Now().LocalExplode(&scan_time); | 
|  |  | 
|  | const std::vector<base::FilePath> saved_scan_paths = | 
|  | CreateSavedScanPaths(scanned_files_mount_->GetRootPath(), scan_time, | 
|  | mojo_ipc::FileType::kPng, scan_data.size()); | 
|  | for (const auto& saved_scan_path : saved_scan_paths) | 
|  | EXPECT_FALSE(base::PathExists(saved_scan_path)); | 
|  |  | 
|  | EXPECT_TRUE(StartScan(scanners[0]->id, settings.Clone())); | 
|  | for (const auto& saved_scan_path : saved_scan_paths) | 
|  | EXPECT_TRUE(base::PathExists(saved_scan_path)); | 
|  |  | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scan_success()); | 
|  | EXPECT_EQ(ProtoScanFailureMode::SCAN_FAILURE_MODE_NO_FAILURE, | 
|  | fake_scan_job_observer_.scan_result()); | 
|  | EXPECT_EQ(saved_scan_paths, fake_scan_job_observer_.scanned_file_paths()); | 
|  | EXPECT_EQ(scan_data.size() - 1, fake_scan_job_observer_.new_page_index()); | 
|  | } | 
|  |  | 
|  | // Tests that a failed scan does not retain values from the previous successful | 
|  | // scan. | 
|  | TEST_F(ScanServiceTest, FailedScanAfterSuccessfulScan) { | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | const std::vector<std::string> scan_data = {"TestData1", "TestData2", | 
|  | "TestData3"}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(scan_data); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | base::Time::Exploded scan_time; | 
|  | // Since we're using mock time, this is deterministic. | 
|  | base::Time::Now().LocalExplode(&scan_time); | 
|  |  | 
|  | const mojo_ipc::ScanSettings settings = CreateScanSettings( | 
|  | scanned_files_mount_->GetRootPath(), mojo_ipc::FileType::kPng); | 
|  | const std::vector<base::FilePath> saved_scan_paths = | 
|  | CreateSavedScanPaths(scanned_files_mount_->GetRootPath(), scan_time, | 
|  | mojo_ipc::FileType::kPng, scan_data.size()); | 
|  | for (const auto& saved_scan_path : saved_scan_paths) | 
|  | EXPECT_FALSE(base::PathExists(saved_scan_path)); | 
|  |  | 
|  | EXPECT_TRUE(StartScan(scanners[0]->id, settings.Clone())); | 
|  | for (const auto& saved_scan_path : saved_scan_paths) | 
|  | EXPECT_TRUE(base::PathExists(saved_scan_path)); | 
|  |  | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scan_success()); | 
|  | EXPECT_EQ(ProtoScanFailureMode::SCAN_FAILURE_MODE_NO_FAILURE, | 
|  | fake_scan_job_observer_.scan_result()); | 
|  | EXPECT_EQ(saved_scan_paths, fake_scan_job_observer_.scanned_file_paths()); | 
|  | EXPECT_EQ(scan_data.size() - 1, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | // Remove the scan data from FakeLorgnetteScannerManager so the scan will | 
|  | // fail. | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse({}); | 
|  |  | 
|  | EXPECT_TRUE(StartScan(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_FALSE(fake_scan_job_observer_.scan_success()); | 
|  | EXPECT_EQ(ProtoScanFailureMode::SCAN_FAILURE_MODE_DEVICE_BUSY, | 
|  | fake_scan_job_observer_.scan_result()); | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scanned_file_paths().empty()); | 
|  | } | 
|  |  | 
|  | // Test that canceling sends an update to the observer OnCancelComplete(). | 
|  | TEST_F(ScanServiceTest, CancelScanBeforeScanCompletes) { | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | const std::vector<std::string> scan_data = {"TestData"}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(scan_data); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | const mojo_ipc::ScanSettings settings = CreateScanSettings( | 
|  | scanned_files_mount_->GetRootPath(), mojo_ipc::FileType::kPng); | 
|  |  | 
|  | StartScan(scanners[0]->id, settings.Clone()); | 
|  | CancelScan(); | 
|  | EXPECT_TRUE(fake_scan_job_observer_.cancel_scan_success()); | 
|  | } | 
|  |  | 
|  | // Test that a multi-page image scan creates a holding space item. | 
|  | TEST_F(ScanServiceTest, HoldingSpaceScan) { | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | const std::vector<std::string> scan_data = {CreatePng(), CreatePng(), | 
|  | CreatePng()}; | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | base::Time::Exploded scan_time; | 
|  | // Since we're using mock time, this is deterministic. | 
|  | base::Time::Now().LocalExplode(&scan_time); | 
|  |  | 
|  | // Verify that the holding space starts out empty. | 
|  | HoldingSpaceKeyedService* holding_space_keyed_service = | 
|  | HoldingSpaceKeyedServiceFactory::GetInstance()->GetService(profile_); | 
|  | ASSERT_TRUE(holding_space_keyed_service); | 
|  | const HoldingSpaceModel* holding_space_model = | 
|  | holding_space_keyed_service->model_for_testing(); | 
|  | ASSERT_TRUE(holding_space_model); | 
|  | size_t num_items_in_holding_space = 0u; | 
|  | ASSERT_EQ(num_items_in_holding_space, holding_space_model->items().size()); | 
|  |  | 
|  | for (int type_num = static_cast<int>(mojo_ipc::FileType::kMinValue); | 
|  | type_num <= static_cast<int>(mojo_ipc::FileType::kMaxValue); | 
|  | ++type_num) { | 
|  | auto type = static_cast<mojo_ipc::FileType>(type_num); | 
|  |  | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(scan_data); | 
|  | const std::vector<base::FilePath> saved_scan_paths = CreateSavedScanPaths( | 
|  | scanned_files_mount_->GetRootPath(), scan_time, type, scan_data.size()); | 
|  | for (const auto& saved_scan_path : saved_scan_paths) | 
|  | EXPECT_FALSE(base::PathExists(saved_scan_path)); | 
|  |  | 
|  | mojo_ipc::ScanSettings settings = | 
|  | CreateScanSettings(scanned_files_mount_->GetRootPath(), type); | 
|  | EXPECT_TRUE(StartScan(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scan_success()); | 
|  | EXPECT_EQ(ProtoScanFailureMode::SCAN_FAILURE_MODE_NO_FAILURE, | 
|  | fake_scan_job_observer_.scan_result()); | 
|  | EXPECT_EQ(saved_scan_paths, fake_scan_job_observer_.scanned_file_paths()); | 
|  |  | 
|  | // Verify that the all pages of each scan are added to the holding space. | 
|  | EXPECT_EQ(num_items_in_holding_space + saved_scan_paths.size(), | 
|  | holding_space_model->items().size()); | 
|  | for (const auto& saved_scan_path : saved_scan_paths) { | 
|  | EXPECT_TRUE(base::PathExists(saved_scan_path)); | 
|  | HoldingSpaceItem* scanned_item = | 
|  | holding_space_model->items()[num_items_in_holding_space++].get(); | 
|  | EXPECT_EQ(scanned_item->type(), HoldingSpaceItem::Type::kScan); | 
|  | EXPECT_EQ(scanned_item->file_path(), saved_scan_path); | 
|  | } | 
|  |  | 
|  | // Remove the scan data from FakeLorgnetteScannerManager so the scan will | 
|  | // fail. | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse({}); | 
|  |  | 
|  | EXPECT_TRUE(StartScan(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_FALSE(fake_scan_job_observer_.scan_success()); | 
|  | EXPECT_EQ(ProtoScanFailureMode::SCAN_FAILURE_MODE_DEVICE_BUSY, | 
|  | fake_scan_job_observer_.scan_result()); | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scanned_file_paths().empty()); | 
|  |  | 
|  | // Verify that no item is added to the holding space when a scan fails. | 
|  | EXPECT_EQ(num_items_in_holding_space, holding_space_model->items().size()); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Test that a multi-page scan can be performed successfully. | 
|  | TEST_F(ScanServiceTest, MultiPageScan) { | 
|  | base::HistogramTester histogram_tester; | 
|  |  | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | const std::vector<std::string> scan_data = {CreatePng()}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(scan_data); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | base::Time::Exploded scan_time; | 
|  | // Since we're using mock time, this is deterministic. | 
|  | base::Time::Now().LocalExplode(&scan_time); | 
|  | const std::vector<base::FilePath> saved_scan_paths = | 
|  | CreateSavedScanPaths(scanned_files_mount_->GetRootPath(), scan_time, | 
|  | mojo_ipc::FileType::kPdf, scan_data.size()); | 
|  | for (const auto& saved_scan_path : saved_scan_paths) | 
|  | EXPECT_FALSE(base::PathExists(saved_scan_path)); | 
|  |  | 
|  | mojo_ipc::ScanSettings settings = CreateScanSettings( | 
|  | scanned_files_mount_->GetRootPath(), mojo_ipc::FileType::kPdf); | 
|  | uint32_t new_page_index = 0; | 
|  |  | 
|  | // Scan the first page without completing the scan. | 
|  | EXPECT_TRUE(StartMultiPageScan(scanners[0]->id, settings.Clone())); | 
|  | for (const auto& saved_scan_path : saved_scan_paths) | 
|  | EXPECT_FALSE(base::PathExists(saved_scan_path)); | 
|  | EXPECT_FALSE(fake_scan_job_observer_.scan_success()); | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scanned_file_paths().empty()); | 
|  | EXPECT_EQ(new_page_index++, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | // Scan the second page without completing the scan. | 
|  | EXPECT_TRUE(ScanNextPage(scanners[0]->id, settings.Clone())); | 
|  | for (const auto& saved_scan_path : saved_scan_paths) | 
|  | EXPECT_FALSE(base::PathExists(saved_scan_path)); | 
|  | EXPECT_FALSE(fake_scan_job_observer_.scan_success()); | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scanned_file_paths().empty()); | 
|  | EXPECT_EQ(new_page_index++, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | // Complete the multi-page scan expecting 2 pages to be scanned and a single | 
|  | // PDF to be created. | 
|  | CompleteMultiPageScan(); | 
|  | for (const auto& saved_scan_path : saved_scan_paths) | 
|  | EXPECT_TRUE(base::PathExists(saved_scan_path)); | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scan_success()); | 
|  | EXPECT_EQ(saved_scan_paths, fake_scan_job_observer_.scanned_file_paths()); | 
|  | histogram_tester.ExpectUniqueSample("Scanning.NumPagesScanned", 2, 1); | 
|  | histogram_tester.ExpectUniqueSample("Scanning.MultiPageScan.NumPagesScanned", | 
|  | 2, 1); | 
|  | histogram_tester.ExpectUniqueSample("Scanning.MultiPageScan.PageScanResult", | 
|  | scanning::ScanJobFailureReason::kSuccess, | 
|  | 2); | 
|  | } | 
|  |  | 
|  | // Test that when a multi-page scan fails, the scan job is marked as failed. | 
|  | TEST_F(ScanServiceTest, MultiPageScanFails) { | 
|  | base::HistogramTester histogram_tester; | 
|  |  | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | const std::vector<std::string> scan_data = {CreatePng()}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(scan_data); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | mojo_ipc::ScanSettings settings = CreateScanSettings( | 
|  | scanned_files_mount_->GetRootPath(), mojo_ipc::FileType::kPdf); | 
|  |  | 
|  | // The first scan should pass with no failure. | 
|  | EXPECT_TRUE(StartMultiPageScan(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_FALSE(fake_scan_job_observer_.scan_success()); | 
|  | EXPECT_EQ(ProtoScanFailureMode::SCAN_FAILURE_MODE_UNKNOWN, | 
|  | fake_scan_job_observer_.multi_page_scan_result()); | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scanned_file_paths().empty()); | 
|  | EXPECT_EQ(0u, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | // Set scan data to empty vector in FakeLorgnetteScannerManager so the next | 
|  | // scan will fail. | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse({}); | 
|  | EXPECT_TRUE(ScanNextPage(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_FALSE(fake_scan_job_observer_.scan_success()); | 
|  | EXPECT_EQ(ProtoScanFailureMode::SCAN_FAILURE_MODE_DEVICE_BUSY, | 
|  | fake_scan_job_observer_.multi_page_scan_result()); | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scanned_file_paths().empty()); | 
|  |  | 
|  | histogram_tester.ExpectBucketCount("Scanning.MultiPageScan.PageScanResult", | 
|  | scanning::ScanJobFailureReason::kSuccess, | 
|  | 1); | 
|  | histogram_tester.ExpectBucketCount( | 
|  | "Scanning.MultiPageScan.PageScanResult", | 
|  | scanning::ScanJobFailureReason::kDeviceBusy, 1); | 
|  | } | 
|  |  | 
|  | // Test that attempting to start a second multi-page scan while another | 
|  | // multi-page scan session is going will fail. | 
|  | TEST_F(ScanServiceTest, StartingAnotherMultiPageScan) { | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | const std::vector<std::string> scan_data = {CreatePng()}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(scan_data); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | mojo_ipc::ScanSettings settings = CreateScanSettings( | 
|  | scanned_files_mount_->GetRootPath(), mojo_ipc::FileType::kPdf); | 
|  |  | 
|  | // The first scan should pass with no failure. | 
|  | EXPECT_TRUE(StartMultiPageScan(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_FALSE(fake_scan_job_observer_.scan_success()); | 
|  | EXPECT_EQ(ProtoScanFailureMode::SCAN_FAILURE_MODE_UNKNOWN, | 
|  | fake_scan_job_observer_.multi_page_scan_result()); | 
|  | EXPECT_TRUE(fake_scan_job_observer_.scanned_file_paths().empty()); | 
|  | EXPECT_EQ(0u, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | // The second attempt should fail. | 
|  | EXPECT_FALSE(StartMultiPageScan(scanners[0]->id, settings.Clone())); | 
|  | } | 
|  |  | 
|  | // Test that a page can be removed from a multi-page scan with two scanned | 
|  | // images. | 
|  | TEST_F(ScanServiceTest, MultiPageScanRemoveWithTwoPages) { | 
|  | base::HistogramTester histogram_tester; | 
|  |  | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | mojo_ipc::ScanSettings settings = CreateScanSettings( | 
|  | scanned_files_mount_->GetRootPath(), mojo_ipc::FileType::kPdf); | 
|  | uint32_t new_page_index = 0; | 
|  |  | 
|  | const std::string first_scanned_image = CreatePng(/*alpha=*/1); | 
|  | const std::vector<std::string> first_scan_data = {first_scanned_image}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(first_scan_data); | 
|  | EXPECT_TRUE(StartMultiPageScan(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_EQ(new_page_index++, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | const std::string second_scanned_image = CreatePng(/*alpha=*/2); | 
|  | const std::vector<std::string> second_scan_data = {second_scanned_image}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(second_scan_data); | 
|  | EXPECT_TRUE(ScanNextPage(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_EQ(new_page_index++, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | // Delete the first page. | 
|  | RemovePage(0); | 
|  | CompleteMultiPageScan(); | 
|  |  | 
|  | const std::vector<std::string> scanned_images = | 
|  | scan_service_->GetScannedImagesForTesting(); | 
|  | EXPECT_EQ(1u, scanned_images.size()); | 
|  | EXPECT_EQ(second_scanned_image, scanned_images[0]); | 
|  |  | 
|  | // Expect 1 record of the Scanning.NumPagesScanned metric in the 1 pages | 
|  | // scanned bucket. | 
|  | histogram_tester.ExpectUniqueSample("Scanning.NumPagesScanned", 1, 1); | 
|  | histogram_tester.ExpectUniqueSample("Scanning.MultiPageScan.NumPagesScanned", | 
|  | 1, 1); | 
|  | histogram_tester.ExpectUniqueSample( | 
|  | "Scanning.MultiPageScan.ToolbarAction", | 
|  | scanning::ScanMultiPageToolbarAction::kRemovePage, 1); | 
|  | } | 
|  |  | 
|  | // Test that a page can be removed from a multi-page scan with three scanned | 
|  | // images. | 
|  | TEST_F(ScanServiceTest, MultiPageScanRemoveWithThreePages) { | 
|  | base::HistogramTester histogram_tester; | 
|  |  | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | mojo_ipc::ScanSettings settings = CreateScanSettings( | 
|  | scanned_files_mount_->GetRootPath(), mojo_ipc::FileType::kPdf); | 
|  | uint32_t new_page_index = 0; | 
|  |  | 
|  | const std::string first_scanned_image = CreatePng(/*alpha=*/1); | 
|  | const std::vector<std::string> first_scan_data = {first_scanned_image}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(first_scan_data); | 
|  | EXPECT_TRUE(StartMultiPageScan(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_EQ(new_page_index++, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | const std::string second_scanned_image = CreatePng(/*alpha=*/2); | 
|  | const std::vector<std::string> second_scan_data = {second_scanned_image}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(second_scan_data); | 
|  | EXPECT_TRUE(ScanNextPage(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_EQ(new_page_index++, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | const std::string third_scanned_image = CreatePng(/*alpha=*/3); | 
|  | const std::vector<std::string> third_scan_data = {third_scanned_image}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(third_scan_data); | 
|  | EXPECT_TRUE(ScanNextPage(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_EQ(new_page_index++, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | // Delete the second page. | 
|  | RemovePage(1); | 
|  | CompleteMultiPageScan(); | 
|  |  | 
|  | const std::vector<std::string> scanned_images = | 
|  | scan_service_->GetScannedImagesForTesting(); | 
|  | EXPECT_EQ(2u, scanned_images.size()); | 
|  | EXPECT_EQ(first_scanned_image, scanned_images[0]); | 
|  | EXPECT_EQ(third_scanned_image, scanned_images[1]); | 
|  |  | 
|  | // Expect 1 record of the Scanning.NumPagesScanned metric in the 2 pages | 
|  | // scanned bucket. | 
|  | histogram_tester.ExpectUniqueSample("Scanning.NumPagesScanned", 2, 1); | 
|  | histogram_tester.ExpectUniqueSample("Scanning.MultiPageScan.NumPagesScanned", | 
|  | 2, 1); | 
|  | histogram_tester.ExpectUniqueSample( | 
|  | "Scanning.MultiPageScan.ToolbarAction", | 
|  | scanning::ScanMultiPageToolbarAction::kRemovePage, 1); | 
|  | } | 
|  |  | 
|  | // Test that if there's only one page available, the page is removed and the | 
|  | // multi-page scan session is reset and a new session can be started. | 
|  | TEST_F(ScanServiceTest, MultiPageScanRemoveLastPage) { | 
|  | base::HistogramTester histogram_tester; | 
|  |  | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | const std::vector<std::string> scan_data = {CreatePng()}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(scan_data); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | mojo_ipc::ScanSettings settings = CreateScanSettings( | 
|  | scanned_files_mount_->GetRootPath(), mojo_ipc::FileType::kPdf); | 
|  | uint32_t new_page_index = 0; | 
|  |  | 
|  | EXPECT_TRUE(StartMultiPageScan(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_EQ(new_page_index++, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | // Removing the page should reset the multi-page scan session. | 
|  | RemovePage(0); | 
|  | --new_page_index; | 
|  |  | 
|  | // Start a new scan and complete it with 1 page. | 
|  | ResetMultiPageScanControllerRemote(); | 
|  | EXPECT_TRUE(StartMultiPageScan(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_EQ(new_page_index++, fake_scan_job_observer_.new_page_index()); | 
|  | CompleteMultiPageScan(); | 
|  |  | 
|  | const std::vector<std::string> scanned_images = | 
|  | scan_service_->GetScannedImagesForTesting(); | 
|  | EXPECT_EQ(1u, scanned_images.size()); | 
|  |  | 
|  | // Expect 1 record of the Scanning.NumPagesScanned metric in the 1 page | 
|  | // scanned bucket. | 
|  | histogram_tester.ExpectUniqueSample("Scanning.NumPagesScanned", 1, 1); | 
|  | histogram_tester.ExpectUniqueSample("Scanning.MultiPageScan.NumPagesScanned", | 
|  | 1, 1); | 
|  | histogram_tester.ExpectUniqueSample( | 
|  | "Scanning.MultiPageScan.ToolbarAction", | 
|  | scanning::ScanMultiPageToolbarAction::kRemovePage, 1); | 
|  | } | 
|  |  | 
|  | // Test that a page can be rescanned and replaced from a multi-page scan with | 
|  | // one scanned image. | 
|  | TEST_F(ScanServiceTest, MultiPageScanRescanWithOnePage) { | 
|  | base::HistogramTester histogram_tester; | 
|  |  | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | mojo_ipc::ScanSettings settings = CreateScanSettings( | 
|  | scanned_files_mount_->GetRootPath(), mojo_ipc::FileType::kPdf); | 
|  | uint32_t new_page_index = 0; | 
|  |  | 
|  | const std::string first_scanned_image = CreatePng(/*alpha=*/1); | 
|  | const std::vector<std::string> first_scan_data = {first_scanned_image}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(first_scan_data); | 
|  | EXPECT_TRUE(StartMultiPageScan(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_EQ(new_page_index++, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | // Rescan the page. | 
|  | const std::string rescanned_scanned_image = CreatePng(/*alpha=*/2); | 
|  | const std::vector<std::string> rescanned_scan_data = { | 
|  | rescanned_scanned_image}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(rescanned_scan_data); | 
|  | EXPECT_TRUE(RescanPage(scanners[0]->id, settings.Clone(), /*page_index=*/0)); | 
|  | EXPECT_EQ(0u, fake_scan_job_observer_.new_page_index()); | 
|  | CompleteMultiPageScan(); | 
|  |  | 
|  | const std::vector<std::string> scanned_images = | 
|  | scan_service_->GetScannedImagesForTesting(); | 
|  | EXPECT_EQ(1u, scanned_images.size()); | 
|  | EXPECT_EQ(rescanned_scanned_image, scanned_images[0]); | 
|  |  | 
|  | // Expect 1 record of the Scanning.NumPagesScanned metric in the 1 pages | 
|  | // scanned bucket. | 
|  | histogram_tester.ExpectUniqueSample("Scanning.NumPagesScanned", 1, 1); | 
|  | histogram_tester.ExpectUniqueSample("Scanning.MultiPageScan.NumPagesScanned", | 
|  | 1, 1); | 
|  | histogram_tester.ExpectUniqueSample( | 
|  | "Scanning.MultiPageScan.ToolbarAction", | 
|  | scanning::ScanMultiPageToolbarAction::kRescanPage, 1); | 
|  | } | 
|  |  | 
|  | // Test that a page can be rescanned and replaced from a multi-page scan with | 
|  | // three scanned images. | 
|  | TEST_F(ScanServiceTest, MultiPageScanRescanWithThreePages) { | 
|  | base::HistogramTester histogram_tester; | 
|  |  | 
|  | fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse( | 
|  | {kFirstTestScannerName}); | 
|  | auto scanners = GetScanners(); | 
|  | ASSERT_EQ(scanners.size(), 1u); | 
|  |  | 
|  | mojo_ipc::ScanSettings settings = CreateScanSettings( | 
|  | scanned_files_mount_->GetRootPath(), mojo_ipc::FileType::kPdf); | 
|  | uint32_t new_page_index = 0; | 
|  |  | 
|  | const std::string first_scanned_image = CreatePng(/*alpha=*/1); | 
|  | const std::vector<std::string> first_scan_data = {first_scanned_image}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(first_scan_data); | 
|  | EXPECT_TRUE(StartMultiPageScan(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_EQ(new_page_index++, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | const std::string second_scanned_image = CreatePng(/*alpha=*/2); | 
|  | const std::vector<std::string> second_scan_data = {second_scanned_image}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(second_scan_data); | 
|  | EXPECT_TRUE(ScanNextPage(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_EQ(new_page_index++, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | const std::string third_scanned_image = CreatePng(/*alpha=*/3); | 
|  | const std::vector<std::string> third_scan_data = {third_scanned_image}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(third_scan_data); | 
|  | EXPECT_TRUE(ScanNextPage(scanners[0]->id, settings.Clone())); | 
|  | EXPECT_EQ(new_page_index++, fake_scan_job_observer_.new_page_index()); | 
|  |  | 
|  | // Rescan the second page. | 
|  | const std::string rescanned_scanned_image = CreatePng(/*alpha=*/4); | 
|  | const std::vector<std::string> rescanned_scan_data = { | 
|  | rescanned_scanned_image}; | 
|  | fake_lorgnette_scanner_manager_.SetScanResponse(rescanned_scan_data); | 
|  | EXPECT_TRUE(RescanPage(scanners[0]->id, settings.Clone(), /*page_index=*/1)); | 
|  | EXPECT_EQ(1u, fake_scan_job_observer_.new_page_index()); | 
|  | CompleteMultiPageScan(); | 
|  |  | 
|  | const std::vector<std::string> scanned_images = | 
|  | scan_service_->GetScannedImagesForTesting(); | 
|  | EXPECT_EQ(3u, scanned_images.size()); | 
|  | EXPECT_EQ(first_scanned_image, scanned_images[0]); | 
|  | EXPECT_EQ(rescanned_scanned_image, scanned_images[1]); | 
|  | EXPECT_EQ(third_scanned_image, scanned_images[2]); | 
|  |  | 
|  | // Expect 1 record of the Scanning.NumPagesScanned metric in the 3 pages | 
|  | // scanned bucket. | 
|  | histogram_tester.ExpectUniqueSample("Scanning.NumPagesScanned", 3, 1); | 
|  | histogram_tester.ExpectUniqueSample("Scanning.MultiPageScan.NumPagesScanned", | 
|  | 3, 1); | 
|  | histogram_tester.ExpectUniqueSample( | 
|  | "Scanning.MultiPageScan.ToolbarAction", | 
|  | scanning::ScanMultiPageToolbarAction::kRescanPage, 1); | 
|  | } | 
|  |  | 
|  | TEST_F(ScanServiceTest, ResetReceiverOnBindInterface) { | 
|  | // This test simulates a user refreshing the WebUI page. The receiver should | 
|  | // be reset before binding the new receiver. Otherwise we would get a DCHECK | 
|  | // error from mojo::Receiver | 
|  | mojo::Remote<scanning::mojom::ScanService> remote; | 
|  | scan_service_->BindInterface(remote.BindNewPipeAndPassReceiver()); | 
|  | base::RunLoop().RunUntilIdle(); | 
|  |  | 
|  | remote.reset(); | 
|  |  | 
|  | scan_service_->BindInterface(remote.BindNewPipeAndPassReceiver()); | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | } | 
|  | }  // namespace ash |