blob: da6a3d078d48eb1fd4d11acd36f27eb1236132ee [file] [log] [blame]
// Copyright 2015 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 "ios/chrome/browser/passwords/credential_manager.h"
#include <memory>
#include <utility>
#include "base/memory/ref_counted.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "components/password_manager/core/browser/password_bubble_experiment.h"
#include "components/password_manager/core/browser/stub_password_manager_client.h"
#include "components/password_manager/core/browser/stub_password_manager_driver.h"
#include "components/password_manager/core/browser/test_password_store.h"
#include "components/password_manager/core/common/credential_manager_types.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/testing_pref_service.h"
#import "ios/web/public/web_state/web_state.h"
#import "ios/chrome/browser/passwords/js_credential_manager.h"
#import "ios/testing/ocmock_complex_type_helper.h"
#import "ios/web/public/test/web_test_with_web_state.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#include "third_party/ocmock/OCMock/OCMock.h"
#include "third_party/ocmock/gtest_support.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using testing::Return;
namespace {
// Type of a function invoked when a promise is resolved.
typedef void (^ResolvePromiseBlock)(NSInteger request_id,
const web::Credential& credential);
} // namespace
// A helper to mock methods that have C++ object parameters.
@interface MockJSCredentialManager : OCMockComplexTypeHelper
@end
@implementation MockJSCredentialManager
- (void)resolvePromiseWithRequestID:(NSInteger)requestID
credential:(const web::Credential&)credential
completionHandler:(void (^)(BOOL))completionHandler {
static_cast<ResolvePromiseBlock>([self blockForSelector:_cmd])(requestID,
credential);
completionHandler(YES);
}
@end
namespace {
const char kTestURL[] = "https://foo.com/login";
// Returns a test credential.
autofill::PasswordForm GetTestPasswordForm1(bool zero_click_allowed) {
autofill::PasswordForm form;
form.origin = GURL(kTestURL);
form.signon_realm = form.origin.spec();
form.username_value = base::ASCIIToUTF16("foo");
form.password_value = base::ASCIIToUTF16("bar");
form.skip_zero_click = !zero_click_allowed;
form.type = autofill::PasswordForm::Type::TYPE_API;
return form;
}
// Returns a test credential matching |GetTestPasswordForm1()|.
web::Credential GetTestWebCredential1(bool zero_click_allowed) {
web::Credential credential;
autofill::PasswordForm form(GetTestPasswordForm1(zero_click_allowed));
credential.type = web::CredentialType::CREDENTIAL_TYPE_PASSWORD;
credential.id = form.username_value;
credential.password = form.password_value;
return credential;
}
// Returns a different test credential.
autofill::PasswordForm GetTestPasswordForm2(bool zero_click_allowed) {
autofill::PasswordForm form;
form.origin = GURL(kTestURL);
form.signon_realm = form.origin.spec();
form.username_value = base::ASCIIToUTF16("baz");
form.password_value = base::ASCIIToUTF16("bah");
form.skip_zero_click = !zero_click_allowed;
return form;
}
// Returns a test credential matching |GetTestPasswordForm2()|.
web::Credential GetTestWebCredential2(bool zero_click_allowed) {
web::Credential credential;
autofill::PasswordForm form(GetTestPasswordForm2(zero_click_allowed));
credential.type = web::CredentialType::CREDENTIAL_TYPE_PASSWORD;
credential.id = form.username_value;
credential.password = form.password_value;
return credential;
}
typedef BOOL (^StringPredicate)(NSString*);
// Returns a block that takes a string argument and returns whether it is equal
// to |string|.
StringPredicate EqualsString(const char* string) {
return [^BOOL(NSString* other) {
return [base::SysUTF8ToNSString(string) isEqualToString:other];
} copy];
}
// A stub PasswordManagerClient for testing.
class StubPasswordManagerClient
: public password_manager::StubPasswordManagerClient {
public:
StubPasswordManagerClient()
: password_manager::StubPasswordManagerClient(),
password_store_(nullptr) {
prefs_.registry()->RegisterBooleanPref(
password_manager::prefs::kCredentialsEnableAutosignin, false);
password_bubble_experiment::RegisterPrefs(prefs_.registry());
}
~StubPasswordManagerClient() override {
if (password_store_)
password_store_->ShutdownOnUIThread();
}
MOCK_CONST_METHOD0(IsSavingAndFillingEnabledForCurrentPage, bool());
void SetPasswordStore(
scoped_refptr<password_manager::PasswordStore> password_store) {
password_store_ = password_store;
}
password_manager::PasswordStore* GetPasswordStore() const override {
return password_store_.get();
}
void SetUserChosenCredential(
const password_manager::CredentialInfo& credential) {
user_chosen_credential_ = credential;
}
bool PromptUserToChooseCredentials(
std::vector<std::unique_ptr<autofill::PasswordForm>> local_forms,
const GURL& origin,
const CredentialsCallback& callback) override {
return false;
}
bool PromptUserToSaveOrUpdatePassword(
std::unique_ptr<password_manager::PasswordFormManager> form_to_save,
password_manager::CredentialSourceType type,
bool update_password) override {
if (!update_password)
saved_form_ = std::move(form_to_save);
return true;
}
PrefService* GetPrefs() override { return &prefs_; }
password_manager::PasswordFormManager* saved_form() {
return saved_form_.get();
}
private:
// PrefService for testing.
TestingPrefServiceSimple prefs_;
// PasswordStore for testing.
scoped_refptr<password_manager::PasswordStore> password_store_;
// The password form shown to the user for saving.
std::unique_ptr<password_manager::PasswordFormManager> saved_form_;
// The credential to be returned to callers of PromptUserToChooseCredentials.
password_manager::CredentialInfo user_chosen_credential_;
DISALLOW_COPY_AND_ASSIGN(StubPasswordManagerClient);
};
// Tests for CredentialManager.
class CredentialManagerTest : public web::WebTestWithWebState {
public:
CredentialManagerTest() {}
~CredentialManagerTest() override {}
void SetUp() override {
web::WebTestWithWebState::SetUp();
id originalMock =
[OCMockObject niceMockForClass:[JSCredentialManager class]];
mock_js_credential_manager_ = [[MockJSCredentialManager alloc]
initWithRepresentedObject:originalMock];
credential_manager_.reset(
new CredentialManager(web_state(), &stub_client_, &stub_driver_,
static_cast<id>(mock_js_credential_manager_)));
LoadHtml(@"", GURL(kTestURL));
}
// Sets up an expectation that the promise identified by |request_id| will be
// resolved with |credential|. |verified| must point to a variable that will
// be checked by the caller to ensure that the expectations were run. (This
// is necessary because OCMock doesn't handle methods with C++ object
// parameters.)
void ExpectPromiseResolved(bool* verified,
int request_id,
const web::Credential& credential) {
SEL selector =
@selector(resolvePromiseWithRequestID:credential:completionHandler:);
web::Credential strong_credential(credential);
[mock_js_credential_manager_
onSelector:selector
callBlockExpectation:^(NSInteger block_request_id,
const web::Credential& block_credential) {
EXPECT_EQ(request_id, block_request_id);
EXPECT_EQ(strong_credential.type, block_credential.type);
EXPECT_EQ(strong_credential.id, block_credential.id);
EXPECT_EQ(strong_credential.name, block_credential.name);
EXPECT_EQ(strong_credential.avatar_url, block_credential.avatar_url);
EXPECT_EQ(strong_credential.password, block_credential.password);
EXPECT_EQ(strong_credential.federation_origin.Serialize(),
block_credential.federation_origin.Serialize());
*verified = true;
}];
}
// Same as |ExpectPromiseResolved(bool*, int, const web::Credential&)| but
// does not expect a credential to be passed.
void ExpectPromiseResolved(int request_id) {
[[[mock_js_credential_manager_ representedObject] expect]
resolvePromiseWithRequestID:request_id
completionHandler:nil];
}
// Clears the expectations set up by |ExpectPromiseResolved()|.
void ClearPromiseResolutionExpectations() {
SEL selector =
@selector(resolvePromiseWithRequestID:credential:completionHandler:);
[mock_js_credential_manager_ removeBlockExpectationOnSelector:selector];
}
// Sets up an expectation that the promise identified by |request_id| will be
// rejected with the given |error_type| and |message|. The caller should use
// EXPECT_OCMOCK_VERIFY to verify that the expectations were run.
void ExpectPromiseRejected(int request_id,
const char* error_type,
const char* message) {
[[[mock_js_credential_manager_ representedObject] expect]
rejectPromiseWithRequestID:request_id
errorType:[OCMArg
checkWithBlock:EqualsString(error_type)]
message:[OCMArg checkWithBlock:EqualsString(message)]
completionHandler:[OCMArg any]];
}
protected:
// Mock for PasswordManagerClient.
StubPasswordManagerClient stub_client_;
// Stub for PasswordManagerDriver.
password_manager::StubPasswordManagerDriver stub_driver_;
// Mock for JSCredentialManager.
MockJSCredentialManager* mock_js_credential_manager_;
// CredentialManager for testing.
std::unique_ptr<CredentialManager> credential_manager_;
private:
explicit CredentialManagerTest(const CredentialManagerTest&) = delete;
CredentialManagerTest& operator=(const CredentialManagerTest&) = delete;
};
// Tests that a credential request is rejected properly when the PasswordStore
// is unavailable.
TEST_F(CredentialManagerTest, RequestRejectedWhenPasswordStoreUnavailable) {
// Clear the password store.
stub_client_.SetPasswordStore(nullptr);
// Requesting a credential should reject the request with an error.
const int request_id = 0;
ExpectPromiseRejected(request_id,
kCredentialsPasswordStoreUnavailableErrorType,
kCredentialsPasswordStoreUnavailableErrorMessage);
credential_manager_->CredentialsRequested(request_id, GURL(kTestURL), false,
std::vector<std::string>(), true);
// Pump the message loop and verify.
WaitForBackgroundTasks();
EXPECT_OCMOCK_VERIFY([mock_js_credential_manager_ representedObject]);
}
// Tests that a credential request is rejected when another request is pending.
TEST_F(CredentialManagerTest, RequestRejectedWhenExistingRequestIsPending) {
// Set a password store, but prevent requests from completing.
stub_client_.SetPasswordStore(new password_manager::TestPasswordStore);
// Make an initial request. Don't pump the message loop, so that the task
// doesn't complete. Expect the request to resolve with an empty credential
// after the message loop is pumped.
const int first_request_id = 0;
bool first_verified = false;
ExpectPromiseResolved(&first_verified, first_request_id, web::Credential());
credential_manager_->CredentialsRequested(first_request_id, GURL(kTestURL),
false, std::vector<std::string>(),
true);
// Making a second request and then pumping the message loop should reject the
// request with an error.
const int second_request_id = 0;
ExpectPromiseRejected(second_request_id, kCredentialsPendingRequestErrorType,
kCredentialsPendingRequestErrorMessage);
credential_manager_->CredentialsRequested(second_request_id, GURL(kTestURL),
false, std::vector<std::string>(),
true);
// Pump the message loop and verify.
WaitForBackgroundTasks();
EXPECT_TRUE(first_verified);
EXPECT_OCMOCK_VERIFY([mock_js_credential_manager_ representedObject]);
}
// Tests that a zero-click credential request is resolved properly with an empty
// credential when zero-click sign-in is disabled.
TEST_F(CredentialManagerTest,
ZeroClickRequestResolvedWithEmptyCredentialWhenZeroClickSignInDisabled) {
// Set a password store, but request a zero-click credential with zero-click
// disabled.
stub_client_.SetPasswordStore(new password_manager::TestPasswordStore);
const bool zero_click = true;
static_cast<TestingPrefServiceSimple*>(stub_client_.GetPrefs())
->SetUserPref(password_manager::prefs::kCredentialsEnableAutosignin,
new base::FundamentalValue(!zero_click));
// Requesting a zero-click credential should immediately resolve the request
// with an empty credential.
const int request_id = 0;
bool verified = false;
ExpectPromiseResolved(&verified, request_id, web::Credential());
credential_manager_->CredentialsRequested(
request_id, GURL(kTestURL), zero_click, std::vector<std::string>(), true);
// Pump the message loop and verify.
WaitForBackgroundTasks();
EXPECT_TRUE(verified);
}
// Tests that a credential request is properly resolved with an empty credential
// when no credentials are available.
TEST_F(CredentialManagerTest,
RequestResolvedWithEmptyCredentialWhenNoneAvailable) {
// Set a password store with no credentials, enable zero-click, and request a
// zero-click credential.
stub_client_.SetPasswordStore(new password_manager::TestPasswordStore);
const bool zero_click = true;
static_cast<TestingPrefServiceSimple*>(stub_client_.GetPrefs())
->SetUserPref(password_manager::prefs::kCredentialsEnableAutosignin,
new base::FundamentalValue(zero_click));
// Requesting a zero-click credential should try to retrieve PasswordForms
// from the PasswordStore and resolve the request with an empty Credential
// when none are found.
const int request_id = 0;
bool verified = false;
ExpectPromiseResolved(&verified, request_id, web::Credential());
credential_manager_->CredentialsRequested(
request_id, GURL(kTestURL), zero_click, std::vector<std::string>(), true);
// Pump the message loop and verify.
WaitForBackgroundTasks();
EXPECT_TRUE(verified);
}
// Tests that a zero-click credential request properly resolves.
TEST_F(CredentialManagerTest, ZeroClickRequestResolved) {
// Set a password store with a credential, enable zero-click, and request a
// zero-click credential.
scoped_refptr<password_manager::TestPasswordStore> store(
new password_manager::TestPasswordStore);
const bool zero_click = true;
store->AddLogin(GetTestPasswordForm1(zero_click));
stub_client_.SetPasswordStore(store);
static_cast<TestingPrefServiceSimple*>(stub_client_.GetPrefs())
->SetUserPref(password_manager::prefs::kCredentialsEnableAutosignin,
new base::FundamentalValue(zero_click));
// Without the first run experience, the zero-click credentials won't be
// passed to the site.
static_cast<TestingPrefServiceSimple*>(stub_client_.GetPrefs())
->SetUserPref(
password_manager::prefs::kWasAutoSignInFirstRunExperienceShown,
new base::FundamentalValue(true));
WaitForBackgroundTasks();
// Requesting a zero-click credential should retrieve a PasswordForm from the
// PasswordStore and resolve the request with a corresponding Credential.
const int request_id = 0;
bool verified = false;
ExpectPromiseResolved(&verified, request_id,
GetTestWebCredential1(zero_click));
credential_manager_->CredentialsRequested(
request_id, GURL(kTestURL), zero_click, std::vector<std::string>(), true);
// Pump the message loop and verify.
WaitForBackgroundTasks();
EXPECT_TRUE(verified);
}
// Tests that a credential request properly resolves.
// TODO(crbug.com/598851): Reenabled this test.
TEST_F(CredentialManagerTest, DISABLED_RequestResolved) {
// Set a password store with two credentials and set a credential to be
// returned by the PasswordManagerClient as if chosen by the user.
scoped_refptr<password_manager::TestPasswordStore> store(
new password_manager::TestPasswordStore);
const bool zero_click = false;
store->AddLogin(GetTestPasswordForm1(zero_click));
store->AddLogin(GetTestPasswordForm2(zero_click));
stub_client_.SetPasswordStore(store);
stub_client_.SetUserChosenCredential(password_manager::CredentialInfo(
GetTestPasswordForm2(zero_click),
password_manager::CredentialType::CREDENTIAL_TYPE_PASSWORD));
WaitForBackgroundTasks();
// Request a credential. The request should be resolved with the credential
// set on the stub client.
const int request_id = 0;
bool verified = false;
ExpectPromiseResolved(&verified, request_id,
GetTestWebCredential2(zero_click));
credential_manager_->CredentialsRequested(
request_id, GURL(kTestURL), zero_click, std::vector<std::string>(), true);
// Pump the message loop and verify.
WaitForBackgroundTasks();
EXPECT_TRUE(verified);
}
// Tests that two requests back-to-back succeed when they wait to be resolved.
// TODO(crbug.com/598851): Reenable this test.
TEST_F(CredentialManagerTest, DISABLED_ConsecutiveRequestsResolve) {
// Set a password store with two credentials and set a credential to be
// returned by the PasswordManagerClient as if chosen by the user.
scoped_refptr<password_manager::TestPasswordStore> store(
new password_manager::TestPasswordStore);
const bool zero_click = false;
store->AddLogin(GetTestPasswordForm1(zero_click));
stub_client_.SetPasswordStore(store);
stub_client_.SetUserChosenCredential(password_manager::CredentialInfo(
GetTestPasswordForm1(zero_click),
password_manager::CredentialType::CREDENTIAL_TYPE_PASSWORD));
WaitForBackgroundTasks();
// Request a credential. The request should be resolved with the credential
// set on the stub client.
const int first_request_id = 0;
bool first_verified = false;
ExpectPromiseResolved(&first_verified, first_request_id,
GetTestWebCredential1(zero_click));
credential_manager_->CredentialsRequested(first_request_id, GURL(kTestURL),
zero_click,
std::vector<std::string>(), true);
// Pump the message loop and verify.
WaitForBackgroundTasks();
EXPECT_TRUE(first_verified);
ClearPromiseResolutionExpectations();
// Make a second request. It should be resolved again.
const int second_request_id = 1;
bool second_verified = false;
ExpectPromiseResolved(&second_verified, second_request_id,
GetTestWebCredential1(zero_click));
credential_manager_->CredentialsRequested(second_request_id, GURL(kTestURL),
zero_click,
std::vector<std::string>(), true);
// Pump the message loop and verify.
WaitForBackgroundTasks();
EXPECT_TRUE(second_verified);
}
// Tests that notifySignedIn prompts the user to save a password.
TEST_F(CredentialManagerTest,
SignInResolvesAndPromptsUserWhenSavingEnabledAndNotBlacklisted) {
// Set a password store so the PasswordFormManager can retrieve credentials.
scoped_refptr<password_manager::TestPasswordStore> store(
new password_manager::TestPasswordStore);
stub_client_.SetPasswordStore(store);
const bool saving_enabled = true;
EXPECT_CALL(stub_client_, IsSavingAndFillingEnabledForCurrentPage())
.WillOnce(Return(saving_enabled))
.WillOnce(Return(saving_enabled));
// Notify the browser that the user signed in.
const int request_id = 0;
ExpectPromiseResolved(request_id);
const bool zero_click = true;
credential_manager_->SignedIn(request_id, GURL(kTestURL),
GetTestWebCredential1(zero_click));
// Pump the message loop and verify.
WaitForBackgroundTasks();
autofill::PasswordForm expected_observed_form(
GetTestPasswordForm1(zero_click));
expected_observed_form.username_value.clear();
expected_observed_form.password_value.clear();
EXPECT_EQ(expected_observed_form, stub_client_.saved_form()->observed_form());
}
// Tests that notifySignedIn doesn't prompt the user to save a password when the
// password manager is disabled for the current page.
TEST_F(CredentialManagerTest,
SignInResolvesAndDoesNotPromptsUserWhenSavingDisabledAndNotBlacklisted) {
// Disable saving.
scoped_refptr<password_manager::TestPasswordStore> store(
new password_manager::TestPasswordStore);
stub_client_.SetPasswordStore(store);
const bool saving_enabled = false;
EXPECT_CALL(stub_client_, IsSavingAndFillingEnabledForCurrentPage())
.WillOnce(Return(saving_enabled));
// Notify the browser that the user signed in.
const int request_id = 0;
ExpectPromiseResolved(request_id);
const bool zero_click = false;
credential_manager_->SignedIn(request_id, GURL(kTestURL),
GetTestWebCredential1(zero_click));
// Pump the message loop and verify that no form was saved.
WaitForBackgroundTasks();
EXPECT_FALSE(stub_client_.saved_form());
}
// Tests that notifySignedIn doesn't prompt the user to save a password when the
// submitted form is blacklisted by the password manager.
TEST_F(CredentialManagerTest,
SignInResolvesAndDoesNotPromptUserWhenSavingEnabledAndBlacklisted) {
// Disable saving.
scoped_refptr<password_manager::TestPasswordStore> store(
new password_manager::TestPasswordStore);
stub_client_.SetPasswordStore(store);
const bool saving_enabled = true;
EXPECT_CALL(stub_client_, IsSavingAndFillingEnabledForCurrentPage())
.WillOnce(Return(saving_enabled))
.WillOnce(Return(saving_enabled));
// Save the credential that will be signed in and mark it blacklisted.
const bool zero_click = false;
autofill::PasswordForm blacklisted_form(GetTestPasswordForm1(zero_click));
blacklisted_form.blacklisted_by_user = true;
store->AddLogin(blacklisted_form);
WaitForBackgroundTasks();
// Notify the browser that the user signed in.
const int request_id = 0;
ExpectPromiseResolved(request_id);
credential_manager_->SignedIn(request_id, GURL(kTestURL),
GetTestWebCredential1(zero_click));
// Pump the message loop and verify that no form was saved.
WaitForBackgroundTasks();
EXPECT_FALSE(stub_client_.saved_form());
}
// Tests that notifySignedOut marks credentials as non-zero-click.
TEST_F(CredentialManagerTest, SignOutResolvesAndMarksFormsNonZeroClick) {
// Create two zero-click credentials for the current page.
std::string current_origin(credential_manager_->GetOrigin().spec());
autofill::PasswordForm form1(GetTestPasswordForm1(true));
form1.signon_realm = current_origin;
autofill::PasswordForm form2(GetTestPasswordForm2(true));
form2.signon_realm = current_origin;
// Add both credentials to the store.
scoped_refptr<password_manager::TestPasswordStore> store(
new password_manager::TestPasswordStore);
stub_client_.SetPasswordStore(store);
store->AddLogin(form1);
store->AddLogin(form2);
WaitForBackgroundTasks();
// Check that both credentials in the store are zero-click.
EXPECT_EQ(1U, store->stored_passwords().size());
const std::vector<autofill::PasswordForm>& passwords_before_signout =
store->stored_passwords().find(current_origin)->second;
EXPECT_EQ(2U, passwords_before_signout.size());
for (const autofill::PasswordForm& form : passwords_before_signout)
EXPECT_FALSE(form.skip_zero_click);
// Sign out the current origin, which has credentials, and a second origin,
// which doesnt.
const int first_request_id = 0;
ExpectPromiseResolved(first_request_id);
credential_manager_->SignedOut(first_request_id,
credential_manager_->GetOrigin());
const int second_request_id = 1;
ExpectPromiseResolved(second_request_id);
credential_manager_->SignedOut(second_request_id, GURL("https://foo.com"));
// Pump the message loop to let the promises resolve. Check that the
// credentials in the store are now non-zero-click and that signing out an
// origin with no credentials had no effect.
WaitForBackgroundTasks();
EXPECT_EQ(1U, store->stored_passwords().size());
const std::vector<autofill::PasswordForm>& passwords_after_signout =
store->stored_passwords().find(current_origin)->second;
EXPECT_EQ(2U, passwords_after_signout.size());
for (const autofill::PasswordForm& form : passwords_after_signout)
EXPECT_TRUE(form.skip_zero_click);
}
} // namespace