summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordimich@chromium.org <dimich@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-04-17 17:49:19 +0000
committerdimich@chromium.org <dimich@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-04-17 17:49:19 +0000
commitdd9cc6223a64068716927bee11291cc5ae7496d4 (patch)
tree73a1d807a0a93a581cb4a2fd794b88ad6af5431f
parent667e934f8978456702d15312bd1acf17c23cb909 (diff)
downloadchromium_src-dd9cc6223a64068716927bee11291cc5ae7496d4.zip
chromium_src-dd9cc6223a64068716927bee11291cc5ae7496d4.tar.gz
chromium_src-dd9cc6223a64068716927bee11291cc5ae7496d4.tar.bz2
Identity API: getAuthToken request queues (token cache prelude)
Serializing calls to getAuthToken will eliminate unnecessary calls to GAIA and consolidate UI flows. There is are two queues of requests for each (extension, scopes) tuple, one for the non-interactive IssueToken flow and one for the interactive prompt. BUG=228908 (step #1 of the bug description) Review URL: https://codereview.chromium.org/14270007 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@194627 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/browser/extensions/api/identity/identity_api.cc138
-rw-r--r--chrome/browser/extensions/api/identity/identity_api.h45
-rw-r--r--chrome/browser/extensions/api/identity/identity_apitest.cc301
-rw-r--r--chrome/browser/extensions/api/identity/identity_mint_queue.cc80
-rw-r--r--chrome/browser/extensions/api/identity/identity_mint_queue.h73
-rw-r--r--chrome/browser/extensions/api/identity/identity_mint_queue_unittest.cc185
-rw-r--r--chrome/chrome_browser_extensions.gypi2
-rw-r--r--chrome/chrome_tests_unit.gypi1
8 files changed, 724 insertions, 101 deletions
diff --git a/chrome/browser/extensions/api/identity/identity_api.cc b/chrome/browser/extensions/api/identity/identity_api.cc
index f51f71c..479728c 100644
--- a/chrome/browser/extensions/api/identity/identity_api.cc
+++ b/chrome/browser/extensions/api/identity/identity_api.cc
@@ -4,6 +4,10 @@
#include "chrome/browser/extensions/api/identity/identity_api.h"
+#include <set>
+#include <string>
+#include <vector>
+
#include "base/lazy_instance.h"
#include "base/stringprintf.h"
#include "base/values.h"
@@ -60,6 +64,7 @@ namespace identity = api::experimental_identity;
IdentityGetAuthTokenFunction::IdentityGetAuthTokenFunction()
: should_prompt_for_scopes_(false),
should_prompt_for_signin_(false) {}
+
IdentityGetAuthTokenFunction::~IdentityGetAuthTokenFunction() {}
bool IdentityGetAuthTokenFunction::RunImpl() {
@@ -85,8 +90,7 @@ bool IdentityGetAuthTokenFunction::RunImpl() {
return false;
}
- // Balanced in OnIssueAdviceSuccess|OnMintTokenSuccess|OnMintTokenFailure|
- // InstallUIAbort|SigninFailed.
+ // Balanced in CompleteFunctionWithResult|CompleteFunctionWithError
AddRef();
if (!HasLoginToken()) {
@@ -95,28 +99,107 @@ bool IdentityGetAuthTokenFunction::RunImpl() {
Release();
return false;
}
- // Display a login prompt. If the subsequent mint fails, don't display the
- // prompt again.
- should_prompt_for_signin_ = false;
- ShowLoginPopup();
+ // Display a login prompt.
+ StartSigninFlow();
} else {
TokenService* token_service = TokenServiceFactory::GetForProfile(profile());
refresh_token_ = token_service->GetOAuth2LoginRefreshToken();
- StartFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE);
+ StartMintTokenFlow(IdentityMintRequestQueue::MINT_TYPE_NONINTERACTIVE);
}
return true;
}
-void IdentityGetAuthTokenFunction::OnMintTokenSuccess(
+void IdentityGetAuthTokenFunction::CompleteFunctionWithResult(
const std::string& access_token) {
SetResult(Value::CreateStringValue(access_token));
SendResponse(true);
Release(); // Balanced in RunImpl.
}
+void IdentityGetAuthTokenFunction::CompleteFunctionWithError(
+ const std::string& error) {
+ error_ = error;
+ SendResponse(false);
+ Release(); // Balanced in RunImpl.
+}
+
+void IdentityGetAuthTokenFunction::StartSigninFlow() {
+ // Display a login prompt. If the subsequent mint fails, don't display the
+ // login prompt again.
+ should_prompt_for_signin_ = false;
+ ShowLoginPopup();
+}
+
+void IdentityGetAuthTokenFunction::StartMintTokenFlow(
+ IdentityMintRequestQueue::MintType type) {
+ mint_token_flow_type_ = type;
+
+ // Flows are serialized to prevent excessive traffic to GAIA, and
+ // to consolidate UI pop-ups.
+ const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(GetExtension());
+ std::set<std::string> scopes(oauth2_info.scopes.begin(),
+ oauth2_info.scopes.end());
+ IdentityAPI* id_api =
+ extensions::IdentityAPI::GetFactoryInstance()->GetForProfile(profile_);
+
+ // If there is an interactive flow in progress, non-interactive
+ // requests should complete immediately since a consent UI is
+ // known to be required.
+ if (!should_prompt_for_scopes_ && !id_api->mint_queue()->empty(
+ IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE,
+ GetExtension()->id(), scopes)) {
+ CompleteFunctionWithError(identity_constants::kNoGrant);
+ return;
+ }
+ id_api->mint_queue()->RequestStart(type,
+ GetExtension()->id(),
+ scopes,
+ this);
+}
+
+void IdentityGetAuthTokenFunction::CompleteMintTokenFlow() {
+ IdentityMintRequestQueue::MintType type = mint_token_flow_type_;
+
+ const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(GetExtension());
+ std::set<std::string> scopes(oauth2_info.scopes.begin(),
+ oauth2_info.scopes.end());
+
+ extensions::IdentityAPI::GetFactoryInstance()->GetForProfile(
+ profile_)->mint_queue()->RequestComplete(type,
+ GetExtension()->id(),
+ scopes,
+ this);
+}
+
+void IdentityGetAuthTokenFunction::StartMintToken(
+ IdentityMintRequestQueue::MintType type) {
+ switch (type) {
+ case IdentityMintRequestQueue::MINT_TYPE_NONINTERACTIVE:
+ StartGaiaRequest(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE);
+ break;
+
+ case IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE:
+ install_ui_.reset(new ExtensionInstallPrompt(GetAssociatedWebContents()));
+ ShowOAuthApprovalDialog(issue_advice_);
+ break;
+
+ default:
+ NOTREACHED() << "Unexepected mint type in StartMintToken: " << type;
+ break;
+ };
+}
+
+void IdentityGetAuthTokenFunction::OnMintTokenSuccess(
+ const std::string& access_token) {
+ CompleteMintTokenFlow();
+ CompleteFunctionWithResult(access_token);
+}
+
void IdentityGetAuthTokenFunction::OnMintTokenFailure(
const GoogleServiceAuthError& error) {
+ CompleteMintTokenFlow();
+
switch (error.state()) {
case GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS:
case GoogleServiceAuthError::ACCOUNT_DELETED:
@@ -125,8 +208,7 @@ void IdentityGetAuthTokenFunction::OnMintTokenFailure(
profile())->ReportAuthError(error);
if (should_prompt_for_signin_) {
// Display a login prompt and try again (once).
- should_prompt_for_signin_ = false;
- ShowLoginPopup();
+ StartSigninFlow();
return;
}
break;
@@ -135,52 +217,48 @@ void IdentityGetAuthTokenFunction::OnMintTokenFailure(
break;
}
- error_ = std::string(identity_constants::kAuthFailure) + error.ToString();
- SendResponse(false);
- Release(); // Balanced in RunImpl.
+ CompleteFunctionWithError(
+ std::string(identity_constants::kAuthFailure) + error.ToString());
}
void IdentityGetAuthTokenFunction::OnIssueAdviceSuccess(
const IssueAdviceInfo& issue_advice) {
+ CompleteMintTokenFlow();
+
should_prompt_for_signin_ = false;
// Existing grant was revoked and we used NO_FORCE, so we got info back
// instead.
if (should_prompt_for_scopes_) {
- install_ui_.reset(new ExtensionInstallPrompt(GetAssociatedWebContents()));
- ShowOAuthApprovalDialog(issue_advice);
+ issue_advice_ = issue_advice;
+ StartMintTokenFlow(IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE);
} else {
- error_ = identity_constants::kNoGrant;
- SendResponse(false);
- Release(); // Balanced in RunImpl.
+ CompleteFunctionWithError(identity_constants::kNoGrant);
}
}
void IdentityGetAuthTokenFunction::SigninSuccess(const std::string& token) {
refresh_token_ = token;
- StartFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE);
+ StartMintTokenFlow(IdentityMintRequestQueue::MINT_TYPE_NONINTERACTIVE);
}
void IdentityGetAuthTokenFunction::SigninFailed() {
- error_ = identity_constants::kUserNotSignedIn;
- SendResponse(false);
- Release();
+ CompleteFunctionWithError(identity_constants::kUserNotSignedIn);
}
void IdentityGetAuthTokenFunction::InstallUIProceed() {
DCHECK(install_ui_->record_oauth2_grant());
// The user has accepted the scopes, so we may now force (recording a grant
// and receiving a token).
- StartFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_FORCE);
+ StartGaiaRequest(OAuth2MintTokenFlow::MODE_MINT_TOKEN_FORCE);
}
void IdentityGetAuthTokenFunction::InstallUIAbort(bool user_initiated) {
- error_ = identity_constants::kUserRejected;
- SendResponse(false);
- Release(); // Balanced in RunImpl.
+ CompleteMintTokenFlow();
+ CompleteFunctionWithError(identity_constants::kUserRejected);
}
-void IdentityGetAuthTokenFunction::StartFlow(OAuth2MintTokenFlow::Mode mode) {
- signin_flow_.reset(NULL);
+void IdentityGetAuthTokenFunction::StartGaiaRequest(
+ OAuth2MintTokenFlow::Mode mode) {
mint_token_flow_.reset(CreateMintTokenFlow(mode));
mint_token_flow_->Start();
}
@@ -339,6 +417,10 @@ void IdentityAPI::Initialize() {
content::Source<TokenService>(token_service));
}
+IdentityMintRequestQueue* IdentityAPI::mint_queue() {
+ return &mint_queue_;
+}
+
void IdentityAPI::ReportAuthError(const GoogleServiceAuthError& error) {
if (!signin_manager_)
Initialize();
diff --git a/chrome/browser/extensions/api/identity/identity_api.h b/chrome/browser/extensions/api/identity/identity_api.h
index de9ecf6..be90e32 100644
--- a/chrome/browser/extensions/api/identity/identity_api.h
+++ b/chrome/browser/extensions/api/identity/identity_api.h
@@ -5,11 +5,13 @@
#ifndef CHROME_BROWSER_EXTENSIONS_API_IDENTITY_IDENTITY_API_H_
#define CHROME_BROWSER_EXTENSIONS_API_IDENTITY_IDENTITY_API_H_
+#include <map>
#include <string>
#include <vector>
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
+#include "chrome/browser/extensions/api/identity/identity_mint_queue.h"
#include "chrome/browser/extensions/api/identity/identity_signin_flow.h"
#include "chrome/browser/extensions/api/identity/web_auth_flow.h"
#include "chrome/browser/extensions/api/profile_keyed_api_factory.h"
@@ -36,9 +38,27 @@ extern const char kInteractionRequired[];
extern const char kInvalidRedirect[];
} // namespace identity_constants
+// identity.getAuthToken fetches an OAuth 2 function for the
+// caller. The request has three sub-flows: non-interactive,
+// interactive, and sign-in.
+//
+// In the non-interactive flow, getAuthToken requests a token from
+// GAIA. GAIA may respond with a token, an error, or "consent
+// required". In the consent required cases, getAuthToken proceeds to
+// the second, interactive phase.
+//
+// The interactive flow presents a scope approval dialog to the
+// user. If the user approves the request, a grant will be recorded on
+// the server, and an access token will be returned to the caller.
+//
+// In some cases we need to display a sign-in dialog. Normally the
+// profile will be signed in already, but if it turns out we need a
+// new login token, there is a sign-in flow. If that flow completes
+// successfully, getAuthToken proceeds to the non-interactive flow.
class IdentityGetAuthTokenFunction : public AsyncExtensionFunction,
- public OAuth2MintTokenFlow::Delegate,
public ExtensionInstallPrompt::Delegate,
+ public IdentityMintRequestQueue::Request,
+ public OAuth2MintTokenFlow::Delegate,
public IdentitySigninFlow::Delegate {
public:
DECLARE_EXTENSION_FUNCTION("experimental.identity.getAuthToken",
@@ -56,6 +76,18 @@ class IdentityGetAuthTokenFunction : public AsyncExtensionFunction,
// ExtensionFunction:
virtual bool RunImpl() OVERRIDE;
+ // Helpers to report async function results to the caller.
+ void CompleteFunctionWithResult(const std::string& access_token);
+ void CompleteFunctionWithError(const std::string& error);
+
+ // Initiate/complete the sub-flows.
+ void StartSigninFlow();
+ void StartMintTokenFlow(IdentityMintRequestQueue::MintType type);
+ void CompleteMintTokenFlow();
+
+ // IdentityMintRequestQueue::Request implementation:
+ virtual void StartMintToken(IdentityMintRequestQueue::MintType type) OVERRIDE;
+
// OAuth2MintTokenFlow::Delegate implementation:
virtual void OnMintTokenSuccess(const std::string& access_token) OVERRIDE;
virtual void OnMintTokenFailure(
@@ -71,9 +103,10 @@ class IdentityGetAuthTokenFunction : public AsyncExtensionFunction,
virtual void InstallUIProceed() OVERRIDE;
virtual void InstallUIAbort(bool user_initiated) OVERRIDE;
- // Starts a MintTokenFlow with the given mode.
- void StartFlow(OAuth2MintTokenFlow::Mode mode);
+ // Starts a mint token request to GAIA.
+ void StartGaiaRequest(OAuth2MintTokenFlow::Mode mode);
+ // Methods for invoking UI. Overridable for testing.
virtual void ShowLoginPopup();
virtual void ShowOAuthApprovalDialog(const IssueAdviceInfo& issue_advice);
// Caller owns the returned instance.
@@ -84,12 +117,14 @@ class IdentityGetAuthTokenFunction : public AsyncExtensionFunction,
virtual bool HasLoginToken() const;
bool should_prompt_for_scopes_;
+ IdentityMintRequestQueue::MintType mint_token_flow_type_;
scoped_ptr<OAuth2MintTokenFlow> mint_token_flow_;
std::string refresh_token_;
bool should_prompt_for_signin_;
// When launched in interactive mode, and if there is no existing grant,
// a permissions prompt will be popped up to the user.
+ IssueAdviceInfo issue_advice_;
scoped_ptr<ExtensionInstallPrompt> install_ui_;
scoped_ptr<IdentitySigninFlow> signin_flow_;
};
@@ -132,6 +167,9 @@ class IdentityAPI : public ProfileKeyedAPI,
virtual ~IdentityAPI();
void Initialize();
+ // Request serialization queue for getAuthToken.
+ IdentityMintRequestQueue* mint_queue();
+
void ReportAuthError(const GoogleServiceAuthError& error);
// ProfileKeyedAPI implementation.
@@ -160,6 +198,7 @@ class IdentityAPI : public ProfileKeyedAPI,
GoogleServiceAuthError error_;
// Used to listen to notifications from the TokenService.
content::NotificationRegistrar registrar_;
+ IdentityMintRequestQueue mint_queue_;
};
template <>
diff --git a/chrome/browser/extensions/api/identity/identity_apitest.cc b/chrome/browser/extensions/api/identity/identity_apitest.cc
index b2ef334..9bf130d 100644
--- a/chrome/browser/extensions/api/identity/identity_apitest.cc
+++ b/chrome/browser/extensions/api/identity/identity_apitest.cc
@@ -38,6 +38,102 @@ namespace utils = extension_function_test_utils;
static const char kAccessToken[] = "auth_token";
+// This helps us be able to wait until an AsyncExtensionFunction calls
+// SendResponse.
+class SendResponseDelegate
+ : public UIThreadExtensionFunction::DelegateForTests {
+ public:
+ SendResponseDelegate() : should_post_quit_(false) {}
+
+ virtual ~SendResponseDelegate() {}
+
+ void set_should_post_quit(bool should_quit) {
+ should_post_quit_ = should_quit;
+ }
+
+ bool HasResponse() {
+ return response_.get() != NULL;
+ }
+
+ bool GetResponse() {
+ EXPECT_TRUE(HasResponse());
+ return *response_.get();
+ }
+
+ virtual void OnSendResponse(UIThreadExtensionFunction* function,
+ bool success,
+ bool bad_message) OVERRIDE {
+ ASSERT_FALSE(bad_message);
+ ASSERT_FALSE(HasResponse());
+ response_.reset(new bool);
+ *response_ = success;
+ if (should_post_quit_) {
+ MessageLoopForUI::current()->Quit();
+ }
+ }
+
+ private:
+ scoped_ptr<bool> response_;
+ bool should_post_quit_;
+};
+
+class AsyncExtensionBrowserTest : public ExtensionBrowserTest {
+ protected:
+ // Asynchronous function runner allows tests to manipulate the browser window
+ // after the call happens.
+ void RunFunctionAsync(
+ UIThreadExtensionFunction* function,
+ const std::string& args) {
+ response_delegate_.reset(new SendResponseDelegate);
+ function->set_test_delegate(response_delegate_.get());
+ scoped_ptr<base::ListValue> parsed_args(utils::ParseList(args));
+ EXPECT_TRUE(parsed_args.get()) <<
+ "Could not parse extension function arguments: " << args;
+ function->SetArgs(parsed_args.get());
+
+ if (!function->GetExtension()) {
+ scoped_refptr<Extension> empty_extension(
+ utils::CreateEmptyExtension());
+ function->set_extension(empty_extension.get());
+ }
+
+ function->set_profile(browser()->profile());
+ function->set_has_callback(true);
+ function->Run();
+ }
+
+ std::string WaitForError(UIThreadExtensionFunction* function) {
+ RunMessageLoopUntilResponse();
+ EXPECT_FALSE(function->GetResultList()) << "Did not expect a result";
+ return function->GetError();
+ }
+
+ base::Value* WaitForSingleResult(UIThreadExtensionFunction* function) {
+ RunMessageLoopUntilResponse();
+ EXPECT_TRUE(function->GetError().empty()) << "Unexpected error: "
+ << function->GetError();
+ const base::Value* single_result = NULL;
+ if (function->GetResultList() != NULL &&
+ function->GetResultList()->Get(0, &single_result)) {
+ return single_result->DeepCopy();
+ }
+ return NULL;
+ }
+
+ private:
+ void RunMessageLoopUntilResponse() {
+ // If the RunImpl of |function| didn't already call SendResponse, run the
+ // message loop until they do.
+ if (!response_delegate_->HasResponse()) {
+ response_delegate_->set_should_post_quit(true);
+ content::RunMessageLoop();
+ }
+ EXPECT_TRUE(response_delegate_->HasResponse());
+ }
+
+ scoped_ptr<SendResponseDelegate> response_delegate_;
+};
+
class TestOAuth2MintTokenFlow : public OAuth2MintTokenFlow {
public:
enum ResultType {
@@ -84,6 +180,10 @@ class TestOAuth2MintTokenFlow : public OAuth2MintTokenFlow {
OAuth2MintTokenFlow::Delegate* delegate_;
};
+ProfileKeyedService* IdentityAPITestFactory(Profile* profile) {
+ return new IdentityAPI(profile);
+}
+
} // namespace
class MockGetAuthTokenFunction : public IdentityGetAuthTokenFunction {
@@ -142,7 +242,12 @@ class MockGetAuthTokenFunction : public IdentityGetAuthTokenFunction {
bool install_ui_shown_;
};
-class GetAuthTokenFunctionTest : public ExtensionBrowserTest {
+class MockQueuedMintRequest : public IdentityMintRequestQueue::Request {
+ public:
+ MOCK_METHOD1(StartMintToken, void(IdentityMintRequestQueue::MintType));
+};
+
+class GetAuthTokenFunctionTest : public AsyncExtensionBrowserTest {
protected:
enum OAuth2Fields {
NONE = 0,
@@ -167,6 +272,11 @@ class GetAuthTokenFunctionTest : public ExtensionBrowserTest {
}
return ext;
}
+
+ void InitializeTestAPIFactory() {
+ IdentityAPI::GetFactoryInstance()->SetTestingFactory(
+ browser()->profile(), &IdentityAPITestFactory);
+ }
};
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
@@ -461,46 +571,133 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
EXPECT_TRUE(func->install_ui_shown());
}
-// This helps us be able to wait until an AsyncExtensionFunction calls
-// SendResponse.
-class SendResponseDelegate
- : public UIThreadExtensionFunction::DelegateForTests {
- public:
- SendResponseDelegate() : should_post_quit_(false) {}
+IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, NoninteractiveQueue) {
+ InitializeTestAPIFactory();
+ scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES));
+ scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction());
+ func->set_extension(extension);
+
+ // Create a fake request to block the queue.
+ const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(extension);
+ std::set<std::string> scopes(oauth2_info.scopes.begin(),
+ oauth2_info.scopes.end());
+ IdentityAPI* id_api =
+ extensions::IdentityAPI::GetFactoryInstance()->GetForProfile(
+ browser()->profile());
+ IdentityMintRequestQueue* queue = id_api->mint_queue();
+ MockQueuedMintRequest queued_request;
+ IdentityMintRequestQueue::MintType type =
+ IdentityMintRequestQueue::MINT_TYPE_NONINTERACTIVE;
+
+ EXPECT_CALL(queued_request, StartMintToken(type)).Times(1);
+ queue->RequestStart(type, extension->id(), scopes, &queued_request);
+
+ // The real request will start processing, but wait in the queue behind
+ // the blocker.
+ EXPECT_CALL(*func.get(), HasLoginToken()).WillOnce(Return(true));
+ RunFunctionAsync(func, "[{}]");
+ // Verify that we have fetched the login token at this point.
+ testing::Mock::VerifyAndClearExpectations(func);
+
+ // The flow will be created after the first queued request clears.
+ TestOAuth2MintTokenFlow* flow = new TestOAuth2MintTokenFlow(
+ TestOAuth2MintTokenFlow::MINT_TOKEN_SUCCESS, func.get());
+ EXPECT_CALL(*func.get(), CreateMintTokenFlow(_)).WillOnce(Return(flow));
- virtual ~SendResponseDelegate() {}
+ queue->RequestComplete(type, extension->id(), scopes, &queued_request);
- void set_should_post_quit(bool should_quit) {
- should_post_quit_ = should_quit;
- }
+ scoped_ptr<base::Value> value(WaitForSingleResult(func));
+ std::string access_token;
+ EXPECT_TRUE(value->GetAsString(&access_token));
+ EXPECT_EQ(std::string(kAccessToken), access_token);
+ EXPECT_FALSE(func->login_ui_shown());
+ EXPECT_FALSE(func->install_ui_shown());
+}
- bool HasResponse() {
- return response_.get() != NULL;
- }
+IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, InteractiveQueue) {
+ InitializeTestAPIFactory();
+ scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES));
+ scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction());
+ func->set_extension(extension);
+
+ // Create a fake request to block the queue.
+ const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(extension);
+ std::set<std::string> scopes(oauth2_info.scopes.begin(),
+ oauth2_info.scopes.end());
+ IdentityAPI* id_api =
+ extensions::IdentityAPI::GetFactoryInstance()->GetForProfile(
+ browser()->profile());
+ IdentityMintRequestQueue* queue = id_api->mint_queue();
+ MockQueuedMintRequest queued_request;
+ IdentityMintRequestQueue::MintType type =
+ IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE;
+
+ EXPECT_CALL(queued_request, StartMintToken(type)).Times(1);
+ queue->RequestStart(type, extension->id(), scopes, &queued_request);
+
+ // The real request will start processing, but wait in the queue behind
+ // the blocker.
+ EXPECT_CALL(*func.get(), HasLoginToken()).WillOnce(Return(true));
+ TestOAuth2MintTokenFlow* flow1 = new TestOAuth2MintTokenFlow(
+ TestOAuth2MintTokenFlow::ISSUE_ADVICE_SUCCESS, func.get());
+ EXPECT_CALL(*func.get(), CreateMintTokenFlow(_)).WillOnce(Return(flow1));
+ RunFunctionAsync(func, "[{\"interactive\": true}]");
+ // Verify that we have fetched the login token and run the first flow.
+ testing::Mock::VerifyAndClearExpectations(func);
+ EXPECT_FALSE(func->install_ui_shown());
- bool GetResponse() {
- EXPECT_TRUE(HasResponse());
- return *response_.get();
- }
+ // The UI will be displayed and the second flow will be created
+ // after the first queued request clears.
+ func->set_install_ui_result(true);
+ TestOAuth2MintTokenFlow* flow2 = new TestOAuth2MintTokenFlow(
+ TestOAuth2MintTokenFlow::MINT_TOKEN_SUCCESS, func.get());
+ EXPECT_CALL(*func.get(), CreateMintTokenFlow(_)).WillOnce(Return(flow2));
- virtual void OnSendResponse(UIThreadExtensionFunction* function,
- bool success,
- bool bad_message) OVERRIDE {
- ASSERT_FALSE(bad_message);
- ASSERT_FALSE(HasResponse());
- response_.reset(new bool);
- *response_ = success;
- if (should_post_quit_) {
- MessageLoopForUI::current()->Quit();
- }
- }
+ queue->RequestComplete(type, extension->id(), scopes, &queued_request);
- private:
- scoped_ptr<bool> response_;
- bool should_post_quit_;
-};
+ scoped_ptr<base::Value> value(WaitForSingleResult(func));
+ std::string access_token;
+ EXPECT_TRUE(value->GetAsString(&access_token));
+ EXPECT_EQ(std::string(kAccessToken), access_token);
+ EXPECT_FALSE(func->login_ui_shown());
+ EXPECT_TRUE(func->install_ui_shown());
+}
+
+IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
+ InteractiveQueuedNoninteractiveFails) {
+ InitializeTestAPIFactory();
+ scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES));
+ scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction());
+ func->set_extension(extension);
+
+ // Create a fake request to block the interactive queue.
+ const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(extension);
+ std::set<std::string> scopes(oauth2_info.scopes.begin(),
+ oauth2_info.scopes.end());
+ IdentityAPI* id_api =
+ extensions::IdentityAPI::GetFactoryInstance()->GetForProfile(
+ browser()->profile());
+ IdentityMintRequestQueue* queue = id_api->mint_queue();
+ MockQueuedMintRequest queued_request;
+ IdentityMintRequestQueue::MintType type =
+ IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE;
+
+ EXPECT_CALL(queued_request, StartMintToken(type)).Times(1);
+ queue->RequestStart(type, extension->id(), scopes, &queued_request);
+
+ // Non-interactive requests fail without hitting GAIA, because a
+ // consent UI is known to be up.
+ EXPECT_CALL(*func.get(), HasLoginToken()).WillOnce(Return(true));
+ std::string error = utils::RunFunctionAndReturnError(
+ func.get(), "[{}]", browser());
+ EXPECT_EQ(std::string(errors::kNoGrant), error);
+ EXPECT_FALSE(func->login_ui_shown());
+ EXPECT_FALSE(func->install_ui_shown());
+
+ queue->RequestComplete(type, extension->id(), scopes, &queued_request);
+}
-class LaunchWebAuthFlowFunctionTest : public ExtensionBrowserTest {
+class LaunchWebAuthFlowFunctionTest : public AsyncExtensionBrowserTest {
protected:
void RunAndCheckBounds(
const std::string& extra_params,
@@ -534,42 +731,6 @@ class LaunchWebAuthFlowFunctionTest : public ExtensionBrowserTest {
web_auth_flow_browser->window()->Close();
}
-
- // Asynchronous function runner allows tests to manipulate the browser window
- // after the call happens.
- void RunFunctionAsync(
- UIThreadExtensionFunction* function,
- const std::string& args) {
- response_delegate_.reset(new SendResponseDelegate);
- function->set_test_delegate(response_delegate_.get());
- scoped_ptr<base::ListValue> parsed_args(utils::ParseList(args));
- EXPECT_TRUE(parsed_args.get()) <<
- "Could not parse extension function arguments: " << args;
- function->SetArgs(parsed_args.get());
-
- scoped_refptr<Extension> empty_extension(
- utils::CreateEmptyExtension());
- function->set_extension(empty_extension.get());
-
- function->set_profile(browser()->profile());
- function->set_has_callback(true);
- function->Run();
- }
-
- std::string WaitForError(UIThreadExtensionFunction* function) {
- // If the RunImpl of |function| didn't already call SendResponse, run the
- // message loop until they do.
- if (!response_delegate_->HasResponse()) {
- response_delegate_->set_should_post_quit(true);
- content::RunMessageLoop();
- }
-
- EXPECT_TRUE(response_delegate_->HasResponse());
- return function->GetError();
- }
-
- private:
- scoped_ptr<SendResponseDelegate> response_delegate_;
};
IN_PROC_BROWSER_TEST_F(LaunchWebAuthFlowFunctionTest, Bounds) {
diff --git a/chrome/browser/extensions/api/identity/identity_mint_queue.cc b/chrome/browser/extensions/api/identity/identity_mint_queue.cc
new file mode 100644
index 0000000..3d206df
--- /dev/null
+++ b/chrome/browser/extensions/api/identity/identity_mint_queue.cc
@@ -0,0 +1,80 @@
+// Copyright (c) 2013 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 "chrome/browser/extensions/api/identity/identity_mint_queue.h"
+
+#include "base/logging.h"
+#include "base/stl_util.h"
+
+namespace extensions {
+
+IdentityMintRequestQueue::IdentityMintRequestQueue() {
+}
+
+IdentityMintRequestQueue::~IdentityMintRequestQueue() {
+ std::map<RequestKey, RequestList>::const_iterator it;
+ for (it = request_queue_.begin(); it != request_queue_.end(); ++it)
+ DCHECK_EQ(it->second.size(), 0lu);
+}
+
+IdentityMintRequestQueue::RequestKey::RequestKey(
+ IdentityMintRequestQueue::MintType type,
+ const std::string& extension_id,
+ const std::set<std::string> scopes) : type(type),
+ extension_id(extension_id),
+ scopes(scopes) {
+}
+
+IdentityMintRequestQueue::RequestKey::~RequestKey() {
+}
+
+bool IdentityMintRequestQueue::RequestKey::operator<(
+ const RequestKey& rhs) const {
+ if (type < rhs.type)
+ return true;
+ else if (rhs.type < type)
+ return false;
+
+ if (extension_id < rhs.extension_id)
+ return true;
+ else if (rhs.extension_id < extension_id)
+ return false;
+
+ return scopes < rhs.scopes;
+}
+
+void IdentityMintRequestQueue::RequestStart(
+ IdentityMintRequestQueue::MintType type,
+ const std::string& extension_id,
+ const std::set<std::string> scopes,
+ IdentityMintRequestQueue::Request* request) {
+ RequestKey key(type, extension_id, scopes);
+ request_queue_[key].push_back(request);
+ // If this is the first request, start it now. RequestComplete will start
+ // all other requests.
+ if (request_queue_[key].size() == 1)
+ request_queue_[key].front()->StartMintToken(type);
+}
+
+void IdentityMintRequestQueue::RequestComplete(
+ IdentityMintRequestQueue::MintType type,
+ const std::string& extension_id,
+ const std::set<std::string> scopes,
+ IdentityMintRequestQueue::Request* request) {
+ RequestKey key(type, extension_id, scopes);
+ CHECK(request_queue_[key].front() == request);
+ request_queue_[key].pop_front();
+ if (request_queue_[key].size() > 0)
+ request_queue_[key].front()->StartMintToken(type);
+}
+
+bool IdentityMintRequestQueue::empty(IdentityMintRequestQueue::MintType type,
+ const std::string& extension_id,
+ const std::set<std::string> scopes) const {
+ RequestKey key(type, extension_id, scopes);
+ return !ContainsKey(request_queue_, key) ||
+ (request_queue_.find(key))->second.empty();
+}
+
+} // namespace extensions
diff --git a/chrome/browser/extensions/api/identity/identity_mint_queue.h b/chrome/browser/extensions/api/identity/identity_mint_queue.h
new file mode 100644
index 0000000..f014998
--- /dev/null
+++ b/chrome/browser/extensions/api/identity/identity_mint_queue.h
@@ -0,0 +1,73 @@
+// Copyright (c) 2012 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.
+
+#ifndef CHROME_BROWSER_EXTENSIONS_API_IDENTITY_IDENTITY_MINT_QUEUE_H_
+#define CHROME_BROWSER_EXTENSIONS_API_IDENTITY_IDENTITY_MINT_QUEUE_H_
+
+#include <list>
+#include <map>
+#include <set>
+#include <string>
+
+namespace extensions {
+
+// getAuthToken requests are serialized to avoid excessive traffic to
+// GAIA and to consolidate UI pop-ups. IdentityMintRequestQueue
+// maitains a set of queues, one for each RequestKey.
+//
+// The queue calls StartMintToken on each Request when it reaches the
+// head of the line.
+//
+// The queue does not own Requests. Request pointers must be valid
+// until they are removed from the queue with RequestComplete.
+class IdentityMintRequestQueue {
+ public:
+ enum MintType {
+ MINT_TYPE_NONINTERACTIVE,
+ MINT_TYPE_INTERACTIVE
+ };
+
+ IdentityMintRequestQueue();
+ virtual ~IdentityMintRequestQueue();
+
+ class Request {
+ public:
+ virtual ~Request() {}
+ virtual void StartMintToken(IdentityMintRequestQueue::MintType type) = 0;
+ };
+
+ struct RequestKey {
+ RequestKey(IdentityMintRequestQueue::MintType type,
+ const std::string& extension_id,
+ const std::set<std::string> scopes);
+ ~RequestKey();
+ bool operator<(const RequestKey& rhs) const;
+ IdentityMintRequestQueue::MintType type;
+ std::string extension_id;
+ std::set<std::string> scopes;
+ };
+
+ // Adds a request to the queue specified by the id and scopes.
+ void RequestStart(IdentityMintRequestQueue::MintType type,
+ const std::string& extension_id,
+ const std::set<std::string> scopes,
+ IdentityMintRequestQueue::Request* request);
+ // Removes a request from the queue specified by the id and scopes.
+ void RequestComplete(IdentityMintRequestQueue::MintType type,
+ const std::string& extension_id,
+ const std::set<std::string> scopes,
+ IdentityMintRequestQueue::Request* request);
+ bool empty(IdentityMintRequestQueue::MintType type,
+ const std::string& extension_id,
+ const std::set<std::string> scopes) const;
+
+ private:
+ typedef std::list<IdentityMintRequestQueue::Request*> RequestList;
+ std::map<RequestKey, RequestList> request_queue_;
+};
+
+
+} // namespace extensions
+
+#endif // CHROME_BROWSER_EXTENSIONS_API_IDENTITY_IDENTITY_MINT_QUEUE_H_
diff --git a/chrome/browser/extensions/api/identity/identity_mint_queue_unittest.cc b/chrome/browser/extensions/api/identity/identity_mint_queue_unittest.cc
new file mode 100644
index 0000000..dc2128e
--- /dev/null
+++ b/chrome/browser/extensions/api/identity/identity_mint_queue_unittest.cc
@@ -0,0 +1,185 @@
+// Copyright (c) 2013 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 "chrome/browser/extensions/api/identity/identity_mint_queue.h"
+
+#include <vector>
+
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using extensions::IdentityMintRequestQueue;
+
+namespace {
+
+class MockRequest : public extensions::IdentityMintRequestQueue::Request {
+ public:
+ MOCK_METHOD1(StartMintToken, void(IdentityMintRequestQueue::MintType));
+};
+
+} // namespace
+
+TEST(IdentityMintQueueTest, SerialRequests) {
+ IdentityMintRequestQueue::MintType type =
+ IdentityMintRequestQueue::MINT_TYPE_NONINTERACTIVE;
+ IdentityMintRequestQueue queue;
+ std::string extension_id("ext_id");
+ MockRequest request1;
+ MockRequest request2;
+
+ EXPECT_CALL(request1, StartMintToken(type)).Times(1);
+ queue.RequestStart(type, extension_id, std::set<std::string>(), &request1);
+ queue.RequestComplete(type, extension_id, std::set<std::string>(), &request1);
+
+ EXPECT_CALL(request2, StartMintToken(type)).Times(1);
+ queue.RequestStart(type, extension_id, std::set<std::string>(), &request2);
+ queue.RequestComplete(type, extension_id, std::set<std::string>(), &request2);
+}
+
+TEST(IdentityMintQueueTest, InteractiveType) {
+ IdentityMintRequestQueue::MintType type =
+ IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE;
+ IdentityMintRequestQueue queue;
+ std::string extension_id("ext_id");
+ MockRequest request1;
+
+ EXPECT_CALL(request1, StartMintToken(type)).Times(1);
+ queue.RequestStart(type, extension_id, std::set<std::string>(), &request1);
+ queue.RequestComplete(type, extension_id, std::set<std::string>(), &request1);
+}
+
+TEST(IdentityMintQueueTest, ParallelRequests) {
+ IdentityMintRequestQueue::MintType type =
+ IdentityMintRequestQueue::MINT_TYPE_NONINTERACTIVE;
+ IdentityMintRequestQueue queue;
+ std::string extension_id("ext_id");
+ MockRequest request1;
+ MockRequest request2;
+ MockRequest request3;
+
+ EXPECT_CALL(request1, StartMintToken(type)).Times(1);
+ queue.RequestStart(type, extension_id, std::set<std::string>(), &request1);
+ queue.RequestStart(type, extension_id, std::set<std::string>(), &request2);
+ queue.RequestStart(type, extension_id, std::set<std::string>(), &request3);
+
+ EXPECT_CALL(request2, StartMintToken(type)).Times(1);
+ queue.RequestComplete(type, extension_id, std::set<std::string>(), &request1);
+
+ EXPECT_CALL(request3, StartMintToken(type)).Times(1);
+ queue.RequestComplete(type, extension_id, std::set<std::string>(), &request2);
+
+ queue.RequestComplete(type, extension_id, std::set<std::string>(), &request3);
+}
+
+TEST(IdentityMintQueueTest, ParallelRequestsFromTwoExtensions) {
+ IdentityMintRequestQueue::MintType type =
+ IdentityMintRequestQueue::MINT_TYPE_NONINTERACTIVE;
+ IdentityMintRequestQueue queue;
+ std::string extension_id1("ext_id_1");
+ std::string extension_id2("ext_id_2");
+ MockRequest request1;
+ MockRequest request2;
+
+ EXPECT_CALL(request1, StartMintToken(type)).Times(1);
+ EXPECT_CALL(request2, StartMintToken(type)).Times(1);
+ queue.RequestStart(type, extension_id1, std::set<std::string>(), &request1);
+ queue.RequestStart(type, extension_id2, std::set<std::string>(), &request2);
+
+ queue.RequestComplete(type, extension_id1,
+ std::set<std::string>(), &request1);
+ queue.RequestComplete(type, extension_id2,
+ std::set<std::string>(), &request2);
+}
+
+TEST(IdentityMintQueueTest, ParallelRequestsForDifferentScopes) {
+ IdentityMintRequestQueue::MintType type =
+ IdentityMintRequestQueue::MINT_TYPE_NONINTERACTIVE;
+ IdentityMintRequestQueue queue;
+ std::string extension_id("ext_id");
+ MockRequest request1;
+ MockRequest request2;
+ std::set<std::string> scopes1;
+ std::set<std::string> scopes2;
+
+ scopes1.insert("a");
+ scopes1.insert("b");
+ scopes2.insert("a");
+
+ EXPECT_CALL(request1, StartMintToken(type)).Times(1);
+ EXPECT_CALL(request2, StartMintToken(type)).Times(1);
+ queue.RequestStart(type, extension_id, scopes1, &request1);
+ queue.RequestStart(type, extension_id, scopes2, &request2);
+
+ queue.RequestComplete(type, extension_id, scopes1, &request1);
+ queue.RequestComplete(type, extension_id, scopes2, &request2);
+}
+
+TEST(IdentityMintQueueTest, KeyComparisons) {
+ std::string extension_id1("ext_id_1");
+ std::string extension_id2("ext_id_2");
+ std::set<std::string> scopes1;
+ std::set<std::string> scopes2;
+ std::set<std::string> scopes3;
+
+ scopes1.insert("a");
+ scopes1.insert("b");
+ scopes2.insert("a");
+
+ std::vector<std::string> ids;
+ ids.push_back(extension_id1);
+ ids.push_back(extension_id2);
+
+ std::vector<std::set<std::string> > scopesets;
+ scopesets.push_back(scopes1);
+ scopesets.push_back(scopes2);
+ scopesets.push_back(scopes3);
+
+ std::vector<IdentityMintRequestQueue::RequestKey> keys;
+ typedef std::vector<
+ IdentityMintRequestQueue::RequestKey>::const_iterator
+ RequestKeyIterator;
+
+ std::vector<std::string>::const_iterator id_it;
+ std::vector<std::set<std::string> >::const_iterator scope_it;
+
+ for (id_it = ids.begin(); id_it != ids.end(); ++id_it) {
+ for (scope_it = scopesets.begin(); scope_it != scopesets.end();
+ ++scope_it) {
+ keys.push_back(IdentityMintRequestQueue::RequestKey(
+ IdentityMintRequestQueue::MINT_TYPE_NONINTERACTIVE, *id_it,
+ *scope_it));
+ keys.push_back(IdentityMintRequestQueue::RequestKey(
+ IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE, *id_it, *scope_it));
+ }
+ }
+
+ // keys should not be less than themselves
+ for (RequestKeyIterator it = keys.begin(); it != keys.end(); ++it) {
+ EXPECT_FALSE(*it < *it);
+ }
+
+ // keys should not equal different keys
+ for (RequestKeyIterator it1 = keys.begin(); it1 != keys.end(); ++it1) {
+ RequestKeyIterator it2 = it1;
+ for (++it2; it2 != keys.end(); ++it2) {
+ EXPECT_TRUE(*it1 < *it2 || *it2 < *it1);
+ EXPECT_FALSE(*it1 < *it2 && *it2 < *it1);
+ }
+ }
+}
+
+TEST(IdentityMintQueueTest, Empty) {
+ IdentityMintRequestQueue::MintType type =
+ IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE;
+ IdentityMintRequestQueue queue;
+ std::string extension_id("ext_id");
+ MockRequest request1;
+
+ EXPECT_TRUE(queue.empty(type, extension_id, std::set<std::string>()));
+ EXPECT_CALL(request1, StartMintToken(type)).Times(1);
+ queue.RequestStart(type, extension_id, std::set<std::string>(), &request1);
+ EXPECT_FALSE(queue.empty(type, extension_id, std::set<std::string>()));
+ queue.RequestComplete(type, extension_id, std::set<std::string>(), &request1);
+ EXPECT_TRUE(queue.empty(type, extension_id, std::set<std::string>()));
+}
diff --git a/chrome/chrome_browser_extensions.gypi b/chrome/chrome_browser_extensions.gypi
index 7484943..a1f7fab 100644
--- a/chrome/chrome_browser_extensions.gypi
+++ b/chrome/chrome_browser_extensions.gypi
@@ -222,6 +222,8 @@
'browser/extensions/api/i18n/i18n_api.h',
'browser/extensions/api/identity/identity_api.cc',
'browser/extensions/api/identity/identity_api.h',
+ 'browser/extensions/api/identity/identity_mint_queue.cc',
+ 'browser/extensions/api/identity/identity_mint_queue.h',
'browser/extensions/api/identity/identity_signin_flow.cc',
'browser/extensions/api/identity/identity_signin_flow.h',
'browser/extensions/api/identity/web_auth_flow.cc',
diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi
index e8ed314..afffa32 100644
--- a/chrome/chrome_tests_unit.gypi
+++ b/chrome/chrome_tests_unit.gypi
@@ -705,6 +705,7 @@
'browser/extensions/api/discovery/discovery_api_unittest.cc',
'browser/extensions/api/extension_action/extension_browser_actions_api_unittest.cc',
'browser/extensions/api/file_system/file_system_api_unittest.cc',
+ 'browser/extensions/api/identity/identity_mint_queue_unittest.cc',
'browser/extensions/api/identity/web_auth_flow_unittest.cc',
'browser/extensions/api/idle/idle_api_unittest.cc',
'browser/extensions/api/messaging/native_message_process_host_unittest.cc',