[go: up one dir, main page]

blob: 8c19a4b700980e6d469f1b8c18ed6ca83151db10 [file] [log] [blame]
// Copyright 2019 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 "components/signin/core/browser/oauth_multilogin_helper.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/test/scoped_task_environment.h"
#include "components/signin/core/browser/set_accounts_in_cookie_result.h"
#include "components/signin/core/browser/test_signin_client.h"
#include "google_apis/gaia/fake_oauth2_token_service.h"
#include "google_apis/gaia/gaia_urls.h"
#include "services/network/test/test_cookie_manager.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace signin {
namespace {
const char kAccountId[] = "account_id_1";
const char kAccountId2[] = "account_id_2";
const char kAccessToken[] = "access_token_1";
const char kAccessToken2[] = "access_token_2";
const char kExternalCcResult[] = "youtube:OK";
constexpr int kMaxFetcherRetries = 3;
const char kMultiloginSuccessResponse[] =
R"()]}'
{
"status": "OK",
"cookies":[
{
"name":"SID",
"value":"SID_value",
"domain":".google.fr",
"path":"/",
"isSecure":true,
"isHttpOnly":false,
"priority":"HIGH",
"maxAge":63070000
}
]
}
)";
const char kMultiloginSuccessResponseTwoCookies[] =
R"()]}'
{
"status": "OK",
"cookies":[
{
"name":"SID",
"value":"SID_value",
"domain":".google.fr",
"path":"/",
"isSecure":true,
"isHttpOnly":false,
"priority":"HIGH",
"maxAge":63070000
},
{
"name":"FOO",
"value":"FOO_value",
"domain":".google.com",
"path":"/",
"isSecure":true,
"isHttpOnly":false,
"priority":"HIGH",
"maxAge":63070000
}
]
}
)";
const char kMultiloginSuccessResponseWithSecondaryDomain[] =
R"()]}'
{
"status": "OK",
"cookies":[
{
"name":"SID",
"value":"SID_value",
"domain":".youtube.com",
"path":"/",
"isSecure":true,
"isHttpOnly":false,
"priority":"HIGH",
"maxAge":63070000
},
{
"name":"FOO",
"value":"FOO_value",
"domain":".google.com",
"path":"/",
"isSecure":true,
"isHttpOnly":false,
"priority":"HIGH",
"maxAge":63070000
}
]
}
)";
const char kMultiloginRetryResponse[] =
R"()]}'
{
"status": "RETRY"
}
)";
const char kMultiloginInvalidTokenResponse[] =
R"()]}'
{
"status": "INVALID_TOKENS",
"failed_accounts": [
{ "obfuscated_id": "account_id_1", "status": "RECOVERABLE" },
{ "obfuscated_id": "account_id_2", "status": "OK" }
]
}
)";
// GMock matcher that checks that the cookie has the expected parameters.
MATCHER_P3(CookieMatcher, name, value, domain, "") {
return arg.Name() == name && arg.Value() == value && arg.Domain() == domain &&
arg.Path() == "/" && arg.IsSecure() && !arg.IsHttpOnly();
}
void RunSetCookieCallbackWithSuccess(
const net::CanonicalCookie&,
const std::string&,
const net::CookieOptions&,
network::mojom::CookieManager::SetCanonicalCookieCallback callback) {
std::move(callback).Run(net::CanonicalCookie::CookieInclusionStatus::INCLUDE);
}
class MockCookieManager
: public ::testing::StrictMock<network::TestCookieManager> {
public:
MOCK_METHOD4(SetCanonicalCookie,
void(const net::CanonicalCookie& cookie,
const std::string& source_scheme,
const net::CookieOptions& cookie_options,
SetCanonicalCookieCallback callback));
};
class MockTokenService : public FakeOAuth2TokenService {
public:
MOCK_METHOD2(InvalidateTokenForMultilogin,
void(const std::string& account_id, const std::string& token));
};
} // namespace
class OAuthMultiloginHelperTest : public testing::Test {
public:
OAuthMultiloginHelperTest() : test_signin_client_(/*prefs=*/nullptr) {
std::unique_ptr<MockCookieManager> cookie_manager =
std::make_unique<MockCookieManager>();
mock_cookie_manager_ = cookie_manager.get();
test_signin_client_.set_cookie_manager(std::move(cookie_manager));
}
~OAuthMultiloginHelperTest() override = default;
std::unique_ptr<OAuthMultiloginHelper> CreateHelper(
const std::vector<std::string> account_ids) {
return std::make_unique<OAuthMultiloginHelper>(
&test_signin_client_, token_service(), account_ids, std::string(),
base::BindOnce(&OAuthMultiloginHelperTest::OnOAuthMultiloginFinished,
base::Unretained(this)));
}
std::unique_ptr<OAuthMultiloginHelper> CreateHelperWithExternalCcResult(
const std::vector<std::string> account_ids) {
return std::make_unique<OAuthMultiloginHelper>(
&test_signin_client_, token_service(), account_ids, kExternalCcResult,
base::BindOnce(&OAuthMultiloginHelperTest::OnOAuthMultiloginFinished,
base::Unretained(this)));
}
network::TestURLLoaderFactory* url_loader() {
return test_signin_client_.GetTestURLLoaderFactory();
}
std::string multilogin_url() const {
return GaiaUrls::GetInstance()->oauth_multilogin_url().spec() +
"?source=ChromiumBrowser";
}
std::string multilogin_url_with_external_cc_result() const {
return GaiaUrls::GetInstance()->oauth_multilogin_url().spec() +
"?source=ChromiumBrowser&externalCcResult=" + kExternalCcResult;
}
MockCookieManager* cookie_manager() { return mock_cookie_manager_; }
MockTokenService* token_service() { return &mock_token_service_; }
protected:
void OnOAuthMultiloginFinished(signin::SetAccountsInCookieResult result) {
DCHECK(!callback_called_);
callback_called_ = true;
result_ = result;
}
base::test::ScopedTaskEnvironment scoped_task_environment_;
bool callback_called_ = false;
signin::SetAccountsInCookieResult result_;
MockCookieManager* mock_cookie_manager_; // Owned by test_signin_client_
TestSigninClient test_signin_client_;
MockTokenService mock_token_service_;
};
// Everything succeeds.
TEST_F(OAuthMultiloginHelperTest, Success) {
token_service()->AddAccount(kAccountId);
std::unique_ptr<OAuthMultiloginHelper> helper = CreateHelper({kAccountId});
// Configure mock cookie manager:
// - check that the cookie is the expected one
// - immediately invoke the callback
EXPECT_CALL(
*cookie_manager(),
SetCanonicalCookie(CookieMatcher("SID", "SID_value", ".google.fr"),
"https", testing::_, testing::_))
.WillOnce(::testing::Invoke(RunSetCookieCallbackWithSuccess));
// Issue access token.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
// Multilogin call.
EXPECT_FALSE(callback_called_);
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->AddResponse(multilogin_url(), kMultiloginSuccessResponse);
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(signin::SetAccountsInCookieResult::kSuccess, result_);
}
// Multiple cookies in the multilogin response.
TEST_F(OAuthMultiloginHelperTest, MultipleCookies) {
token_service()->AddAccount(kAccountId);
std::unique_ptr<OAuthMultiloginHelper> helper = CreateHelper({kAccountId});
// Configure mock cookie manager:
// - check that the cookie is the expected one
// - immediately invoke the callback
EXPECT_CALL(
*cookie_manager(),
SetCanonicalCookie(CookieMatcher("SID", "SID_value", ".google.fr"),
"https", testing::_, testing::_))
.WillOnce(::testing::Invoke(RunSetCookieCallbackWithSuccess));
EXPECT_CALL(
*cookie_manager(),
SetCanonicalCookie(CookieMatcher("FOO", "FOO_value", ".google.com"),
"https", testing::_, testing::_))
.WillOnce(::testing::Invoke(RunSetCookieCallbackWithSuccess));
// Issue access token.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
// Multilogin call.
EXPECT_FALSE(callback_called_);
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->AddResponse(multilogin_url(),
kMultiloginSuccessResponseTwoCookies);
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(signin::SetAccountsInCookieResult::kSuccess, result_);
}
// Multiple cookies in the multilogin response.
TEST_F(OAuthMultiloginHelperTest, SuccessWithExternalCcResult) {
token_service()->AddAccount(kAccountId);
std::unique_ptr<OAuthMultiloginHelper> helper =
CreateHelperWithExternalCcResult({kAccountId});
// Configure mock cookie manager:
// - check that the cookie is the expected one
// - immediately invoke the callback
EXPECT_CALL(
*cookie_manager(),
SetCanonicalCookie(CookieMatcher("SID", "SID_value", ".youtube.com"),
"https", testing::_, testing::_))
.WillOnce(::testing::Invoke(RunSetCookieCallbackWithSuccess));
EXPECT_CALL(
*cookie_manager(),
SetCanonicalCookie(CookieMatcher("FOO", "FOO_value", ".google.com"),
"https", testing::_, testing::_))
.WillOnce(::testing::Invoke(RunSetCookieCallbackWithSuccess));
// Issue access token.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
// Multilogin call.
EXPECT_FALSE(callback_called_);
EXPECT_TRUE(
url_loader()->IsPending(multilogin_url_with_external_cc_result()));
url_loader()->AddResponse(multilogin_url_with_external_cc_result(),
kMultiloginSuccessResponseWithSecondaryDomain);
EXPECT_FALSE(
url_loader()->IsPending(multilogin_url_with_external_cc_result()));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(signin::SetAccountsInCookieResult::kSuccess, result_);
}
// Failure to get the access token.
TEST_F(OAuthMultiloginHelperTest, OneAccountAccessTokenFailure) {
token_service()->AddAccount(kAccountId);
std::unique_ptr<OAuthMultiloginHelper> helper = CreateHelper({kAccountId});
token_service()->IssueErrorForAllPendingRequestsForAccount(
kAccountId,
GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(signin::SetAccountsInCookieResult::kPersistentError, result_);
}
// Retry on transient errors in the multilogin call.
TEST_F(OAuthMultiloginHelperTest, OneAccountTransientMultiloginError) {
token_service()->AddAccount(kAccountId);
std::unique_ptr<OAuthMultiloginHelper> helper = CreateHelper({kAccountId});
// Configure mock cookie manager:
// - check that the cookie is the expected one
// - immediately invoke the callback
EXPECT_CALL(
*cookie_manager(),
SetCanonicalCookie(CookieMatcher("SID", "SID_value", ".google.fr"),
"https", testing::_, testing::_))
.WillOnce(::testing::Invoke(RunSetCookieCallbackWithSuccess));
// Issue access token.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
// Multilogin call fails with transient error.
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->SimulateResponseForPendingRequest(multilogin_url(),
kMultiloginRetryResponse);
// Call is retried and succeeds.
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
EXPECT_FALSE(callback_called_);
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->AddResponse(multilogin_url(), kMultiloginSuccessResponse);
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(signin::SetAccountsInCookieResult::kSuccess, result_);
}
// Stop retrying after too many transient errors in the multilogin call.
TEST_F(OAuthMultiloginHelperTest,
OneAccountTransientMultiloginErrorMaxRetries) {
token_service()->AddAccount(kAccountId);
std::unique_ptr<OAuthMultiloginHelper> helper = CreateHelper({kAccountId});
// Issue access token.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
// Multilogin call fails with transient error, retry many times.
for (int i = 0; i < kMaxFetcherRetries; ++i) {
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
EXPECT_FALSE(callback_called_);
url_loader()->SimulateResponseForPendingRequest(multilogin_url(),
kMultiloginRetryResponse);
}
// Failure after exceeding the maximum number of retries.
EXPECT_TRUE(callback_called_);
EXPECT_EQ(signin::SetAccountsInCookieResult::kTransientError, result_);
}
// Persistent error in the multilogin call.
TEST_F(OAuthMultiloginHelperTest, OneAccountPersistentMultiloginError) {
token_service()->AddAccount(kAccountId);
std::unique_ptr<OAuthMultiloginHelper> helper = CreateHelper({kAccountId});
// Issue access token.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
// Multilogin call fails with persistent error.
EXPECT_FALSE(callback_called_);
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->AddResponse(multilogin_url(), "blah"); // Unexpected response.
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(signin::SetAccountsInCookieResult::kPersistentError, result_);
}
// Retry on "invalid token" in the multilogin response.
TEST_F(OAuthMultiloginHelperTest, InvalidTokenError) {
token_service()->AddAccount(kAccountId);
token_service()->AddAccount(kAccountId2);
std::unique_ptr<OAuthMultiloginHelper> helper =
CreateHelper({kAccountId, kAccountId2});
// Configure mock cookie manager:
// - check that the cookie is the expected one
// - immediately invoke the callback
EXPECT_CALL(
*cookie_manager(),
SetCanonicalCookie(CookieMatcher("SID", "SID_value", ".google.fr"),
"https", testing::_, testing::_))
.WillOnce(::testing::Invoke(RunSetCookieCallbackWithSuccess));
// The failed access token should be invalidated.
EXPECT_CALL(*token_service(),
InvalidateTokenForMultilogin(kAccountId, kAccessToken));
// Issue access tokens.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
OAuth2AccessTokenConsumer::TokenResponse success_response_2;
success_response_2.access_token = kAccessToken2;
token_service()->IssueAllTokensForAccount(kAccountId2, success_response_2);
// Multilogin call fails with invalid token for kAccountId.
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->SimulateResponseForPendingRequest(
multilogin_url(), kMultiloginInvalidTokenResponse);
// Both tokens are retried.
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
EXPECT_FALSE(callback_called_);
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
token_service()->IssueAllTokensForAccount(kAccountId2, success_response);
// Multilogin succeeds the second time.
EXPECT_FALSE(callback_called_);
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->AddResponse(multilogin_url(), kMultiloginSuccessResponse);
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(signin::SetAccountsInCookieResult::kSuccess, result_);
}
// Retry on "invalid token" in the multilogin response.
TEST_F(OAuthMultiloginHelperTest, InvalidTokenErrorMaxRetries) {
token_service()->AddAccount(kAccountId);
token_service()->AddAccount(kAccountId2);
std::unique_ptr<OAuthMultiloginHelper> helper =
CreateHelper({kAccountId, kAccountId2});
// The failed access token should be invalidated.
EXPECT_CALL(*token_service(),
InvalidateTokenForMultilogin(kAccountId, kAccessToken))
.Times(kMaxFetcherRetries);
// Issue access tokens.
OAuth2AccessTokenConsumer::TokenResponse success_response;
success_response.access_token = kAccessToken;
OAuth2AccessTokenConsumer::TokenResponse success_response_2;
success_response_2.access_token = kAccessToken2;
// Multilogin call fails with invalid token for kAccountId. Retry many times.
for (int i = 0; i < kMaxFetcherRetries; ++i) {
token_service()->IssueAllTokensForAccount(kAccountId, success_response);
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
token_service()->IssueAllTokensForAccount(kAccountId2, success_response_2);
EXPECT_FALSE(callback_called_);
EXPECT_TRUE(url_loader()->IsPending(multilogin_url()));
url_loader()->SimulateResponseForPendingRequest(
multilogin_url(), kMultiloginInvalidTokenResponse);
}
// The maximum number of retries is reached, fail.
EXPECT_FALSE(url_loader()->IsPending(multilogin_url()));
EXPECT_TRUE(callback_called_);
EXPECT_EQ(signin::SetAccountsInCookieResult::kTransientError, result_);
}
} // namespace signin