blob: 084d33238439ff8868551a06f220c05a2def8e5b [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 <stdint.h>
#include <memory>
#include <utility>
#include <vector>
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/memory/ref_counted.h"
#include "base/path_service.h"
#include "base/strings/strcat.h"
#include "base/strings/string16.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_data_service_factory.h"
#include "chrome/test/payments/payment_request_platform_browsertest_base.h"
#include "components/keyed_service/core/service_access_type.h"
#include "components/payments/content/payment_manifest_web_data_service.h"
#include "components/payments/core/features.h"
#include "components/payments/core/secure_payment_confirmation_instrument.h"
#include "components/webdata/common/web_data_service_consumer.h"
#include "content/public/browser/authenticator_environment.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "device/fido/virtual_fido_device_factory.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace payments {
namespace {
std::vector<uint8_t> GetEncodedIcon(const std::string& icon_file_name) {
base::FilePath base_path;
CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &base_path));
std::string icon_as_string;
base::FilePath icon_file_path =
base_path.AppendASCII("components/test/data/payments")
.AppendASCII(icon_file_name);
{
base::ScopedAllowBlockingForTesting allow_blocking;
CHECK(base::PathExists(icon_file_path));
CHECK(base::ReadFileToString(icon_file_path, &icon_as_string));
}
return std::vector<uint8_t>(icon_as_string.begin(), icon_as_string.end());
}
class SecurePaymentConfirmationTest
: public PaymentRequestPlatformBrowserTestBase,
public WebDataServiceConsumer {
public:
SecurePaymentConfirmationTest() {
// Enable the browser-side feature flag as it's disabled by default on
// non-origin trial platforms.
feature_list_.InitAndEnableFeature(features::kSecurePaymentConfirmation);
}
void SetUpCommandLine(base::CommandLine* command_line) override {
PaymentRequestPlatformBrowserTestBase::SetUpCommandLine(command_line);
command_line->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
}
void OnWebDataServiceRequestDone(
WebDataServiceBase::Handle h,
std::unique_ptr<WDTypedResult> result) override {
ASSERT_NE(nullptr, result);
ASSERT_EQ(BOOL_RESULT, result->GetType());
EXPECT_TRUE(static_cast<WDResult<bool>*>(result.get())->GetValue());
database_write_responded_ = true;
}
void OnAppListReady() override {
PaymentRequestPlatformBrowserTestBase::OnAppListReady();
if (confirm_payment_)
ASSERT_TRUE(test_controller()->ConfirmPayment());
}
bool database_write_responded_ = false;
bool confirm_payment_ = false;
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest, NoAuthenticator) {
test_controller()->SetHasAuthenticator(false);
NavigateTo("a.com", "/secure_payment_confirmation.html");
// EvalJs waits for JavaScript promise to resolve.
EXPECT_EQ(
"The payment method \"secure-payment-confirmation\" is not supported.",
content::EvalJs(GetActiveWebContents(),
"getSecurePaymentConfirmationStatus()"));
}
#if defined(OS_ANDROID)
// TODO(https://crbug.com/1110320): Implement SetHasAuthenticator() for Android,
// so secure payment confirmation can be tested on Android as well.
#define MAYBE_NoInstrumentInStorage DISABLED_NoInstrumentInStorage
#define MAYBE_CheckInstrumentInStorageAfterCanMakePayment \
DISABLED_CheckInstrumentInStorageAfterCanMakePayment
#define MAYBE_PaymentSheetShowsApp DISABLED_PaymentSheetShowsApp
#define MAYBE_CanMakePayment_HasAuthenticator \
DISABLED_CanMakePayment_HasAuthenticator
#else
#define MAYBE_NoInstrumentInStorage NoInstrumentInStorage
#define MAYBE_CheckInstrumentInStorageAfterCanMakePayment \
CheckInstrumentInStorageAfterCanMakePayment
#define MAYBE_PaymentSheetShowsApp PaymentSheetShowsApp
#define MAYBE_CanMakePayment_HasAuthenticator CanMakePayment_HasAuthenticator
#endif // OS_ANDROID
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,
MAYBE_NoInstrumentInStorage) {
test_controller()->SetHasAuthenticator(true);
NavigateTo("a.com", "/secure_payment_confirmation.html");
// EvalJs waits for JavaScript promise to resolve.
EXPECT_EQ(
"The payment method \"secure-payment-confirmation\" is not supported.",
content::EvalJs(GetActiveWebContents(),
"getSecurePaymentConfirmationStatus()"));
}
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,
MAYBE_CheckInstrumentInStorageAfterCanMakePayment) {
test_controller()->SetHasAuthenticator(true);
NavigateTo("a.com", "/secure_payment_confirmation.html");
// EvalJs waits for JavaScript promise to resolve.
EXPECT_EQ(
"The payment method \"secure-payment-confirmation\" is not supported.",
content::EvalJs(
GetActiveWebContents(),
base::StringPrintf(
"getSecurePaymentConfirmationStatusAfterCanMakePayment()")));
}
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,
MAYBE_PaymentSheetShowsApp) {
test_controller()->SetHasAuthenticator(true);
NavigateTo("a.com", "/secure_payment_confirmation.html");
std::vector<uint8_t> credential_id = {'c', 'r', 'e', 'd'};
std::vector<uint8_t> icon = GetEncodedIcon("icon.png");
WebDataServiceFactory::GetPaymentManifestWebDataForProfile(
Profile::FromBrowserContext(GetActiveWebContents()->GetBrowserContext()),
ServiceAccessType::EXPLICIT_ACCESS)
->AddSecurePaymentConfirmationInstrument(
std::make_unique<SecurePaymentConfirmationInstrument>(
std::move(credential_id), "relying-party.example",
base::ASCIIToUTF16("Stub label"), std::move(icon)),
/*consumer=*/this);
ResetEventWaiterForSingleEvent(TestEvent::kAppListReady);
// ExecJs starts executing JavaScript and immediately returns, not waiting for
// any promise to return.
EXPECT_TRUE(content::ExecJs(GetActiveWebContents(),
"getSecurePaymentConfirmationStatus()"));
WaitForObservedEvent();
EXPECT_TRUE(database_write_responded_);
ASSERT_FALSE(test_controller()->app_descriptions().empty());
EXPECT_EQ(1u, test_controller()->app_descriptions().size());
EXPECT_EQ("Stub label", test_controller()->app_descriptions().front().label);
}
// canMakePayment() and hasEnrolledInstrument() should return false on platforms
// without a compatible authenticator.
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,
CanMakePayment_NoAuthenticator) {
test_controller()->SetHasAuthenticator(false);
NavigateTo("a.com", "/secure_payment_confirmation.html");
EXPECT_EQ("false",
content::EvalJs(GetActiveWebContents(),
"securePaymentConfirmationCanMakePayment()"));
EXPECT_EQ("false", content::EvalJs(
GetActiveWebContents(),
"securePaymentConfirmationHasEnrolledInstrument()"));
}
// canMakePayment() and hasEnrolledInstrument() should return true on platforms
// with a compatible authenticator regardless of the presence of payment
// credentials.
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationTest,
MAYBE_CanMakePayment_HasAuthenticator) {
test_controller()->SetHasAuthenticator(true);
NavigateTo("a.com", "/secure_payment_confirmation.html");
EXPECT_EQ("true",
content::EvalJs(GetActiveWebContents(),
"securePaymentConfirmationCanMakePayment()"));
EXPECT_EQ("true",
content::EvalJs(GetActiveWebContents(),
"securePaymentConfirmationCanMakePaymentTwice()"));
EXPECT_EQ("true", content::EvalJs(
GetActiveWebContents(),
"securePaymentConfirmationHasEnrolledInstrument()"));
}
// Intentionally do not enable the "SecurePaymentConfirmation" Blink runtime
// feature or the browser-side Finch flag.
class SecurePaymentConfirmationDisabledTest
: public PaymentRequestPlatformBrowserTestBase {};
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledTest,
PaymentMethodNotSupported) {
test_controller()->SetHasAuthenticator(true);
NavigateTo("a.com", "/secure_payment_confirmation.html");
// EvalJs waits for JavaScript promise to resolve.
EXPECT_EQ(
"The payment method \"secure-payment-confirmation\" is not supported.",
content::EvalJs(GetActiveWebContents(),
"getSecurePaymentConfirmationStatus()"));
}
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledTest,
CannotMakePayment) {
test_controller()->SetHasAuthenticator(true);
NavigateTo("a.com", "/secure_payment_confirmation.html");
EXPECT_EQ("false",
content::EvalJs(GetActiveWebContents(),
"securePaymentConfirmationCanMakePayment()"));
EXPECT_EQ("false", content::EvalJs(
GetActiveWebContents(),
"securePaymentConfirmationHasEnrolledInstrument()"));
}
// Test that the feature can be disabled by the browser-side Finch flag.
class SecurePaymentConfirmationDisabledByFinchTest
: public PaymentRequestPlatformBrowserTestBase {
public:
SecurePaymentConfirmationDisabledByFinchTest() {
// The feature should get disabled by the feature state despite experimental
// web platform features being enabled.
feature_list_.InitAndDisableFeature(features::kSecurePaymentConfirmation);
}
void SetUpCommandLine(base::CommandLine* command_line) override {
PaymentRequestPlatformBrowserTestBase::SetUpCommandLine(command_line);
command_line->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
}
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledByFinchTest,
PaymentMethodNotSupported) {
test_controller()->SetHasAuthenticator(true);
NavigateTo("a.com", "/secure_payment_confirmation.html");
// EvalJs waits for JavaScript promise to resolve.
EXPECT_EQ(
"The payment method \"secure-payment-confirmation\" is not supported.",
content::EvalJs(GetActiveWebContents(),
"getSecurePaymentConfirmationStatus()"));
}
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDisabledByFinchTest,
CannotMakePayment) {
test_controller()->SetHasAuthenticator(true);
NavigateTo("a.com", "/secure_payment_confirmation.html");
EXPECT_EQ("false",
content::EvalJs(GetActiveWebContents(),
"securePaymentConfirmationCanMakePayment()"));
EXPECT_EQ("false", content::EvalJs(
GetActiveWebContents(),
"securePaymentConfirmationHasEnrolledInstrument()"));
}
// Creation tests do not work on Android because there is not a way to
// override authenticator creation.
#if !defined(OS_ANDROID)
class SecurePaymentConfirmationCreationTest
: public SecurePaymentConfirmationTest {
public:
// PaymentCredential creation uses the normal Web Authentication code path
// for creating the public key credential, rather than using
// IntenralAuthenticator. This stubs out authenticator instantiation in
// content.
void ReplaceFidoDiscoveryFactory() {
auto owned_virtual_device_factory =
std::make_unique<device::test::VirtualFidoDeviceFactory>();
auto* virtual_device_factory = owned_virtual_device_factory.get();
content::AuthenticatorEnvironment::GetInstance()
->ReplaceDefaultDiscoveryFactoryForTesting(
std::move(owned_virtual_device_factory));
virtual_device_factory->SetTransport(
device::FidoTransportProtocol::kInternal);
virtual_device_factory->SetSupportedProtocol(
device::ProtocolVersion::kCtap2);
virtual_device_factory->mutable_state()->fingerprints_enrolled = true;
// Currently this only supports tests relying on user-verifying platform
// authenticators.
device::VirtualCtap2Device::Config config;
config.is_platform_authenticator = true;
config.internal_uv_support = true;
virtual_device_factory->SetCtap2Config(config);
}
const std::string GetDefaultIconURL() {
return https_server()->GetURL("a.com", "/icon.png").spec();
}
};
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,
CreatePaymentCredential) {
base::HistogramTester histogram_tester;
ReplaceFidoDiscoveryFactory();
NavigateTo("a.com", "/secure_payment_confirmation.html");
EXPECT_EQ("OK",
content::EvalJs(GetActiveWebContents(),
content::JsReplace("createPaymentCredential($1)",
GetDefaultIconURL())));
// Verify that credential id size gets recorded.
histogram_tester.ExpectTotalCount(
"PaymentRequest.SecurePaymentConfirmationCredentialIdSizeInBytes", 1U);
}
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,
LookupPaymentCredential) {
ReplaceFidoDiscoveryFactory();
NavigateTo("a.com", "/secure_payment_confirmation.html");
std::string credentialIdentifier =
content::EvalJs(
GetActiveWebContents(),
content::JsReplace("createCredentialAndReturnItsIdentifier($1)",
GetDefaultIconURL()))
.ExtractString();
// Cross the origin boundary.
NavigateTo("b.com", "/secure_payment_confirmation.html");
test_controller()->SetHasAuthenticator(true);
ResetEventWaiterForSingleEvent(TestEvent::kAppListReady);
// ExecJs starts executing JavaScript and immediately returns, not waiting for
// any promise to return.
EXPECT_TRUE(content::ExecJs(
GetActiveWebContents(),
content::JsReplace("getSecurePaymentConfirmationStatus($1)",
credentialIdentifier)));
WaitForObservedEvent();
ASSERT_FALSE(test_controller()->app_descriptions().empty());
EXPECT_EQ(1u, test_controller()->app_descriptions().size());
EXPECT_EQ("display_name_for_instrument",
test_controller()->app_descriptions().front().label);
}
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,
ConfirmPaymentInCrossOriginIframe) {
NavigateTo("a.com", "/secure_payment_confirmation.html");
ReplaceFidoDiscoveryFactory();
std::string credentialIdentifier =
content::EvalJs(
GetActiveWebContents(),
content::JsReplace("createCredentialAndReturnItsIdentifier($1)",
GetDefaultIconURL()))
.ExtractString();
NavigateTo("b.com", "/iframe_poster.html");
test_controller()->SetHasAuthenticator(true);
confirm_payment_ = true;
// EvalJs waits for JavaScript promise to resolve.
EXPECT_EQ(
"success",
content::EvalJs(
GetActiveWebContents(),
content::JsReplace(
"postToIframe($1, $2);",
https_server()->GetURL("c.com", "/iframe_receiver.html").spec(),
credentialIdentifier)));
}
IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationCreationTest,
ChallengeIsReturned) {
NavigateTo("a.com", "/secure_payment_confirmation.html");
ReplaceFidoDiscoveryFactory();
std::string credentialIdentifier =
content::EvalJs(
GetActiveWebContents(),
content::JsReplace("createCredentialAndReturnItsIdentifier($1)",
GetDefaultIconURL()))
.ExtractString();
NavigateTo("b.com", "/get_challenge.html");
test_controller()->SetHasAuthenticator(true);
confirm_payment_ = true;
// Strip the trailing slash ("/") from the merchant origin in serialization to
// match the implementation behavior.
std::string merchant_origin = https_server()->GetURL("b.com", "/").spec();
ASSERT_EQ('/', merchant_origin[merchant_origin.length() - 1]);
merchant_origin = merchant_origin.substr(0, merchant_origin.length() - 1);
ASSERT_NE('/', merchant_origin[merchant_origin.length() - 1]);
// EvalJs waits for JavaScript promise to resolve.
// The `networkData` field is the base64 encoding of 'hello world', which is
// set in `get_challenge.js`.
EXPECT_EQ("{\"merchantData\":{\"merchantOrigin\":\"" + merchant_origin +
"\",\"total\":{\"currency\":\"USD\"," +
"\"value\":\"0.01\"}},\"networkData\":\"aGVsbG8gd29ybGQ=\"}",
content::EvalJs(GetActiveWebContents(),
content::JsReplace("getChallenge($1, $2);",
credentialIdentifier, "0.01")));
// Verify that passing a promise into PaymentRequest.show() that updates the
// `total` price will result in the challenge price being set only after the
// promise resolves with the finalized price.
EXPECT_EQ("{\"merchantData\":{\"merchantOrigin\":\"" + merchant_origin +
"\",\"total\":{\"currency\":\"USD\"," +
"\"value\":\"0.02\"}},\"networkData\":\"aGVsbG8gd29ybGQ=\"}",
content::EvalJs(
GetActiveWebContents(),
content::JsReplace("getChallengeWithShowPromise($1, $2, $3);",
credentialIdentifier, "0.01", "0.02")));
// Verify that the returned challenge correctly reflects the modified amount.
EXPECT_EQ(
"{\"merchantData\":{\"merchantOrigin\":\"" + merchant_origin +
"\",\"total\":{\"currency\":\"USD\"," +
"\"value\":\"0.03\"}},\"networkData\":\"aGVsbG8gd29ybGQ=\"}",
content::EvalJs(GetActiveWebContents(),
content::JsReplace("getChallengeWithModifier($1, $2);",
credentialIdentifier, "0.03")));
// Verify that the returned challenge correctly reflects the modified amount
// that is set when the promised passed into PaymentRequest.show() resolves.
EXPECT_EQ(
"{\"merchantData\":{\"merchantOrigin\":\"" + merchant_origin +
"\",\"total\":{\"currency\":\"USD\"," +
"\"value\":\"0.04\"}},\"networkData\":\"aGVsbG8gd29ybGQ=\"}",
content::EvalJs(
GetActiveWebContents(),
content::JsReplace("getChallengeWithModifierAndShowPromise($1, $2);",
credentialIdentifier, "0.04")));
}
#endif // !defined(OS_ANDROID)
} // namespace
} // namespace payments