// Copyright 2023 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/extensions/api/document_scan/fake_document_scan_ash.h"
#include <utility>
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "chrome/browser/extensions/api/document_scan/document_scan_test_utils.h"
namespace extensions {
FakeDocumentScanAsh::FakeDocumentScanAsh() = default;
FakeDocumentScanAsh::~FakeDocumentScanAsh() = default;
FakeDocumentScanAsh::OpenScannerState::OpenScannerState() = default;
FakeDocumentScanAsh::OpenScannerState::~OpenScannerState() = default;
FakeDocumentScanAsh::OpenScannerState::OpenScannerState(
const std::string& client_id,
const std::string& connection_string)
: client_id(client_id), connection_string(connection_string) {}
void FakeDocumentScanAsh::GetScannerNames(GetScannerNamesCallback callback) {
std::move(callback).Run(scanner_names_);
}
void FakeDocumentScanAsh::ScanFirstPage(const std::string& scanner_name,
ScanFirstPageCallback callback) {
if (scan_data_.has_value()) {
std::move(callback).Run(crosapi::mojom::ScanFailureMode::kNoFailure,
scan_data_.value()[0]);
} else {
std::move(callback).Run(crosapi::mojom::ScanFailureMode::kDeviceBusy,
std::nullopt);
}
}
void FakeDocumentScanAsh::GetScannerList(
const std::string& client_id,
crosapi::mojom::ScannerEnumFilterPtr filter,
GetScannerListCallback callback) {
auto response = crosapi::mojom::GetScannerListResponse::New();
response->result = crosapi::mojom::ScannerOperationResult::kSuccess;
for (const auto& scanner : scanners_) {
response->scanners.emplace_back(scanner.Clone());
// Since this scanner will be listed, also create an entry that allows
// callers to open it.
auto open_response = crosapi::mojom::OpenScannerResponse::New();
open_response->result = crosapi::mojom::ScannerOperationResult::kSuccess;
open_response->scanner_id = scanner->id;
open_response->scanner_handle = scanner->id + "-handle-" + client_id;
open_response->options.emplace();
open_response->options.value()["option1"] =
CreateTestScannerOption("option1", 5);
open_responses_[scanner->id] = std::move(open_response);
}
std::move(callback).Run(std::move(response));
}
void FakeDocumentScanAsh::OpenScanner(const std::string& client_id,
const std::string& scanner_id,
OpenScannerCallback callback) {
// If a response for scanner_id hasn't been set, this is the equivalent
// of trying to open a device that has been unplugged or disappeared off the
// network.
if (!base::Contains(open_responses_, scanner_id)) {
auto response = crosapi::mojom::OpenScannerResponse::New();
response->scanner_id = scanner_id;
response->result = crosapi::mojom::ScannerOperationResult::kDeviceMissing;
std::move(callback).Run(std::move(response));
return;
}
// If the scanner is already open by a different client, the real backend will
// report DEVICE_BUSY to any other clients trying to open it. Do the same
// here.
for (const auto& [handle, original] : open_scanners_) {
if (original.connection_string == scanner_id &&
original.client_id != client_id) {
auto response = crosapi::mojom::OpenScannerResponse::New();
response->scanner_id = scanner_id;
response->result = crosapi::mojom::ScannerOperationResult::kDeviceBusy;
std::move(callback).Run(std::move(response));
return;
}
}
crosapi::mojom::OpenScannerResponsePtr response =
open_responses_[scanner_id].Clone();
response->scanner_handle =
response->scanner_handle.value_or(scanner_id + "-handle") +
base::StringPrintf("%03zu", ++handle_count_);
open_scanners_[response->scanner_handle.value()] =
OpenScannerState(client_id, scanner_id);
std::move(callback).Run(std::move(response));
}
void FakeDocumentScanAsh::GetOptionGroups(const std::string& scanner_handle,
GetOptionGroupsCallback callback) {
if (!base::Contains(open_scanners_, scanner_handle)) {
auto response = crosapi::mojom::GetOptionGroupsResponse::New();
response->scanner_handle = scanner_handle;
response->result = crosapi::mojom::ScannerOperationResult::kInvalid;
std::move(callback).Run(std::move(response));
return;
}
// The API handler just passes through responses from this function, so always
// returning a hardcoded value shouldn't matter.
auto response = crosapi::mojom::GetOptionGroupsResponse::New();
response->result = crosapi::mojom::ScannerOperationResult::kSuccess;
response->scanner_handle = scanner_handle;
response->groups.emplace();
auto option_group = crosapi::mojom::OptionGroup::New();
option_group->title = "title";
option_group->members.emplace_back("item1");
option_group->members.emplace_back("item2");
response->groups->emplace_back(std::move(option_group));
std::move(callback).Run(std::move(response));
}
void FakeDocumentScanAsh::CloseScanner(const std::string& scanner_handle,
CloseScannerCallback callback) {
auto response = crosapi::mojom::CloseScannerResponse::New();
response->scanner_handle = scanner_handle;
if (base::Contains(open_scanners_, scanner_handle)) {
response->result = crosapi::mojom::ScannerOperationResult::kSuccess;
} else {
response->result = crosapi::mojom::ScannerOperationResult::kInvalid;
}
open_scanners_.erase(scanner_handle);
std::move(callback).Run(std::move(response));
}
void FakeDocumentScanAsh::StartPreparedScan(
const std::string& scanner_handle,
crosapi::mojom::StartScanOptionsPtr options,
StartPreparedScanCallback callback) {
if (!base::Contains(open_scanners_, scanner_handle)) {
auto response = crosapi::mojom::StartPreparedScanResponse::New();
response->scanner_handle = scanner_handle;
response->result = crosapi::mojom::ScannerOperationResult::kInvalid;
std::move(callback).Run(std::move(response));
return;
}
auto response = crosapi::mojom::StartPreparedScanResponse::New();
response->scanner_handle = scanner_handle;
if (options->max_read_size.has_value() &&
options->max_read_size.value() < smallest_max_read_) {
response->result = crosapi::mojom::ScannerOperationResult::kInvalid;
std::move(callback).Run(std::move(response));
return;
}
response->result = crosapi::mojom::ScannerOperationResult::kSuccess;
response->job_handle = base::StringPrintf(
"%s-job-%03zu", scanner_handle.c_str(), ++handle_count_);
open_scanners_.at(scanner_handle).job_handle = response->job_handle;
std::move(callback).Run(std::move(response));
}
void FakeDocumentScanAsh::ReadScanData(const std::string& job_handle,
ReadScanDataCallback callback) {
// The API handler just passes through responses from this function, so always
// returning a hardcoded value for valid job handles shouldn't matter. For
// invalid job handles, report them as cancelled.
auto response = crosapi::mojom::ReadScanDataResponse::New();
response->job_handle = job_handle;
response->result = crosapi::mojom::ScannerOperationResult::kCancelled;
for (auto& [scanner_handle, state] : open_scanners_) {
if (state.job_handle.value_or("") == job_handle) {
response->result = crosapi::mojom::ScannerOperationResult::kSuccess;
response->data.emplace(std::vector<int8_t>{'i', 'm', 'g'});
response->estimated_completion = 12;
break;
}
}
std::move(callback).Run(std::move(response));
}
void FakeDocumentScanAsh::SetOptions(
const std::string& scanner_handle,
std::vector<crosapi::mojom::OptionSettingPtr> options,
SetOptionsCallback callback) {
auto response = crosapi::mojom::SetOptionsResponse::New();
response->scanner_handle = scanner_handle;
response->results.reserve(options.size());
if (!base::Contains(open_scanners_, scanner_handle)) {
for (const auto& setting : options) {
response->results.emplace_back(crosapi::mojom::SetOptionResult::New(
setting->name,
crosapi::mojom::ScannerOperationResult::kDeviceMissing));
}
std::move(callback).Run(std::move(response));
return;
}
// Fake setting options by copying and overriding the original config that
// would have been returned for this scanner.
const auto& open_response =
open_responses_[open_scanners_[scanner_handle].connection_string];
if (!open_response->options.has_value()) {
for (const auto& setting : options) {
response->results.emplace_back(crosapi::mojom::SetOptionResult::New(
setting->name,
crosapi::mojom::ScannerOperationResult::kInternalError));
}
std::move(callback).Run(std::move(response));
return;
}
response->options.emplace();
response->options->reserve(open_response->options->size());
for (const auto& [name, option] : open_response->options.value()) {
response->options->try_emplace(name, option.Clone());
}
for (const auto& setting : options) {
auto result = crosapi::mojom::SetOptionResult::New();
result->name = setting->name;
// Ensure the returned options contains the requested option so that callers
// can look up the value. The real backend doesn't behave this way, but
// this avoids a ton of boilerplate in tests without changing the handler
// code coverage that can be achieved with the fake.
if (!base::Contains(response->options.value(), setting->name)) {
auto option = crosapi::mojom::ScannerOption::New();
option->name = setting->name;
option->type = setting->type;
response->options->try_emplace(setting->name, std::move(option));
}
if (setting->value.is_null()) {
result->result = crosapi::mojom::ScannerOperationResult::kSuccess;
} else {
// If there's a value, make sure the value type matches the option type.
// The real backend does a lot more validation, but other cases are
// handled as pass-through, so there's no need to implement everything in
// this fake.
switch (setting->type) {
case crosapi::mojom::OptionType::kBool:
if (setting->value->is_bool_value()) {
result->result = crosapi::mojom::ScannerOperationResult::kSuccess;
response->options->at(setting->name)->value =
crosapi::mojom::OptionValue::NewBoolValue(
setting->value->get_bool_value());
} else {
result->result = crosapi::mojom::ScannerOperationResult::kWrongType;
}
break;
case crosapi::mojom::OptionType::kInt:
if (setting->value->is_int_value()) {
result->result = crosapi::mojom::ScannerOperationResult::kSuccess;
response->options->at(setting->name)->value =
crosapi::mojom::OptionValue::NewIntValue(
setting->value->get_int_value());
} else if (setting->value->is_int_list()) {
result->result = crosapi::mojom::ScannerOperationResult::kSuccess;
response->options->at(setting->name)->value =
crosapi::mojom::OptionValue::NewIntList(
{setting->value->get_int_list().begin(),
setting->value->get_int_list().end()});
} else {
result->result = crosapi::mojom::ScannerOperationResult::kWrongType;
}
break;
case crosapi::mojom::OptionType::kFixed:
if (setting->value->is_fixed_value()) {
result->result = crosapi::mojom::ScannerOperationResult::kSuccess;
response->options->at(setting->name)->value =
crosapi::mojom::OptionValue::NewFixedValue(
setting->value->get_fixed_value());
} else if (setting->value->is_fixed_list()) {
result->result = crosapi::mojom::ScannerOperationResult::kSuccess;
response->options->at(setting->name)->value =
crosapi::mojom::OptionValue::NewFixedList(
{setting->value->get_fixed_list().begin(),
setting->value->get_fixed_list().end()});
} else {
result->result = crosapi::mojom::ScannerOperationResult::kWrongType;
}
break;
case crosapi::mojom::OptionType::kString:
if (setting->value->is_string_value()) {
result->result = crosapi::mojom::ScannerOperationResult::kSuccess;
response->options->at(setting->name)->value =
crosapi::mojom::OptionValue::NewStringValue(
setting->value->get_string_value());
} else {
result->result = crosapi::mojom::ScannerOperationResult::kWrongType;
}
break;
default:
// Claim it succeeded, but don't update the returned option value.
// This is a valid outcome for a real scanner, so the frontend has to
// account for it, anyway.
result->result = crosapi::mojom::ScannerOperationResult::kSuccess;
break;
}
}
response->results.emplace_back(std::move(result));
}
std::move(callback).Run(std::move(response));
}
void FakeDocumentScanAsh::CancelScan(const std::string& job_handle,
CancelScanCallback callback) {
auto response = crosapi::mojom::CancelScanResponse::New();
response->job_handle = job_handle;
// Explicitly set this to kAdfJammed instead of kInvalid since kAdfJammed is
// not used in the DocumentScanAPIHandler cancel methods. If this was
// kInvalid the tests may not know if the kInvalid was returned from this fake
// (in which case, the test may not be testing what it is intended to test) or
// was returned from the DocumentScanAPIHandler object (as expected).
response->result = crosapi::mojom::ScannerOperationResult::kAdfJammed;
// Check all of our open scanners. If any has this job, cancel it and return
// a success result. If not, return a failure result.
for (auto& [scanner_handle, state] : open_scanners_) {
if (state.job_handle.value_or("") == job_handle) {
response->result = crosapi::mojom::ScannerOperationResult::kSuccess;
state.job_handle.reset();
break;
}
}
std::move(callback).Run(std::move(response));
}
void FakeDocumentScanAsh::SetGetScannerNamesResponse(
std::vector<std::string> scanner_names) {
scanner_names_ = std::move(scanner_names);
}
void FakeDocumentScanAsh::SetScanResponse(
const std::optional<std::vector<std::string>>& scan_data) {
if (scan_data.has_value()) {
DCHECK(!scan_data.value().empty());
}
scan_data_ = scan_data;
}
void FakeDocumentScanAsh::AddScanner(crosapi::mojom::ScannerInfoPtr scanner) {
scanners_.emplace_back(std::move(scanner));
}
void FakeDocumentScanAsh::SetOpenScannerResponse(
const std::string& connection_string,
crosapi::mojom::OpenScannerResponsePtr response) {
open_responses_[connection_string] = std::move(response);
}
void FakeDocumentScanAsh::SetSmallestMaxReadSize(size_t max_size) {
smallest_max_read_ = max_size;
}
} // namespace extensions