blob: 373880501dab80cb2c81163f2c741b70c553f74e [file] [log] [blame]
// 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 "components/payments/content/secure_payment_confirmation_app.h"
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/memory/raw_ptr.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/gmock_move_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "components/payments/content/browser_binding/fake_browser_bound_key.h"
#include "components/payments/content/browser_binding/fake_browser_bound_key_store.h"
#include "components/payments/content/browser_binding/passkey_browser_binder.h"
#include "components/payments/content/mock_web_payments_web_data_service.h"
#include "components/payments/content/payment_request_spec.h"
#include "components/payments/core/features.h"
#include "components/payments/core/method_strings.h"
#include "components/payments/core/secure_payment_confirmation_metrics.h"
#include "components/webauthn/core/browser/mock_internal_authenticator.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/test_web_contents_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/payments/payment_request.mojom.h"
#include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h"
#include "url/origin.h"
namespace payments {
namespace {
// Arbitrary change.
using ::base::test::RunOnceCallback;
using ::testing::_;
using ::testing::DoAll;
using ::testing::ElementsAreArray;
using ::testing::Eq;
using ::testing::Field;
using ::testing::IsEmpty;
using ::testing::Matcher;
using ::testing::Optional;
using ::testing::Pointer;
using ::testing::Property;
using ::testing::Return;
using ::testing::SaveArg;
#if BUILDFLAG(IS_ANDROID)
static constexpr char kAlgorithmIdentifier = 1;
#endif // BUILDFLAG(IS_ANDROID)
static constexpr char kChallengeBase64[] = "aaaa";
static constexpr char kCredentialIdBase64[] = "cccc";
class SecurePaymentConfirmationAppTest : public testing::Test,
public PaymentApp::Delegate {
protected:
SecurePaymentConfirmationAppTest() {
mojom::PaymentDetailsPtr details = mojom::PaymentDetails::New();
details->total = mojom::PaymentItem::New();
details->total->amount = mojom::PaymentCurrencyAmount::New();
details->total->amount->currency = "USD";
details->total->amount->value = "1.25";
std::vector<mojom::PaymentMethodDataPtr> method_data;
spec_ = std::make_unique<PaymentRequestSpec>(
mojom::PaymentOptions::New(), std::move(details),
std::move(method_data), /*observer=*/nullptr, /*app_locale=*/"en-US");
}
void SetUp() override {
ASSERT_TRUE(base::Base64Decode(kChallengeBase64, &challenge_bytes_));
ASSERT_TRUE(base::Base64Decode(kCredentialIdBase64, &credential_id_bytes_));
}
mojom::SecurePaymentConfirmationRequestPtr MakeRequest(
std::optional<
std::vector<device::PublicKeyCredentialParams::CredentialInfo>>
credential_parameters = std::nullopt) {
auto request = mojom::SecurePaymentConfirmationRequest::New();
request->challenge =
std::vector<uint8_t>(challenge_bytes_.begin(), challenge_bytes_.end());
if (credential_parameters) {
request->browser_bound_pub_key_cred_params =
std::move(*credential_parameters);
}
request->instrument = blink::mojom::PaymentCredentialInstrument::New();
return request;
}
// PaymentApp::Delegate:
void OnInstrumentDetailsReady(const std::string& method_name,
const std::string& stringified_details,
const PayerData& payer_data) override {
EXPECT_EQ(method_name, methods::kSecurePaymentConfirmation);
EXPECT_EQ(stringified_details, "{}");
EXPECT_EQ(payer_data.payer_name, "");
EXPECT_EQ(payer_data.payer_email, "");
EXPECT_EQ(payer_data.payer_phone, "");
EXPECT_TRUE(payer_data.shipping_address.is_null());
EXPECT_EQ(payer_data.selected_shipping_option_id, "");
on_instrument_details_ready_called_ = true;
}
void OnInstrumentDetailsError(const std::string& error_message) override {
EXPECT_EQ(error_message,
"The operation either timed out or was not allowed. See: "
"https://www.w3.org/TR/webauthn-2/"
"#sctn-privacy-considerations-client.");
on_instrument_details_error_called_ = true;
}
const std::u16string payment_instrument_label_ = u"test instrument";
const std::u16string payment_instrument_details_ = u"instrument details";
std::unique_ptr<PaymentRequestSpec> spec_;
std::string challenge_bytes_;
std::string credential_id_bytes_;
bool on_instrument_details_ready_called_ = false;
bool on_instrument_details_error_called_ = false;
scoped_refptr<FakeBrowserBoundKeyStore> browser_bound_key_store_ =
base::MakeRefCounted<FakeBrowserBoundKeyStore>();
content::BrowserTaskEnvironment task_environment_;
content::TestBrowserContext context_;
content::TestWebContentsFactory web_contents_factory_;
raw_ptr<content::WebContents> web_contents_;
base::WeakPtrFactory<SecurePaymentConfirmationAppTest> weak_ptr_factory_{
this};
};
TEST_F(SecurePaymentConfirmationAppTest, Smoke) {
web_contents_ = web_contents_factory_.CreateWebContents(&context_);
std::vector<uint8_t> credential_id(credential_id_bytes_.begin(),
credential_id_bytes_.end());
auto authenticator =
std::make_unique<webauthn::MockInternalAuthenticator>(web_contents_);
webauthn::MockInternalAuthenticator* mock_authenticator = authenticator.get();
SecurePaymentConfirmationApp app(
web_contents_, "effective_rp.example", payment_instrument_label_,
payment_instrument_details_,
/*payment_instrument_icon=*/std::make_unique<SkBitmap>(),
std::move(credential_id),
/*passkey_browser_binder=*/nullptr,
/*device_supports_browser_bound_keys_in_hardware=*/true,
url::Origin::Create(GURL("https://merchant.example")), spec_->AsWeakPtr(),
MakeRequest(), std::move(authenticator),
/*payment_entities_logos=*/{});
std::vector<uint8_t> expected_bytes =
std::vector<uint8_t>(challenge_bytes_.begin(), challenge_bytes_.end());
EXPECT_CALL(*mock_authenticator, GetAssertion(_, _))
.WillOnce(
[&expected_bytes](
blink::mojom::PublicKeyCredentialRequestOptionsPtr options,
webauthn::InternalAuthenticator::GetAssertionCallback callback) {
EXPECT_EQ(options->challenge, expected_bytes);
auto struct_ptr_is_not_null = Property(
&mojo::StructPtr<
blink::mojom::AuthenticationExtensionsClientInputs>::
is_null,
false);
EXPECT_THAT(options->extensions, struct_ptr_is_not_null);
std::move(callback).Run(
blink::mojom::AuthenticatorStatus::SUCCESS,
blink::mojom::GetAssertionAuthenticatorResponse::New(),
/*dom_exception_details=*/nullptr);
});
app.InvokePaymentApp(/*delegate=*/weak_ptr_factory_.GetWeakPtr());
EXPECT_TRUE(on_instrument_details_ready_called_);
EXPECT_FALSE(on_instrument_details_error_called_);
}
#if BUILDFLAG(IS_ANDROID)
struct BrowserBoundKeyTestParams {
std::optional<
std::vector<::device::PublicKeyCredentialParams::CredentialInfo>>
credential_parameters;
int32_t algorithm_identifier = 0;
bool is_new_bbk = false;
bool is_off_the_record = false;
bool expect_browser_bound_key = false;
bool device_supports_browser_bound_keys_in_hardware = false;
SecurePaymentConfirmationBrowserBoundKeyInclusionResult
expected_inclusion_metric_result;
std::string test_name_suffix;
};
class SecurePaymentConfirmationAppBrowserBindingTest
: public SecurePaymentConfirmationAppTest,
public ::testing::WithParamInterface<BrowserBoundKeyTestParams> {};
INSTANTIATE_TEST_SUITE_P(
SecurePaymentConfirmationAppBrowserBindingTest,
SecurePaymentConfirmationAppBrowserBindingTest,
::testing::Values(
BrowserBoundKeyTestParams{
.credential_parameters =
{{device::PublicKeyCredentialParams::CredentialInfo(
device::CredentialType::kPublicKey,
kAlgorithmIdentifier)}},
.algorithm_identifier = kAlgorithmIdentifier,
.is_new_bbk = false,
.is_off_the_record = false,
.expect_browser_bound_key = true,
.device_supports_browser_bound_keys_in_hardware = true,
.expected_inclusion_metric_result =
SecurePaymentConfirmationBrowserBoundKeyInclusionResult::
kIncludedExisting,
.test_name_suffix = "WithSpecifiedAlgorithm",
},
BrowserBoundKeyTestParams{
.credential_parameters =
{{device::PublicKeyCredentialParams::CredentialInfo(
device::CredentialType::kPublicKey,
kAlgorithmIdentifier)}},
.algorithm_identifier = kAlgorithmIdentifier,
.is_new_bbk = true,
.is_off_the_record = false,
.expect_browser_bound_key = true,
.device_supports_browser_bound_keys_in_hardware = true,
.expected_inclusion_metric_result =
SecurePaymentConfirmationBrowserBoundKeyInclusionResult::
kIncludedNew,
.test_name_suffix = "WithoutPreExistingKey",
},
BrowserBoundKeyTestParams{
.credential_parameters =
{{device::PublicKeyCredentialParams::CredentialInfo(
device::CredentialType::kPublicKey,
kAlgorithmIdentifier)}},
.algorithm_identifier = kAlgorithmIdentifier,
.is_new_bbk = false,
.is_off_the_record = true,
.expect_browser_bound_key = true,
.device_supports_browser_bound_keys_in_hardware = true,
.expected_inclusion_metric_result =
SecurePaymentConfirmationBrowserBoundKeyInclusionResult::
kIncludedExisting,
.test_name_suffix = "WhenOffTheRecordWithPreExistingKey",
},
BrowserBoundKeyTestParams{
.credential_parameters =
{{device::PublicKeyCredentialParams::CredentialInfo(
device::CredentialType::kPublicKey,
kAlgorithmIdentifier)}},
.algorithm_identifier = kAlgorithmIdentifier,
.is_new_bbk = true,
.is_off_the_record = true,
.expect_browser_bound_key = false,
.device_supports_browser_bound_keys_in_hardware = true,
.expected_inclusion_metric_result =
SecurePaymentConfirmationBrowserBoundKeyInclusionResult::
kNotIncludedWithDeviceHardware,
.test_name_suffix = "WhenOffTheRecordWithoutPreExistingKey",
},
BrowserBoundKeyTestParams{
.credential_parameters = std::nullopt,
.algorithm_identifier = base::strict_cast<int32_t>(
device::CoseAlgorithmIdentifier::kEs256),
.is_new_bbk = true,
.is_off_the_record = false,
.expect_browser_bound_key = true,
.device_supports_browser_bound_keys_in_hardware = true,
.expected_inclusion_metric_result =
SecurePaymentConfirmationBrowserBoundKeyInclusionResult::
kIncludedNew,
.test_name_suffix = "Es256WithDefaults",
},
BrowserBoundKeyTestParams{
.credential_parameters = std::nullopt,
.algorithm_identifier = base::strict_cast<int32_t>(
device::CoseAlgorithmIdentifier::kRs256),
.is_new_bbk = true,
.is_off_the_record = false,
.expect_browser_bound_key = true,
.device_supports_browser_bound_keys_in_hardware = true,
.expected_inclusion_metric_result =
SecurePaymentConfirmationBrowserBoundKeyInclusionResult::
kIncludedNew,
.test_name_suffix = "Rs256WithDefaults",
},
BrowserBoundKeyTestParams{
.credential_parameters = std::nullopt,
.algorithm_identifier = kAlgorithmIdentifier,
.is_new_bbk = true,
.is_off_the_record = false,
.expect_browser_bound_key = false,
.device_supports_browser_bound_keys_in_hardware = true,
.expected_inclusion_metric_result =
SecurePaymentConfirmationBrowserBoundKeyInclusionResult::
kNotIncludedWithDeviceHardware,
.test_name_suffix = "WithNonDefaultAlgorithm",
},
BrowserBoundKeyTestParams{
.credential_parameters = std::nullopt,
.algorithm_identifier = kAlgorithmIdentifier,
.is_new_bbk = true,
.is_off_the_record = false,
.expect_browser_bound_key = false,
.device_supports_browser_bound_keys_in_hardware = false,
.expected_inclusion_metric_result =
SecurePaymentConfirmationBrowserBoundKeyInclusionResult::
kNotIncludedWithoutDeviceHardware,
.test_name_suffix = "NotIncludedWithoutDeviceHardware",
}),
[](const ::testing::TestParamInfo<BrowserBoundKeyTestParams>& info) {
return info.param.test_name_suffix;
});
auto InvokeAuthenticatorCallback(std::vector<uint8_t> client_data_json) {
auto authenticator_response =
blink::mojom::GetAssertionAuthenticatorResponse::New();
authenticator_response->info = blink::mojom::CommonCredentialInfo::New();
authenticator_response->info->client_data_json = client_data_json;
authenticator_response->extensions =
blink::mojom::AuthenticationExtensionsClientOutputs::New();
return base::test::RunOnceCallback<1>(
blink::mojom::AuthenticatorStatus::SUCCESS,
std::move(authenticator_response),
/*dom_exception_details=*/nullptr);
}
TEST_P(SecurePaymentConfirmationAppBrowserBindingTest,
AddsBrowserBoundKeyAndSignature) {
context_.set_is_off_the_record(GetParam().is_off_the_record);
web_contents_ = web_contents_factory_.CreateWebContents(&context_);
base::HistogramTester histograms;
base::test::ScopedFeatureList features(
blink::features::kSecurePaymentConfirmationBrowserBoundKeys);
auto authenticator =
std::make_unique<webauthn::MockInternalAuthenticator>(web_contents_);
webauthn::MockInternalAuthenticator* mock_authenticator = authenticator.get();
std::vector<uint8_t> credential_id(credential_id_bytes_.begin(),
credential_id_bytes_.end());
const std::vector<uint8_t> client_data_json({0x01, 0x02, 0x03, 0x04});
const std::vector<uint8_t> public_key_as_cose_key({0x05, 0x06, 0x07, 0x08});
const std::vector<uint8_t> signature({0x09, 0x0a, 0x0b, 0x0c});
const std::vector<uint8_t> browser_bound_key_id({0x0d, 0x0e, 0x0f, 0x10});
scoped_refptr<MockWebPaymentsWebDataService> mock_service =
base::MakeRefCounted<MockWebPaymentsWebDataService>();
auto binder = std::make_unique<PasskeyBrowserBinder>(browser_bound_key_store_,
mock_service);
binder->SetRandomBytesAsVectorCallbackForTesting(base::BindRepeating(
[](const std::vector<uint8_t>& value, size_t) { return value; },
browser_bound_key_id));
SecurePaymentConfirmationApp app(
web_contents_, "effective_rp.example", payment_instrument_label_,
payment_instrument_details_,
/*payment_instrument_icon=*/std::make_unique<SkBitmap>(), credential_id,
std::move(binder),
GetParam().device_supports_browser_bound_keys_in_hardware,
url::Origin::Create(GURL("https://merchant.example")), spec_->AsWeakPtr(),
MakeRequest(GetParam().credential_parameters), std::move(authenticator),
/*payment_entities_logos=*/{});
browser_bound_key_store_->PutFakeKey(FakeBrowserBoundKey(
browser_bound_key_id, public_key_as_cose_key, signature,
GetParam().algorithm_identifier, client_data_json,
/*is_new=*/GetParam().is_new_bbk));
WebDataServiceConsumer* web_data_service_consumer = nullptr;
WebDataServiceBase::Handle web_data_service_handle = 1234;
EXPECT_CALL(*mock_service, GetBrowserBoundKey(Eq(credential_id),
Eq("effective_rp.example"), _))
.WillOnce(DoAll(SaveArg<2>(&web_data_service_consumer),
Return(web_data_service_handle)));
EXPECT_CALL(
*mock_authenticator,
SetPaymentOptions(Pointee(Field(
"browser_bound_public_key",
&blink::mojom::PaymentOptions::browser_bound_public_key,
GetParam().expect_browser_bound_key
? std::optional<std::vector<uint8_t>>(public_key_as_cose_key)
: std::nullopt))));
EXPECT_CALL(*mock_authenticator, GetAssertion(_, _))
.WillOnce(InvokeAuthenticatorCallback(client_data_json));
app.InvokePaymentApp(/*delegate=*/weak_ptr_factory_.GetWeakPtr());
// Simulate the retrieval of an existing browser bound key.
ASSERT_TRUE(web_data_service_consumer);
auto metadata_result =
std::make_unique<WDResult<std::optional<std::vector<uint8_t>>>>(
WDResultType::BROWSER_BOUND_KEY, GetParam().is_new_bbk
? std::vector<uint8_t>()
: browser_bound_key_id);
web_data_service_consumer->OnWebDataServiceRequestDone(
web_data_service_handle, std::move(metadata_result));
ASSERT_TRUE(on_instrument_details_ready_called_);
mojom::PaymentResponsePtr payment_response =
app.SetAppSpecificResponseFields(mojom::PaymentResponse::New());
EXPECT_THAT(
payment_response->get_assertion_authenticator_response->extensions
->payment,
Pointee(Field("browser_bound_signature",
&blink::mojom::AuthenticationExtensionsPaymentResponse::
browser_bound_signature,
ElementsAreArray(GetParam().expect_browser_bound_key
? signature
: std::vector<uint8_t>()))));
histograms.ExpectUniqueSample(
"PaymentRequest.SecurePaymentConfirmation.BrowserBoundKeyInclusion",
GetParam().expected_inclusion_metric_result,
/*expected_bucket_count=*/1);
}
#endif // BUILDFLAG(IS_ANDROID)
class SecurePaymentConfirmationAppWithUxRefreshFlagTest
: public SecurePaymentConfirmationAppTest {
public:
SecurePaymentConfirmationAppWithUxRefreshFlagTest()
: scoped_feature_list_{
blink::features::kSecurePaymentConfirmationUxRefresh} {}
const GURL kPaymentEntity1LogoUrl =
GURL("https://payment-entity-1.example/icon.png");
const GURL kPaymentEntity2LogoUrl =
GURL("https://payment-entity-2.example/icon.png");
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
Matcher<blink::mojom::ShownPaymentEntityLogoPtr> IsShownPaymentEntityLogo(
GURL url,
std::string label) {
return Pointer(AllOf(
Field("url", &blink::mojom::ShownPaymentEntityLogo::url, url),
Field("label", &blink::mojom::ShownPaymentEntityLogo::label, label)));
}
TEST_F(SecurePaymentConfirmationAppWithUxRefreshFlagTest, NoCredentials) {
web_contents_ = web_contents_factory_.CreateWebContents(&context_);
SecurePaymentConfirmationApp app(
web_contents_, "effective_rp.example", payment_instrument_label_,
payment_instrument_details_,
/*payment_instrument_icon=*/std::make_unique<SkBitmap>(),
/*credential_id=*/std::vector<uint8_t>(),
/*passkey_browser_binder=*/nullptr,
/*device_supports_browser_bound_keys_in_hardware=*/false,
url::Origin::Create(GURL("https://merchant.example")), spec_->AsWeakPtr(),
MakeRequest(), /*authenticator=*/nullptr,
/*payment_entities_logos=*/{});
EXPECT_FALSE(app.HasEnrolledInstrument());
EXPECT_EQ(app.GetId(), "spc");
}
// Test that the SPC app returns HasEnrolledInstrument true when the ux refresh
// feature is enabled but there are credentials (i.e. no fallback).
TEST_F(SecurePaymentConfirmationAppWithUxRefreshFlagTest, WithCredentials) {
web_contents_ = web_contents_factory_.CreateWebContents(&context_);
std::vector<uint8_t> credential_id(credential_id_bytes_.begin(),
credential_id_bytes_.end());
SecurePaymentConfirmationApp app(
web_contents_, "effective_rp.example", payment_instrument_label_,
payment_instrument_details_,
/*payment_instrument_icon=*/std::make_unique<SkBitmap>(), credential_id,
/*passkey_browser_binder=*/nullptr,
/*device_supports_browser_bound_keys_in_hardware=*/false,
url::Origin::Create(GURL("https://merchant.example")), spec_->AsWeakPtr(),
MakeRequest(),
std::make_unique<webauthn::MockInternalAuthenticator>(web_contents_),
/*payment_entities_logos=*/{});
EXPECT_TRUE(app.HasEnrolledInstrument());
EXPECT_EQ(app.GetId(), base::Base64Encode(credential_id));
}
TEST_F(SecurePaymentConfirmationAppWithUxRefreshFlagTest,
AddsPaymentEntitiesLogosAndDetailsToPaymentOptions) {
web_contents_ = web_contents_factory_.CreateWebContents(&context_);
std::vector<uint8_t> credential_id(credential_id_bytes_.begin(),
credential_id_bytes_.end());
auto authenticator =
std::make_unique<webauthn::MockInternalAuthenticator>(web_contents_);
webauthn::MockInternalAuthenticator* mock_authenticator = authenticator.get();
auto drawsSomethingBitmap1 = std::make_unique<SkBitmap>();
drawsSomethingBitmap1->allocN32Pixels(/*width=*/32, /*height=*/32);
auto drawsSomethingBitmap2 = std::make_unique<SkBitmap>();
drawsSomethingBitmap2->allocN32Pixels(/*width=*/32, /*height=*/64);
std::vector<PaymentApp::PaymentEntityLogo> logos;
logos.emplace_back(u"PaymentEntity #1", std::move(drawsSomethingBitmap1),
kPaymentEntity1LogoUrl);
logos.emplace_back(u"PaymentEntity #2", std::move(drawsSomethingBitmap2),
kPaymentEntity2LogoUrl);
mojom::SecurePaymentConfirmationRequestPtr request = MakeRequest();
request->instrument->details = "**** 1234";
SecurePaymentConfirmationApp app(
web_contents_, "effective_rp.example", payment_instrument_label_,
payment_instrument_details_,
/*payment_instrument_icon=*/std::make_unique<SkBitmap>(),
std::move(credential_id),
/*passkey_browser_binder=*/nullptr,
/*device_supports_browser_bound_keys_in_hardware=*/false,
url::Origin::Create(GURL("https://merchant.example")), spec_->AsWeakPtr(),
std::move(request), std::move(authenticator), std::move(logos));
blink::mojom::PaymentOptionsPtr payment_options;
EXPECT_CALL(*mock_authenticator, SetPaymentOptions)
.WillOnce(MoveArg<0>(&payment_options));
app.InvokePaymentApp(/*delegate=*/weak_ptr_factory_.GetWeakPtr());
// The first logo is not included because its bitmap is not set.
EXPECT_THAT(payment_options,
Pointer(AllOf(
Field("payment_entities_logos",
&blink::mojom::PaymentOptions::payment_entities_logos,
Optional(ElementsAre(
IsShownPaymentEntityLogo(kPaymentEntity1LogoUrl,
"PaymentEntity #1"),
IsShownPaymentEntityLogo(kPaymentEntity2LogoUrl,
"PaymentEntity #2")))),
Field("instrument", &blink::mojom::PaymentOptions::instrument,
Pointer(Field(
"details",
&blink::mojom::PaymentCredentialInstrument::details,
"**** 1234"))))));
}
TEST_F(SecurePaymentConfirmationAppWithUxRefreshFlagTest,
PaymentEntitiesLogosWithoutBitmapsAreEmptyUrlsInPaymentOptions) {
web_contents_ = web_contents_factory_.CreateWebContents(&context_);
std::vector<uint8_t> credential_id(credential_id_bytes_.begin(),
credential_id_bytes_.end());
auto authenticator =
std::make_unique<webauthn::MockInternalAuthenticator>(web_contents_);
webauthn::MockInternalAuthenticator* mock_authenticator = authenticator.get();
auto drawsSomethingBitmap = std::make_unique<SkBitmap>();
drawsSomethingBitmap->allocN32Pixels(/*width=*/32, /*height=*/32);
std::vector<PaymentApp::PaymentEntityLogo> logos;
logos.emplace_back(u"PaymentEntity #1",
/*icon=*/nullptr, kPaymentEntity1LogoUrl);
logos.emplace_back(u"PaymentEntity #2", std::move(drawsSomethingBitmap),
kPaymentEntity2LogoUrl);
SecurePaymentConfirmationApp app(
web_contents_, "effective_rp.example", payment_instrument_label_,
payment_instrument_details_,
/*payment_instrument_icon=*/std::make_unique<SkBitmap>(),
std::move(credential_id),
/*passkey_browser_binder=*/nullptr,
/*device_supports_browser_bound_keys_in_hardware=*/false,
url::Origin::Create(GURL("https://merchant.example")), spec_->AsWeakPtr(),
MakeRequest(), std::move(authenticator), std::move(logos));
blink::mojom::PaymentOptionsPtr payment_options;
EXPECT_CALL(*mock_authenticator, SetPaymentOptions)
.WillOnce(MoveArg<0>(&payment_options));
app.InvokePaymentApp(/*delegate=*/weak_ptr_factory_.GetWeakPtr());
// The first logo is not included because its bitmap is not set.
EXPECT_THAT(
payment_options,
Pointer(Field(
"payment_entities_logos",
&blink::mojom::PaymentOptions::payment_entities_logos,
Optional(ElementsAre(
IsShownPaymentEntityLogo(GURL::EmptyGURL(), "PaymentEntity #1"),
IsShownPaymentEntityLogo(kPaymentEntity2LogoUrl,
"PaymentEntity #2"))))));
}
class SecurePaymentConfirmationAppWithDisabledUxRefreshFlagTest
: public SecurePaymentConfirmationAppTest {
public:
SecurePaymentConfirmationAppWithDisabledUxRefreshFlagTest()
: scoped_feature_list_() {
scoped_feature_list_.InitAndDisableFeature(
blink::features::kSecurePaymentConfirmationUxRefresh);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(SecurePaymentConfirmationAppWithDisabledUxRefreshFlagTest,
DoesNotAddPaymentEntitiesLogosAndDetailsToPaymentOptions) {
web_contents_ = web_contents_factory_.CreateWebContents(&context_);
std::vector<uint8_t> credential_id(credential_id_bytes_.begin(),
credential_id_bytes_.end());
auto authenticator =
std::make_unique<webauthn::MockInternalAuthenticator>(web_contents_);
webauthn::MockInternalAuthenticator* mock_authenticator = authenticator.get();
SecurePaymentConfirmationApp app(
web_contents_, "effective_rp.example", payment_instrument_label_,
payment_instrument_details_,
/*payment_instrument_icon=*/std::make_unique<SkBitmap>(),
std::move(credential_id),
/*passkey_browser_binder=*/nullptr,
/*device_supports_browser_bound_keys_in_hardware=*/false,
url::Origin::Create(GURL("https://merchant.example")), spec_->AsWeakPtr(),
MakeRequest(), std::move(authenticator),
/*payment_entities_logos=*/{});
blink::mojom::PaymentOptionsPtr payment_options;
EXPECT_CALL(*mock_authenticator, SetPaymentOptions)
.WillOnce(MoveArg<0>(&payment_options));
app.InvokePaymentApp(/*delegate=*/weak_ptr_factory_.GetWeakPtr());
EXPECT_THAT(payment_options,
Pointer(AllOf(
Field("payment_entities_logos",
&blink::mojom::PaymentOptions::payment_entities_logos,
std::cref(std::nullopt)),
Field("instrument", &blink::mojom::PaymentOptions::instrument,
Pointer(Field(
"details",
&blink::mojom::PaymentCredentialInstrument::details,
std::cref(std::nullopt)))))));
}
// Test that OnInstrumentDetailsError is called when the authenticator returns
// an error.
TEST_F(SecurePaymentConfirmationAppTest, OnInstrumentDetailsError) {
web_contents_ = web_contents_factory_.CreateWebContents(&context_);
std::vector<uint8_t> credential_id(credential_id_bytes_.begin(),
credential_id_bytes_.end());
auto authenticator =
std::make_unique<webauthn::MockInternalAuthenticator>(web_contents_);
webauthn::MockInternalAuthenticator* mock_authenticator = authenticator.get();
SecurePaymentConfirmationApp app(
web_contents_, "effective_rp.example", payment_instrument_label_,
payment_instrument_details_,
/*payment_instrument_icon=*/std::make_unique<SkBitmap>(),
std::move(credential_id),
/*passkey_browser_binder=*/nullptr,
/*device_supports_browser_bound_keys_in_hardware=*/false,
url::Origin::Create(GURL("https://merchant.example")), spec_->AsWeakPtr(),
MakeRequest(), std::move(authenticator),
/*payment_entities_logos=*/{});
EXPECT_CALL(*mock_authenticator, GetAssertion(_, _))
.WillOnce(RunOnceCallback<1>(
blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR,
blink::mojom::GetAssertionAuthenticatorResponse::New(),
/*dom_exception_details=*/nullptr));
app.InvokePaymentApp(/*delegate=*/weak_ptr_factory_.GetWeakPtr());
EXPECT_FALSE(on_instrument_details_ready_called_);
EXPECT_TRUE(on_instrument_details_error_called_);
}
class SecurePaymentConfirmationAppFallbackTest
: public SecurePaymentConfirmationAppTest {
public:
SecurePaymentConfirmationAppFallbackTest() {
feature_list_.InitAndEnableFeature(
features::kSecurePaymentConfirmationFallback);
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Test that the SPC app can be created without credentials.
TEST_F(SecurePaymentConfirmationAppFallbackTest, NoCredentials) {
web_contents_ = web_contents_factory_.CreateWebContents(&context_);
SecurePaymentConfirmationApp app(
web_contents_, "effective_rp.example", payment_instrument_label_,
payment_instrument_details_,
/*payment_instrument_icon=*/std::make_unique<SkBitmap>(),
/*credential_id=*/std::vector<uint8_t>(),
/*passkey_browser_binder=*/nullptr,
/*device_supports_browser_bound_keys_in_hardware=*/false,
url::Origin::Create(GURL("https://merchant.example")), spec_->AsWeakPtr(),
MakeRequest(), /*authenticator=*/nullptr,
/*payment_entities_logos=*/{});
EXPECT_FALSE(app.HasEnrolledInstrument());
EXPECT_EQ(app.GetId(), "spc");
}
// Test that the SPC app returns HasEnrolledInstrument true when the fallback
// feature is enabled but there are credentials (i.e. no fallback).
TEST_F(SecurePaymentConfirmationAppFallbackTest, WithCredentials) {
web_contents_ = web_contents_factory_.CreateWebContents(&context_);
std::vector<uint8_t> credential_id(credential_id_bytes_.begin(),
credential_id_bytes_.end());
SecurePaymentConfirmationApp app(
web_contents_, "effective_rp.example", payment_instrument_label_,
payment_instrument_details_,
/*payment_instrument_icon=*/std::make_unique<SkBitmap>(), credential_id,
/*passkey_browser_binder=*/nullptr,
/*device_supports_browser_bound_keys_in_hardware=*/false,
url::Origin::Create(GURL("https://merchant.example")), spec_->AsWeakPtr(),
MakeRequest(),
std::make_unique<webauthn::MockInternalAuthenticator>(web_contents_),
/*payment_entities_logos=*/{});
EXPECT_TRUE(app.HasEnrolledInstrument());
EXPECT_EQ(app.GetId(), base::Base64Encode(credential_id));
}
} // namespace
} // namespace payments