blob: 0810c3f09359a50a0bbc4de85c6b0020a676d6d3 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/scanning/scan_service.h"
#include <cstdint>
#include <map>
#include <string>
#include <vector>
#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/optional.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "chrome/browser/chromeos/scanning/fake_lorgnette_scanner_manager.h"
#include "chromeos/components/scanning/mojom/scanning.mojom-test-utils.h"
#include "chromeos/components/scanning/mojom/scanning.mojom.h"
#include "chromeos/dbus/lorgnette/lorgnette_service.pb.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/skia/include/core/SkBitmap.h"
#include "ui/gfx/codec/png_codec.h"
namespace chromeos {
namespace {
namespace mojo_ipc = scanning::mojom;
// 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 char kSecondTestScannerName[] = "Test Scanner 2";
// Document source name used for tests.
constexpr char kDocumentSourceName[] = "Flatbed";
// Resolutions used for tests.
constexpr uint32_t kFirstResolution = 75;
constexpr uint32_t kSecondResolution = 300;
// Returns a DocumentSource object.
lorgnette::DocumentSource CreateLorgnetteDocumentSource() {
lorgnette::DocumentSource source;
source.set_type(lorgnette::SOURCE_PLATEN);
source.set_name(kDocumentSourceName);
return source;
}
// Returns a ScannerCapabilities object.
lorgnette::ScannerCapabilities CreateLorgnetteScannerCapabilities() {
lorgnette::ScannerCapabilities caps;
*caps.add_sources() = CreateLorgnetteDocumentSource();
caps.add_color_modes(lorgnette::MODE_COLOR);
caps.add_resolutions(kFirstResolution);
caps.add_resolutions(kSecondResolution);
return caps;
}
// 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 std::string& type,
int num_pages_to_scan) {
std::vector<base::FilePath> file_paths;
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, type.c_str())));
}
return file_paths;
}
// 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 manually generated PNG image.
std::string CreatePng() {
SkBitmap bitmap;
bitmap.allocN32Pixels(100, 100);
bitmap.eraseARGB(255, 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) {
mojo_ipc::ScanSettings settings;
settings.scan_to_path = scan_to_path;
settings.file_type = file_type;
return settings;
}
} // 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) override {
page_complete_ = true;
}
void OnScanComplete(bool success,
const base::FilePath& last_scanned_file_path) override {
scan_success_ = success;
last_scanned_file_path_ = last_scanned_file_path;
}
void OnCancelComplete(bool success) override {
cancel_scan_success_ = success;
}
// 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_success_;
}
// Returns true if the cancel scan request completed successfully.
bool cancel_scan_success() const { return cancel_scan_success_; }
// Returns the file path of the file saved last.
base::FilePath last_scanned_file_path() const {
return last_scanned_file_path_;
}
private:
uint32_t progress_ = 0;
bool page_complete_ = false;
bool scan_success_ = false;
bool cancel_scan_success_ = false;
base::FilePath last_scanned_file_path_;
mojo::Receiver<mojo_ipc::ScanJobObserver> receiver_{this};
};
class ScanServiceTest : public testing::Test {
public:
ScanServiceTest() = default;
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
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;
}
// Performs a cancel scan request.
void CancelScan() {
scan_service_remote_->CancelScan();
task_environment_.RunUntilIdle();
}
protected:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
base::ScopedTempDir temp_dir_;
FakeLorgnetteScannerManager fake_lorgnette_scanner_manager_;
FakeScanJobObserver fake_scan_job_observer_;
ScanService scan_service_{&fake_lorgnette_scanner_manager_, base::FilePath(),
base::FilePath()};
private:
mojo::Remote<mojo_ipc::ScanService> scan_service_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,
base::UTF8ToUTF16(kFirstTestScannerName));
}
// 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,
base::UTF8ToUTF16(kFirstTestScannerName));
EXPECT_EQ(scanners[1]->display_name,
base::UTF8ToUTF16(kSecondTestScannerName));
EXPECT_NE(scanners[0]->id, scanners[1]->id);
}
// 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());
EXPECT_TRUE(caps->color_modes.empty());
EXPECT_TRUE(caps->resolutions.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(
base::nullopt);
auto scanners = GetScanners();
ASSERT_EQ(scanners.size(), 1u);
auto caps = GetScannerCapabilities(scanners[0]->id);
EXPECT_TRUE(caps->sources.empty());
EXPECT_TRUE(caps->color_modes.empty());
EXPECT_TRUE(caps->resolutions.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->color_modes.size(), 1u);
EXPECT_EQ(caps->color_modes[0], mojo_ipc::ColorMode::kColor);
ASSERT_EQ(caps->resolutions.size(), 2u);
EXPECT_EQ(caps->resolutions[0], kFirstResolution);
EXPECT_EQ(caps->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) {
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 base::FilePath my_files_path(kMyFilesPath);
scan_service_.SetMyFilesPathForTesting(my_files_path);
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);
scan_service_.SetMyFilesPathForTesting(temp_dir_.GetPath());
std::map<std::string, mojo_ipc::FileType> file_types = {
{"png", mojo_ipc::FileType::kPng}, {"jpg", mojo_ipc::FileType::kJpg}};
for (const auto& type : file_types) {
const std::vector<base::FilePath> saved_scan_paths = CreateSavedScanPaths(
temp_dir_.GetPath(), scan_time, type.first, scan_data.size());
for (const auto& saved_scan_path : saved_scan_paths)
EXPECT_FALSE(base::PathExists(saved_scan_path));
mojo_ipc::ScanSettings settings =
CreateScanSettings(temp_dir_.GetPath(), type.second);
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(saved_scan_paths.back(),
fake_scan_job_observer_.last_scanned_file_path());
}
}
// Test that a scan with PDF file format can be perfomed successfully.
TEST_F(ScanServiceTest, PdfScan) {
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);
scan_service_.SetMyFilesPathForTesting(temp_dir_.GetPath());
mojo_ipc::ScanSettings settings =
CreateScanSettings(temp_dir_.GetPath(), mojo_ipc::FileType::kPdf);
const base::FilePath saved_scan_path =
CreateSavedPdfScanPath(temp_dir_.GetPath(), 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());
EXPECT_EQ(saved_scan_path, fake_scan_job_observer_.last_scanned_file_path());
}
// 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);
scan_service_.SetMyFilesPathForTesting(temp_dir_.GetPath());
const mojo_ipc::ScanSettings settings =
CreateScanSettings(temp_dir_.GetPath(), mojo_ipc::FileType::kPng);
EXPECT_TRUE(StartScan(scanners[0]->id, settings.Clone()));
EXPECT_FALSE(fake_scan_job_observer_.scan_success());
EXPECT_TRUE(fake_scan_job_observer_.last_scanned_file_path().empty());
}
// Test that when a page fails to save during the scan, the scan job is marked
// as failed.
TEST_F(ScanServiceTest, PageSaveFails) {
fake_lorgnette_scanner_manager_.SetGetScannerNamesResponse(
{kFirstTestScannerName});
// Sending an empty string in test data simulates a page saving to fail.
const std::vector<std::string> scan_data = {"TestData1", "", "TestData3"};
fake_lorgnette_scanner_manager_.SetScanResponse(scan_data);
auto scanners = GetScanners();
ASSERT_EQ(scanners.size(), 1u);
scan_service_.SetMyFilesPathForTesting(temp_dir_.GetPath());
const mojo_ipc::ScanSettings settings =
CreateScanSettings(temp_dir_.GetPath(), mojo_ipc::FileType::kJpg);
EXPECT_TRUE(StartScan(scanners[0]->id, settings.Clone()));
EXPECT_FALSE(fake_scan_job_observer_.scan_success());
EXPECT_TRUE(fake_scan_job_observer_.last_scanned_file_path().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);
scan_service_.SetMyFilesPathForTesting(temp_dir_.GetPath());
const mojo_ipc::ScanSettings settings =
CreateScanSettings(temp_dir_.GetPath(), mojo_ipc::FileType::kPng);
EXPECT_TRUE(StartScan(scanners[0]->id, settings.Clone()));
EXPECT_FALSE(fake_scan_job_observer_.scan_success());
EXPECT_TRUE(fake_scan_job_observer_.last_scanned_file_path().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(
temp_dir_.GetPath(), scan_time, "png", 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(saved_scan_paths.back(),
fake_scan_job_observer_.last_scanned_file_path());
}
// 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);
scan_service_.SetMyFilesPathForTesting(temp_dir_.GetPath());
const mojo_ipc::ScanSettings settings =
CreateScanSettings(temp_dir_.GetPath(), mojo_ipc::FileType::kPng);
const std::vector<base::FilePath> saved_scan_paths = CreateSavedScanPaths(
temp_dir_.GetPath(), scan_time, "png", 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(saved_scan_paths.back(),
fake_scan_job_observer_.last_scanned_file_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_TRUE(fake_scan_job_observer_.last_scanned_file_path().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);
scan_service_.SetMyFilesPathForTesting(temp_dir_.GetPath());
const mojo_ipc::ScanSettings settings =
CreateScanSettings(temp_dir_.GetPath(), mojo_ipc::FileType::kPng);
StartScan(scanners[0]->id, settings.Clone());
CancelScan();
EXPECT_TRUE(fake_scan_job_observer_.cancel_scan_success());
}
} // namespace chromeos