blob: e0b587ea8fe77f4070515bad8d23f9780f603b98 [file] [log] [blame]
// 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.
//
// A complete set of unit tests for OAuth2MintTokenFlow.
#include "google_apis/gaia/oauth2_mint_token_flow.h"
#include <memory>
#include <string>
#include <vector>
#include "base/json/json_reader.h"
#include "base/optional.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "google_apis/gaia/oauth2_access_token_fetcher.h"
#include "net/base/net_errors.h"
#include "net/cookies/canonical_cookie.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using testing::_;
using testing::ByRef;
using testing::Eq;
using testing::StrictMock;
namespace {
static const char kValidTokenResponse[] =
"{"
" \"token\": \"at1\","
" \"issueAdvice\": \"Auto\","
" \"expiresIn\": \"3600\""
"}";
static const char kTokenResponseNoAccessToken[] =
"{"
" \"issueAdvice\": \"Auto\""
"}";
static const char kValidIssueAdviceResponse[] =
"{"
" \"issueAdvice\": \"consent\","
" \"consent\": {"
" \"oauthClient\": {"
" \"name\": \"Test app\","
" \"iconUri\": \"\","
" \"developerEmail\": \"munjal@chromium.org\""
" },"
" \"scopes\": ["
" {"
" \"description\": \"Manage your calendars\","
" \"detail\": \"\nView and manage your calendars\n\""
" },"
" {"
" \"description\": \"Manage your documents\","
" \"detail\": \"\nView your documents\nUpload new documents\n\""
" }"
" ]"
" }"
"}";
static const char kIssueAdviceResponseNoDescription[] =
"{"
" \"issueAdvice\": \"consent\","
" \"consent\": {"
" \"oauthClient\": {"
" \"name\": \"Test app\","
" \"iconUri\": \"\","
" \"developerEmail\": \"munjal@chromium.org\""
" },"
" \"scopes\": ["
" {"
" \"description\": \"Manage your calendars\","
" \"detail\": \"\nView and manage your calendars\n\""
" },"
" {"
" \"detail\": \"\nView your documents\nUpload new documents\n\""
" }"
" ]"
" }"
"}";
static const char kIssueAdviceResponseNoDetail[] =
"{"
" \"issueAdvice\": \"consent\","
" \"consent\": {"
" \"oauthClient\": {"
" \"name\": \"Test app\","
" \"iconUri\": \"\","
" \"developerEmail\": \"munjal@chromium.org\""
" },"
" \"scopes\": ["
" {"
" \"description\": \"Manage your calendars\","
" \"detail\": \"\nView and manage your calendars\n\""
" },"
" {"
" \"description\": \"Manage your documents\""
" }"
" ]"
" }"
"}";
static const char kValidRemoteConsentResponse[] =
"{"
" \"issueAdvice\": \"remoteConsent\","
" \"resolutionData\": {"
" \"resolutionApproach\": \"resolveInBrowser\","
" \"resolutionUrl\": \"https://test.com/consent?param=value\","
" \"browserCookies\": ["
" {"
" \"name\": \"test_name\","
" \"value\": \"test_value\","
" \"domain\": \"test.com\","
" \"path\": \"/\","
" \"maxAgeSeconds\": \"60\","
" \"isSecure\": false,"
" \"isHttpOnly\": true,"
" \"sameSite\": \"none\""
" },"
" {"
" \"name\": \"test_name2\","
" \"value\": \"test_value2\","
" \"domain\": \"test.com\""
" }"
" ]"
" }"
"}";
static const char kInvalidRemoteConsentResponse[] =
"{"
" \"issueAdvice\": \"remoteConsent\","
" \"resolutionData\": {"
" \"resolutionApproach\": \"resolveInBrowser\""
" }"
"}";
std::vector<std::string> CreateTestScopes() {
std::vector<std::string> scopes;
scopes.push_back("http://scope1");
scopes.push_back("http://scope2");
return scopes;
}
static IssueAdviceInfo CreateIssueAdvice() {
IssueAdviceInfo ia;
IssueAdviceInfoEntry e1;
e1.description = base::ASCIIToUTF16("Manage your calendars");
e1.details.push_back(base::ASCIIToUTF16("View and manage your calendars"));
ia.push_back(e1);
IssueAdviceInfoEntry e2;
e2.description = base::ASCIIToUTF16("Manage your documents");
e2.details.push_back(base::ASCIIToUTF16("View your documents"));
e2.details.push_back(base::ASCIIToUTF16("Upload new documents"));
ia.push_back(e2);
return ia;
}
static RemoteConsentResolutionData CreateRemoteConsentResolutionData() {
RemoteConsentResolutionData resolution_data;
resolution_data.url = GURL("https://test.com/consent?param=value");
resolution_data.cookies.push_back(net::CanonicalCookie::CreateSanitizedCookie(
resolution_data.url, "test_name", "test_value", "test.com", "/",
base::Time(), base::Time(), base::Time(), false, true,
net::CookieSameSite::LAX_MODE, net::COOKIE_PRIORITY_DEFAULT));
resolution_data.cookies.push_back(net::CanonicalCookie::CreateSanitizedCookie(
resolution_data.url, "test_name2", "test_value2", "test.com", "/",
base::Time(), base::Time(), base::Time(), false, false,
net::CookieSameSite::UNSPECIFIED, net::COOKIE_PRIORITY_DEFAULT));
return resolution_data;
}
class MockDelegate : public OAuth2MintTokenFlow::Delegate {
public:
MockDelegate() {}
~MockDelegate() override {}
MOCK_METHOD2(OnMintTokenSuccess, void(const std::string& access_token,
int time_to_live));
MOCK_METHOD1(OnIssueAdviceSuccess,
void (const IssueAdviceInfo& issue_advice));
MOCK_METHOD1(OnRemoteConsentSuccess,
void(const RemoteConsentResolutionData& resolution_data));
MOCK_METHOD1(OnMintTokenFailure,
void(const GoogleServiceAuthError& error));
};
class MockMintTokenFlow : public OAuth2MintTokenFlow {
public:
explicit MockMintTokenFlow(MockDelegate* delegate,
const OAuth2MintTokenFlow::Parameters& parameters)
: OAuth2MintTokenFlow(delegate, parameters) {}
~MockMintTokenFlow() override {}
MOCK_METHOD0(CreateAccessTokenFetcher,
std::unique_ptr<OAuth2AccessTokenFetcher>());
};
} // namespace
class OAuth2MintTokenFlowTest : public testing::Test {
public:
OAuth2MintTokenFlowTest() {}
~OAuth2MintTokenFlowTest() override {}
protected:
void CreateFlow(OAuth2MintTokenFlow::Mode mode) {
return CreateFlow(&delegate_, mode, "");
}
void CreateFlowWithDeviceId(const std::string& device_id) {
return CreateFlow(&delegate_, OAuth2MintTokenFlow::MODE_ISSUE_ADVICE,
device_id);
}
void CreateFlow(MockDelegate* delegate,
OAuth2MintTokenFlow::Mode mode,
const std::string& device_id) {
std::string ext_id = "ext1";
std::string client_id = "client1";
std::vector<std::string> scopes(CreateTestScopes());
flow_ = std::make_unique<MockMintTokenFlow>(
delegate, OAuth2MintTokenFlow::Parameters(ext_id, client_id, scopes,
device_id, mode));
}
// Helper to parse the given string to base::Value.
static std::unique_ptr<base::Value> ParseJson(const std::string& str) {
base::Optional<base::Value> value = base::JSONReader::Read(str);
EXPECT_TRUE(value.has_value());
EXPECT_TRUE(value->is_dict());
return std::make_unique<base::Value>(std::move(*value));
}
std::unique_ptr<MockMintTokenFlow> flow_;
StrictMock<MockDelegate> delegate_;
};
TEST_F(OAuth2MintTokenFlowTest, CreateApiCallBody) {
{ // Issue advice mode.
CreateFlow(OAuth2MintTokenFlow::MODE_ISSUE_ADVICE);
std::string body = flow_->CreateApiCallBody();
std::string expected_body(
"force=false"
"&response_type=none"
"&scope=http://scope1+http://scope2"
"&client_id=client1"
"&origin=ext1");
EXPECT_EQ(expected_body, body);
}
{ // Record grant mode.
CreateFlow(OAuth2MintTokenFlow::MODE_RECORD_GRANT);
std::string body = flow_->CreateApiCallBody();
std::string expected_body(
"force=true"
"&response_type=none"
"&scope=http://scope1+http://scope2"
"&client_id=client1"
"&origin=ext1");
EXPECT_EQ(expected_body, body);
}
{ // Mint token no force mode.
CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE);
std::string body = flow_->CreateApiCallBody();
std::string expected_body(
"force=false"
"&response_type=token"
"&scope=http://scope1+http://scope2"
"&client_id=client1"
"&origin=ext1");
EXPECT_EQ(expected_body, body);
}
{ // Mint token force mode.
CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_FORCE);
std::string body = flow_->CreateApiCallBody();
std::string expected_body(
"force=true"
"&response_type=token"
"&scope=http://scope1+http://scope2"
"&client_id=client1"
"&origin=ext1");
EXPECT_EQ(expected_body, body);
}
{ // Mint token with device_id.
CreateFlowWithDeviceId("device_id1");
std::string body = flow_->CreateApiCallBody();
std::string expected_body(
"force=false"
"&response_type=none"
"&scope=http://scope1+http://scope2"
"&client_id=client1"
"&origin=ext1"
"&device_id=device_id1"
"&device_type=chrome"
"&lib_ver=extension");
EXPECT_EQ(expected_body, body);
}
}
TEST_F(OAuth2MintTokenFlowTest, ParseMintTokenResponse) {
{ // Access token missing.
std::unique_ptr<base::Value> json = ParseJson(kTokenResponseNoAccessToken);
std::string at;
int ttl;
EXPECT_FALSE(OAuth2MintTokenFlow::ParseMintTokenResponse(json.get(), &at,
&ttl));
EXPECT_TRUE(at.empty());
}
{ // All good.
std::unique_ptr<base::Value> json = ParseJson(kValidTokenResponse);
std::string at;
int ttl;
EXPECT_TRUE(OAuth2MintTokenFlow::ParseMintTokenResponse(json.get(), &at,
&ttl));
EXPECT_EQ("at1", at);
EXPECT_EQ(3600, ttl);
}
}
TEST_F(OAuth2MintTokenFlowTest, ParseIssueAdviceResponse) {
{ // Description missing.
std::unique_ptr<base::Value> json =
ParseJson(kIssueAdviceResponseNoDescription);
IssueAdviceInfo ia;
EXPECT_FALSE(OAuth2MintTokenFlow::ParseIssueAdviceResponse(
json.get(), &ia));
EXPECT_TRUE(ia.empty());
}
{ // Detail missing.
std::unique_ptr<base::Value> json = ParseJson(kIssueAdviceResponseNoDetail);
IssueAdviceInfo ia;
EXPECT_FALSE(OAuth2MintTokenFlow::ParseIssueAdviceResponse(
json.get(), &ia));
EXPECT_TRUE(ia.empty());
}
{ // All good.
std::unique_ptr<base::Value> json = ParseJson(kValidIssueAdviceResponse);
IssueAdviceInfo ia;
EXPECT_TRUE(OAuth2MintTokenFlow::ParseIssueAdviceResponse(
json.get(), &ia));
IssueAdviceInfo ia_expected(CreateIssueAdvice());
EXPECT_EQ(ia_expected, ia);
}
}
TEST_F(OAuth2MintTokenFlowTest, ParseRemoteConsentResponse) {
std::unique_ptr<base::Value> json = ParseJson(kValidRemoteConsentResponse);
RemoteConsentResolutionData resolution_data;
ASSERT_TRUE(OAuth2MintTokenFlow::ParseRemoteConsentResponse(
json.get(), &resolution_data));
RemoteConsentResolutionData expected_resolution_data =
CreateRemoteConsentResolutionData();
EXPECT_EQ(resolution_data, expected_resolution_data);
}
TEST_F(OAuth2MintTokenFlowTest, ParseRemoteConsentResponse_EmptyCookies) {
std::unique_ptr<base::Value> json = ParseJson(kValidRemoteConsentResponse);
json->FindListPath("resolutionData.browserCookies")->GetList().clear();
RemoteConsentResolutionData resolution_data;
EXPECT_TRUE(OAuth2MintTokenFlow::ParseRemoteConsentResponse(
json.get(), &resolution_data));
RemoteConsentResolutionData expected_resolution_data =
CreateRemoteConsentResolutionData();
expected_resolution_data.cookies.clear();
EXPECT_EQ(resolution_data, expected_resolution_data);
}
TEST_F(OAuth2MintTokenFlowTest, ParseRemoteConsentResponse_NoCookies) {
std::unique_ptr<base::Value> json = ParseJson(kValidRemoteConsentResponse);
EXPECT_TRUE(json->RemovePath("resolutionData.browserCookies"));
RemoteConsentResolutionData resolution_data;
EXPECT_TRUE(OAuth2MintTokenFlow::ParseRemoteConsentResponse(
json.get(), &resolution_data));
RemoteConsentResolutionData expected_resolution_data =
CreateRemoteConsentResolutionData();
expected_resolution_data.cookies.clear();
EXPECT_EQ(resolution_data, expected_resolution_data);
}
TEST_F(OAuth2MintTokenFlowTest, ParseRemoteConsentResponse_NoResolutionData) {
std::unique_ptr<base::Value> json = ParseJson(kValidRemoteConsentResponse);
EXPECT_TRUE(json->RemoveKey("resolutionData"));
RemoteConsentResolutionData resolution_data;
EXPECT_FALSE(OAuth2MintTokenFlow::ParseRemoteConsentResponse(
json.get(), &resolution_data));
EXPECT_TRUE(resolution_data.url.is_empty());
EXPECT_TRUE(resolution_data.cookies.empty());
}
TEST_F(OAuth2MintTokenFlowTest, ParseRemoteConsentResponse_NoUrl) {
std::unique_ptr<base::Value> json = ParseJson(kValidRemoteConsentResponse);
EXPECT_TRUE(json->RemovePath("resolutionData.resolutionUrl"));
RemoteConsentResolutionData resolution_data;
EXPECT_FALSE(OAuth2MintTokenFlow::ParseRemoteConsentResponse(
json.get(), &resolution_data));
EXPECT_TRUE(resolution_data.url.is_empty());
EXPECT_TRUE(resolution_data.cookies.empty());
}
TEST_F(OAuth2MintTokenFlowTest, ParseRemoteConsentResponse_BadUrl) {
std::unique_ptr<base::Value> json = ParseJson(kValidRemoteConsentResponse);
EXPECT_TRUE(json->SetStringPath("resolutionData.resolutionUrl", "not-a-url"));
RemoteConsentResolutionData resolution_data;
EXPECT_FALSE(OAuth2MintTokenFlow::ParseRemoteConsentResponse(
json.get(), &resolution_data));
EXPECT_TRUE(resolution_data.url.is_empty());
EXPECT_TRUE(resolution_data.cookies.empty());
}
TEST_F(OAuth2MintTokenFlowTest, ParseRemoteConsentResponse_NoApproach) {
std::unique_ptr<base::Value> json = ParseJson(kValidRemoteConsentResponse);
EXPECT_TRUE(json->RemovePath("resolutionData.resolutionApproach"));
RemoteConsentResolutionData resolution_data;
EXPECT_FALSE(OAuth2MintTokenFlow::ParseRemoteConsentResponse(
json.get(), &resolution_data));
EXPECT_TRUE(resolution_data.url.is_empty());
EXPECT_TRUE(resolution_data.cookies.empty());
}
TEST_F(OAuth2MintTokenFlowTest, ParseRemoteConsentResponse_BadApproach) {
std::unique_ptr<base::Value> json = ParseJson(kValidRemoteConsentResponse);
EXPECT_TRUE(
json->SetStringPath("resolutionData.resolutionApproach", "badApproach"));
RemoteConsentResolutionData resolution_data;
EXPECT_FALSE(OAuth2MintTokenFlow::ParseRemoteConsentResponse(
json.get(), &resolution_data));
EXPECT_TRUE(resolution_data.url.is_empty());
EXPECT_TRUE(resolution_data.cookies.empty());
}
TEST_F(OAuth2MintTokenFlowTest,
ParseRemoteConsentResponse_BadCookie_MissingRequiredField) {
static const char* kRequiredFields[] = {"name", "value", "domain"};
for (const auto* required_field : kRequiredFields) {
std::unique_ptr<base::Value> json = ParseJson(kValidRemoteConsentResponse);
base::Value::ListStorage& cookies =
json->FindListPath("resolutionData.browserCookies")->GetList();
EXPECT_TRUE(cookies[0].RemoveKey(required_field));
RemoteConsentResolutionData resolution_data;
EXPECT_FALSE(OAuth2MintTokenFlow::ParseRemoteConsentResponse(
json.get(), &resolution_data));
EXPECT_TRUE(resolution_data.url.is_empty());
EXPECT_TRUE(resolution_data.cookies.empty());
}
}
TEST_F(OAuth2MintTokenFlowTest,
ParseRemoteConsentResponse_MissingCookieOptionalField) {
static const char* kOptionalFields[] = {"path", "maxAgeSeconds", "isSecure",
"isHttpOnly", "sameSite"};
for (const auto* optional_field : kOptionalFields) {
std::unique_ptr<base::Value> json = ParseJson(kValidRemoteConsentResponse);
base::Value::ListStorage& cookies =
json->FindListPath("resolutionData.browserCookies")->GetList();
EXPECT_TRUE(cookies[0].RemoveKey(optional_field));
RemoteConsentResolutionData resolution_data;
EXPECT_TRUE(OAuth2MintTokenFlow::ParseRemoteConsentResponse(
json.get(), &resolution_data));
RemoteConsentResolutionData expected_resolution_data =
CreateRemoteConsentResolutionData();
EXPECT_EQ(resolution_data, expected_resolution_data);
}
}
TEST_F(OAuth2MintTokenFlowTest,
ParseRemoteConsentResponse_BadCookie_BadMaxAge) {
std::unique_ptr<base::Value> json = ParseJson(kValidRemoteConsentResponse);
base::Value::ListStorage& cookies =
json->FindListPath("resolutionData.browserCookies")->GetList();
cookies[0].SetStringKey("maxAgeSeconds", "not-a-number");
RemoteConsentResolutionData resolution_data;
EXPECT_FALSE(OAuth2MintTokenFlow::ParseRemoteConsentResponse(
json.get(), &resolution_data));
EXPECT_TRUE(resolution_data.url.is_empty());
EXPECT_TRUE(resolution_data.cookies.empty());
}
TEST_F(OAuth2MintTokenFlowTest, ParseRemoteConsentResponse_BadCookieList) {
std::unique_ptr<base::Value> json = ParseJson(kValidRemoteConsentResponse);
base::Value::ListStorage& cookies =
json->FindListPath("resolutionData.browserCookies")->GetList();
cookies.push_back(base::Value(42));
RemoteConsentResolutionData resolution_data;
EXPECT_FALSE(OAuth2MintTokenFlow::ParseRemoteConsentResponse(
json.get(), &resolution_data));
EXPECT_TRUE(resolution_data.url.is_empty());
EXPECT_TRUE(resolution_data.cookies.empty());
}
TEST_F(OAuth2MintTokenFlowTest, ProcessApiCallSuccess) {
network::mojom::URLResponseHeadPtr head_200 =
network::CreateURLResponseHead(net::HTTP_OK);
{ // No body.
CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE);
EXPECT_CALL(delegate_, OnMintTokenFailure(_));
flow_->ProcessApiCallSuccess(head_200.get(), nullptr);
}
{ // Bad json.
CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE);
EXPECT_CALL(delegate_, OnMintTokenFailure(_));
flow_->ProcessApiCallSuccess(head_200.get(),
std::make_unique<std::string>("foo"));
}
{ // Valid json: no access token.
CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE);
EXPECT_CALL(delegate_, OnMintTokenFailure(_));
flow_->ProcessApiCallSuccess(
head_200.get(),
std::make_unique<std::string>(kTokenResponseNoAccessToken));
}
{ // Valid json: good token response.
CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE);
EXPECT_CALL(delegate_, OnMintTokenSuccess("at1", 3600));
flow_->ProcessApiCallSuccess(
head_200.get(), std::make_unique<std::string>(kValidTokenResponse));
}
{ // Valid json: no description.
CreateFlow(OAuth2MintTokenFlow::MODE_ISSUE_ADVICE);
EXPECT_CALL(delegate_, OnMintTokenFailure(_));
flow_->ProcessApiCallSuccess(
head_200.get(),
std::make_unique<std::string>(kIssueAdviceResponseNoDescription));
}
{ // Valid json: no detail.
CreateFlow(OAuth2MintTokenFlow::MODE_ISSUE_ADVICE);
EXPECT_CALL(delegate_, OnMintTokenFailure(_));
flow_->ProcessApiCallSuccess(
head_200.get(),
std::make_unique<std::string>(kIssueAdviceResponseNoDetail));
}
{ // Valid json: good issue advice response.
CreateFlow(OAuth2MintTokenFlow::MODE_ISSUE_ADVICE);
IssueAdviceInfo ia(CreateIssueAdvice());
EXPECT_CALL(delegate_, OnIssueAdviceSuccess(ia));
flow_->ProcessApiCallSuccess(
head_200.get(),
std::make_unique<std::string>(kValidIssueAdviceResponse));
}
{ // Valid json: remote consent response.
CreateFlow(OAuth2MintTokenFlow::MODE_ISSUE_ADVICE);
RemoteConsentResolutionData resolution_data =
CreateRemoteConsentResolutionData();
EXPECT_CALL(delegate_, OnRemoteConsentSuccess(Eq(ByRef(resolution_data))));
flow_->ProcessApiCallSuccess(
head_200.get(),
std::make_unique<std::string>(kValidRemoteConsentResponse));
}
{ // Valid json: bad remote consent response, fallback to issue advice.
CreateFlow(OAuth2MintTokenFlow::MODE_ISSUE_ADVICE);
IssueAdviceInfo empty_info;
EXPECT_CALL(delegate_, OnIssueAdviceSuccess(empty_info));
flow_->ProcessApiCallSuccess(
head_200.get(),
std::make_unique<std::string>(kInvalidRemoteConsentResponse));
}
}
TEST_F(OAuth2MintTokenFlowTest, ProcessApiCallFailure) {
network::mojom::URLResponseHead head;
{ // Null delegate should work fine.
CreateFlow(nullptr, OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE, "");
flow_->ProcessApiCallFailure(net::ERR_FAILED, &head, nullptr);
}
{ // Non-null delegate.
CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE);
EXPECT_CALL(delegate_, OnMintTokenFailure(_));
flow_->ProcessApiCallFailure(net::ERR_FAILED, &head, nullptr);
}
{ // Null head might happen.
CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE);
EXPECT_CALL(delegate_, OnMintTokenFailure(_));
flow_->ProcessApiCallFailure(net::ERR_FAILED, nullptr, nullptr);
}
}