blob: 3c0e0642392b4aaeb9b159cfbc8db45ed56a4dd6 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/invalidation/impl/per_user_topic_subscription_manager.h"
#include <iostream>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "base/barrier_callback.h"
#include "base/functional/bind.h"
#include "base/json/json_string_value_serializer.h"
#include "base/json/json_writer.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/values.h"
#include "components/invalidation/impl/profile_identity_provider.h"
#include "components/invalidation/public/invalidation_util.h"
#include "components/prefs/testing_pref_service.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "net/http/http_status_code.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
using testing::_;
using testing::Contains;
using testing::Eq;
using testing::IsEmpty;
using testing::NiceMock;
using testing::Not;
using testing::SizeIs;
using testing::UnorderedElementsAreArray;
namespace invalidation {
using RequestType = PerUserTopicSubscriptionManager::RequestType;
namespace {
size_t kInvalidationTopicsCount = 5;
const char kInvalidationRegistrationScope[] =
"https://firebaseperusertopics-pa.googleapis.com";
const char kProjectId[] = "8181035976";
const char kTypeSubscribedForInvalidation[] =
"invalidation.per_sender_registered_for_invalidation";
const char kActiveRegistrationTokens[] =
"invalidation.per_sender_active_registration_tokens";
const char kFakeInstanceIdToken[] = "fake_instance_id_token";
class MockIdentityDiagnosticsObserver
: public signin::IdentityManager::DiagnosticsObserver {
public:
MOCK_METHOD3(OnAccessTokenRequested,
void(const CoreAccountId&,
const std::string&,
const signin::ScopeSet&));
MOCK_METHOD2(OnAccessTokenRemovedFromCache,
void(const CoreAccountId&, const signin::ScopeSet&));
};
std::string IndexToName(size_t index) {
char name[2] = "a";
name[0] += static_cast<char>(index);
return name;
}
TopicMap GetSequenceOfTopicsStartingAt(size_t start, size_t count) {
TopicMap topics;
for (size_t i = start; i < start + count; ++i) {
topics.emplace(IndexToName(i), TopicMetadata{false});
}
return topics;
}
TopicMap GetSequenceOfTopics(size_t count) {
return GetSequenceOfTopicsStartingAt(0, count);
}
TopicSet TopicSetFromTopics(const TopicMap& topics) {
TopicSet topic_set;
for (const auto& topic : topics) {
topic_set.insert(topic.first);
}
return topic_set;
}
network::mojom::URLResponseHeadPtr CreateHeadersForTest(int responce_code) {
auto head = network::mojom::URLResponseHead::New();
head->headers = new net::HttpResponseHeaders(base::StringPrintf(
"HTTP/1.1 %d OK\nContent-type: text/html\n\n", responce_code));
head->mime_type = "text/html";
return head;
}
GURL FullSubscriptionUrl(const std::string& token) {
return GURL(base::StringPrintf(
"%s/v1/perusertopics/%s/rel/topics/?subscriber_token=%s",
kInvalidationRegistrationScope, kProjectId, token.c_str()));
}
GURL FullUnSubscriptionUrlForTopic(const std::string& topic) {
return GURL(base::StringPrintf(
"%s/v1/perusertopics/%s/rel/topics/%s?subscriber_token=%s",
kInvalidationRegistrationScope, kProjectId, topic.c_str(),
kFakeInstanceIdToken));
}
network::URLLoaderCompletionStatus CreateStatusForTest(
int status,
const std::string& response_body) {
network::URLLoaderCompletionStatus response_status(status);
response_status.decoded_body_length = response_body.size();
return response_status;
}
// SubscriptionRequestFinishedEvent instances are used to keep track of
// PerUserTopicSubscriptionManager::Observer::OnSubscriptionRequestFinished
// invocations.
struct SubscriptionRequestFinishedEvent {
Topic topic;
PerUserTopicSubscriptionManager::RequestType request_type;
Status status;
friend bool operator==(const SubscriptionRequestFinishedEvent& lhs,
const SubscriptionRequestFinishedEvent& rhs) = default;
friend auto operator<=>(const SubscriptionRequestFinishedEvent& lhs,
const SubscriptionRequestFinishedEvent& rhs) =
default;
};
std::ostream& operator<<(std::ostream& os,
const SubscriptionRequestFinishedEvent& event) {
os << "SubscriptionRequestFinished{topic='" << event.topic
<< "', request_type=" << static_cast<int>(event.request_type)
<< ", status={code=" << static_cast<int>(event.status.code)
<< ", message='" << event.status.message << "'}}";
return os;
}
// Returns a set of SubscriptionRequestFinishedEvent events of type
// `request_type` with the resulting Status `status`, one for each topic in
// `topics`.
std::multiset<SubscriptionRequestFinishedEvent>
SubscriptionRequestFinishedEvents(const TopicMap& topics,
RequestType request_type,
const Status& status) {
std::multiset<SubscriptionRequestFinishedEvent> events;
for (const auto& topic : topics) {
events.insert(
SubscriptionRequestFinishedEvent{topic.first, request_type, status});
}
return events;
}
} // namespace
class RegistrationManagerStateObserver
: public PerUserTopicSubscriptionManager::Observer {
public:
void OnSubscriptionChannelStateChanged(
SubscriptionChannelState state) override {
state_ = state;
if (run_loop_) {
run_loop_->Quit();
}
}
void OnSubscriptionRequestFinished(
Topic topic,
PerUserTopicSubscriptionManager::RequestType request_type,
Status status) override {
subscription_request_finished_events_.insert(
SubscriptionRequestFinishedEvent{topic, request_type, status});
if (run_loop_) {
run_loop_->Quit();
}
}
void WaitForState(SubscriptionChannelState expected_state) {
while (state_ != expected_state) {
run_loop_ = std::make_unique<base::RunLoop>();
run_loop_->Run();
run_loop_.reset();
}
}
// Waits for the recorded OnSubscriptionRequestFinished invocations to match
// `expected`, then clears the recorded OnSubscriptionRequestFinished
// invocations.
// TODO - b/321195077: Prevent this from blocking in case `excepted.size()`
// is too big.
void WaitForSubscriptionRequestsFinished(
const std::multiset<SubscriptionRequestFinishedEvent>& expected) {
while (subscription_request_finished_events_.size() < expected.size()) {
run_loop_ = std::make_unique<base::RunLoop>();
run_loop_->Run();
run_loop_.reset();
}
EXPECT_THAT(subscription_request_finished_events_,
UnorderedElementsAreArray(expected));
subscription_request_finished_events_.clear();
}
void ExpectNoSubscriptionRequestsFinished() {
base::RunLoop().RunUntilIdle();
EXPECT_THAT(subscription_request_finished_events_, IsEmpty());
}
private:
SubscriptionChannelState state_ = SubscriptionChannelState::NOT_STARTED;
std::unique_ptr<base::RunLoop> run_loop_;
std::multiset<SubscriptionRequestFinishedEvent>
subscription_request_finished_events_;
};
class PerUserTopicSubscriptionManagerTest : public testing::Test {
protected:
PerUserTopicSubscriptionManagerTest() = default;
~PerUserTopicSubscriptionManagerTest() override = default;
void SetUp() override {
PerUserTopicSubscriptionManager::RegisterProfilePrefs(
pref_service_.registry());
AccountInfo account = identity_test_env_.MakePrimaryAccountAvailable(
"example@gmail.com", signin::ConsentLevel::kSync);
identity_test_env_.SetAutomaticIssueOfAccessTokens(true);
identity_provider_ = std::make_unique<ProfileIdentityProvider>(
identity_test_env_.identity_manager());
}
std::unique_ptr<PerUserTopicSubscriptionManager> BuildRegistrationManager() {
auto reg_manager = std::make_unique<PerUserTopicSubscriptionManager>(
identity_provider_.get(), &pref_service_, url_loader_factory(),
kProjectId);
reg_manager->Init();
reg_manager->AddObserver(&state_observer_);
return reg_manager;
}
network::TestURLLoaderFactory* url_loader_factory() {
return &url_loader_factory_;
}
TestingPrefServiceSimple* pref_service() { return &pref_service_; }
const base::Value::Dict& GetSubscribedTopics() const {
const base::Value::Dict* subscribed_topics =
pref_service_.GetDict(kTypeSubscribedForInvalidation)
.FindDict(kProjectId);
DCHECK(subscribed_topics);
return *subscribed_topics;
}
void WaitForState(SubscriptionChannelState expected_state) {
state_observer_.WaitForState(expected_state);
}
void WaitForSubscriptionRequestsFinished(
const std::multiset<SubscriptionRequestFinishedEvent>& expected) {
state_observer_.WaitForSubscriptionRequestsFinished(expected);
}
void ExpectNoSubscriptionRequestsFinished() {
state_observer_.ExpectNoSubscriptionRequestsFinished();
}
void WaitForTopics(const PerUserTopicSubscriptionManager& manager,
const TopicMap& expected_topics) {
while (manager.GetSubscribedTopicsForTest() !=
TopicSetFromTopics(expected_topics)) {
pref_service()->user_prefs_store()->WaitUntilValueChanges(
kTypeSubscribedForInvalidation);
}
}
void AddCorrectSubscriptionResponse(
const std::string& private_topic = std::string(),
const std::string& token = kFakeInstanceIdToken,
int http_responce_code = net::HTTP_OK) {
base::Value::Dict value;
value.Set("privateTopicName",
private_topic.empty() ? "test-pr" : private_topic.c_str());
std::string serialized_response;
JSONStringValueSerializer serializer(&serialized_response);
serializer.Serialize(value);
url_loader_factory()->AddResponse(
FullSubscriptionUrl(token), CreateHeadersForTest(http_responce_code),
serialized_response, CreateStatusForTest(net::OK, serialized_response));
}
void AddCorrectUnSubscriptionResponseForTopic(const std::string& topic) {
url_loader_factory()->AddResponse(
FullUnSubscriptionUrlForTopic(topic),
CreateHeadersForTest(net::HTTP_OK), std::string() /* response_body */,
CreateStatusForTest(net::OK, std::string() /* response_body */));
}
void FastForwardTimeBy(base::TimeDelta delta) {
task_environment_.FastForwardBy(delta);
}
signin::IdentityTestEnvironment* identity_test_env() {
return &identity_test_env_;
}
private:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
network::TestURLLoaderFactory url_loader_factory_;
TestingPrefServiceSimple pref_service_;
signin::IdentityTestEnvironment identity_test_env_;
std::unique_ptr<ProfileIdentityProvider> identity_provider_;
RegistrationManagerStateObserver state_observer_;
};
TEST_F(PerUserTopicSubscriptionManagerTest,
EmptyPrivateTopicShouldNotUpdateSubscribedTopics) {
auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);
auto per_user_topic_subscription_manager = BuildRegistrationManager();
EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
// Empty response body should result in no successful registrations.
std::string response_body;
url_loader_factory()->AddResponse(
FullSubscriptionUrl(kFakeInstanceIdToken),
CreateHeadersForTest(net::HTTP_OK), response_body,
CreateStatusForTest(net::OK, response_body));
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
topics, RequestType::kSubscribe,
Status(StatusCode::FAILED, "Body missing")));
// The response didn't contain non-empty topic name. So nothing was
// registered.
EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
}
TEST_F(PerUserTopicSubscriptionManagerTest, ShouldUpdateSubscribedTopics) {
auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);
auto per_user_topic_subscription_manager = BuildRegistrationManager();
ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
AddCorrectSubscriptionResponse();
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
WaitForTopics(*per_user_topic_subscription_manager, topics);
EXPECT_TRUE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
for (const auto& topic : topics) {
const base::Value::Dict& subscribed_topics = GetSubscribedTopics();
const std::string* private_topic_value =
subscribed_topics.FindString(topic.first);
ASSERT_NE(private_topic_value, nullptr);
}
}
TEST_F(PerUserTopicSubscriptionManagerTest,
ShouldNotRequestAccessTokenWhileHaveNoRequests) {
NiceMock<MockIdentityDiagnosticsObserver> identity_observer;
identity_test_env()->identity_manager()->AddDiagnosticsObserver(
&identity_observer);
EXPECT_CALL(identity_observer, OnAccessTokenRequested(_, _, _)).Times(0);
BuildRegistrationManager()->UpdateSubscribedTopics({}, kFakeInstanceIdToken);
identity_test_env()->identity_manager()->RemoveDiagnosticsObserver(
&identity_observer);
}
TEST_F(PerUserTopicSubscriptionManagerTest, ShouldRepeatRequestsOnFailure) {
// For this test, we want to manually control when access tokens are returned.
identity_test_env()->SetAutomaticIssueOfAccessTokens(false);
NiceMock<MockIdentityDiagnosticsObserver> identity_observer;
identity_test_env()->identity_manager()->AddDiagnosticsObserver(
&identity_observer);
auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);
auto per_user_topic_subscription_manager = BuildRegistrationManager();
ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
// The first subscription attempt will fail.
AddCorrectSubscriptionResponse(
/*private_topic=*/std::string(), kFakeInstanceIdToken,
net::HTTP_INTERNAL_SERVER_ERROR);
// Since this is a generic failure, not an auth error, the existing access
// token should *not* get invalidated.
EXPECT_CALL(identity_observer, OnAccessTokenRemovedFromCache(_, _)).Times(0);
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
// This should have resulted in a request for an access token. Return one.
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
"access_token", base::Time::Max());
// Wait for all of the subscription requests to fail. No retries
// have been attempted yet.
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
topics, RequestType::kSubscribe,
Status(StatusCode::FAILED, "HTTP Error: 500")));
// Since the subscriptions failed, the requests should still be pending.
EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
EXPECT_FALSE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
// The second attempt will succeed.
AddCorrectSubscriptionResponse();
// Repeating subscriptions shouldn't bypass backoff.
// This should have resulted in a request for an access token. Return one.
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
"access_token", base::Time::Max());
// Initial backoff is 2 seconds with 20% jitter, so the minimum possible delay
// is 1600ms. Advance time to just before that; nothing should have changed
// yet.
FastForwardTimeBy(base::Milliseconds(1500));
EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
EXPECT_FALSE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
// The maximum backoff is 2 seconds; advance to just past that.
// Access token should be refreshed in order to avoid requests with expired
// access token.
EXPECT_CALL(identity_observer, OnAccessTokenRequested(_, _, _));
FastForwardTimeBy(base::Milliseconds(600));
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
"access_token", base::Time::Max());
// Retries should be triggered now.
// Wait for all of the subscription requests to finish.
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
topics, RequestType::kSubscribe, Status::Success()));
// Now all subscriptions should have finished.
EXPECT_FALSE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
EXPECT_TRUE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
identity_test_env()->identity_manager()->RemoveDiagnosticsObserver(
&identity_observer);
}
TEST_F(PerUserTopicSubscriptionManagerTest, ShouldNotRepeatOngoingRequests) {
auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);
auto per_user_topic_subscription_manager = BuildRegistrationManager();
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
// The requests are not finished, so there should be one pending request per
// invalidation topic.
EXPECT_EQ(per_user_topic_subscription_manager
->GetPendingSubscriptionsCountForTest(),
kInvalidationTopicsCount);
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
// No changes in wanted subscriptions or access token, so there should still
// be only one pending request per invalidation topic.
EXPECT_EQ(per_user_topic_subscription_manager
->GetPendingSubscriptionsCountForTest(),
kInvalidationTopicsCount);
}
TEST_F(PerUserTopicSubscriptionManagerTest,
ShouldRepeatAccessTokenRequestOnFailure) {
// For this test, we want to manually control when access tokens are returned.
identity_test_env()->SetAutomaticIssueOfAccessTokens(false);
NiceMock<MockIdentityDiagnosticsObserver> identity_observer;
identity_test_env()->identity_manager()->AddDiagnosticsObserver(
&identity_observer);
auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);
auto per_user_topic_subscription_manager = BuildRegistrationManager();
ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
// Emulate failure on first access token request.
EXPECT_CALL(identity_observer, OnAccessTokenRequested(_, _, _));
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
GoogleServiceAuthError(GoogleServiceAuthError::CONNECTION_FAILED));
testing::Mock::VerifyAndClearExpectations(&identity_observer);
// Initial backoff is 2 seconds with 20% jitter, so the minimum possible delay
// is 1600ms. Advance time to just before that; nothing should have changed
// yet.
EXPECT_CALL(identity_observer, OnAccessTokenRequested(_, _, _)).Times(0);
// UpdateSubscribedTopics() call shouldn't lead to backoff bypassing.
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
FastForwardTimeBy(base::Milliseconds(1500));
testing::Mock::VerifyAndClearExpectations(&identity_observer);
// The maximum backoff is 2 seconds; advance to just past that. Now access
// token should be requested.
EXPECT_CALL(identity_observer, OnAccessTokenRequested(_, _, _));
FastForwardTimeBy(base::Milliseconds(600));
testing::Mock::VerifyAndClearExpectations(&identity_observer);
// Add valid responses to access token and subscription requests and ensure
// that subscription requests were successful.
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
"valid_access_token", base::Time::Max());
AddCorrectSubscriptionResponse();
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
topics, RequestType::kSubscribe, Status::Success()));
EXPECT_FALSE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
EXPECT_TRUE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
identity_test_env()->identity_manager()->RemoveDiagnosticsObserver(
&identity_observer);
}
TEST_F(PerUserTopicSubscriptionManagerTest,
ShouldInvalidateAccessTokenOnUnauthorized) {
// For this test, we need to manually control when access tokens are returned.
identity_test_env()->SetAutomaticIssueOfAccessTokens(false);
NiceMock<MockIdentityDiagnosticsObserver> identity_observer;
identity_test_env()->identity_manager()->AddDiagnosticsObserver(
&identity_observer);
auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);
auto per_user_topic_subscription_manager = BuildRegistrationManager();
ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
// The first subscription attempt will fail with an "unauthorized" error.
AddCorrectSubscriptionResponse(
/*private_topic=*/std::string(), kFakeInstanceIdToken,
net::HTTP_UNAUTHORIZED);
// This error should result in invalidating the access token.
EXPECT_CALL(identity_observer, OnAccessTokenRemovedFromCache(_, _));
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
// This should have resulted in a request for an access token. Return one
// (which is considered invalid, e.g. already expired).
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
"invalid_access_token", base::Time::Now());
// Now the subscription requests should be scheduled.
ASSERT_FALSE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
// Wait for the subscription requests to fail.
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
topics, RequestType::kSubscribe,
Status(StatusCode::AUTH_FAILURE, "HTTP Error: 401")));
// Since the subscriptions failed, the requests should still be pending.
ASSERT_FALSE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
// A new access token should have been requested. Serving one will trigger
// another subscription attempt; let this one succeed.
AddCorrectSubscriptionResponse();
EXPECT_CALL(identity_observer, OnAccessTokenRemovedFromCache(_, _)).Times(0);
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
"valid_access_token", base::Time::Max());
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
topics, RequestType::kSubscribe, Status::Success()));
EXPECT_FALSE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
EXPECT_TRUE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
identity_test_env()->identity_manager()->RemoveDiagnosticsObserver(
&identity_observer);
}
TEST_F(PerUserTopicSubscriptionManagerTest,
ShouldInvalidateAccessTokenOnlyOnce) {
// For this test, we need to manually control when access tokens are returned.
identity_test_env()->SetAutomaticIssueOfAccessTokens(false);
NiceMock<MockIdentityDiagnosticsObserver> identity_observer;
identity_test_env()->identity_manager()->AddDiagnosticsObserver(
&identity_observer);
auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);
auto per_user_topic_subscription_manager = BuildRegistrationManager();
ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
// The first subscription attempt will fail with an "unauthorized" error.
AddCorrectSubscriptionResponse(
/*private_topic=*/std::string(), kFakeInstanceIdToken,
net::HTTP_UNAUTHORIZED);
// This error should result in invalidating the access token.
EXPECT_CALL(identity_observer, OnAccessTokenRemovedFromCache(_, _));
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
// This should have resulted in a request for an access token. Return one
// (which is considered invalid, e.g. already expired).
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
"invalid_access_token", base::Time::Now());
// Now the subscription requests should be scheduled.
ASSERT_FALSE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
// Wait for the subscription requests to fail.
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
topics, RequestType::kSubscribe,
Status(StatusCode::AUTH_FAILURE, "HTTP Error: 401")));
// Since the subscriptions failed, the requests should still be pending.
ASSERT_FALSE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
// At this point, the old access token should have been invalidated and a new
// one requested. The new one should *not* get invalidated.
EXPECT_CALL(identity_observer, OnAccessTokenRemovedFromCache(_, _)).Times(0);
// Serving a new access token will trigger another subscription attempt, but
// the subscription requests will still fail with the same error.
identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
"invalid_access_token_2", base::Time::Max());
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
topics, RequestType::kSubscribe,
Status(StatusCode::AUTH_FAILURE, "HTTP Error: 401")));
// On the second auth failure, we should have given up - no new access token
// request should have happened, and all the pending subscriptions should have
// been dropped, even though still no topics are subscribed.
EXPECT_FALSE(identity_test_env()->IsAccessTokenRequestPending());
EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
EXPECT_TRUE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
identity_test_env()->identity_manager()->RemoveDiagnosticsObserver(
&identity_observer);
}
TEST_F(PerUserTopicSubscriptionManagerTest,
ShouldNotRepeatRequestsOnForbidden) {
auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);
auto per_user_topic_subscription_manager = BuildRegistrationManager();
ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
AddCorrectSubscriptionResponse(
/*private_topic=*/std::string(), kFakeInstanceIdToken,
net::HTTP_FORBIDDEN);
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
topics, RequestType::kSubscribe,
Status(StatusCode::FAILED_NON_RETRIABLE, "HTTP Error: 403")));
EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
EXPECT_TRUE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
}
TEST_F(PerUserTopicSubscriptionManagerTest,
ShouldUnsubscribeTopicsAndDeleteFromPrefs) {
auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);
AddCorrectSubscriptionResponse();
auto per_user_topic_subscription_manager = BuildRegistrationManager();
EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
topics, RequestType::kSubscribe, Status::Success()));
EXPECT_EQ(TopicSetFromTopics(topics),
per_user_topic_subscription_manager->GetSubscribedTopicsForTest());
// Unsubscribe from some topics.
auto unsubscribed_topics = GetSequenceOfTopics(3);
auto still_subscribed_topics =
GetSequenceOfTopicsStartingAt(3, kInvalidationTopicsCount - 3);
for (const auto& topic : unsubscribed_topics) {
AddCorrectUnSubscriptionResponseForTopic(topic.first);
}
per_user_topic_subscription_manager->UpdateSubscribedTopics(
still_subscribed_topics, kFakeInstanceIdToken);
// Expect the unsubscribe requests to start and succeed.
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
unsubscribed_topics, RequestType::kUnsubscribe, Status::Success()));
// Topics were unsubscribed, check that they're not in the prefs.
for (const auto& topic : unsubscribed_topics) {
const base::Value::Dict& subscribed_topics = GetSubscribedTopics();
const base::Value* private_topic_value =
subscribed_topics.Find(topic.first);
ASSERT_EQ(private_topic_value, nullptr);
}
// Check that still subscribed topics are still in the prefs.
for (const auto& topic : still_subscribed_topics) {
const base::Value::Dict& subscribed_topics = GetSubscribedTopics();
const std::string* private_topic_value =
subscribed_topics.FindString(topic.first);
ASSERT_NE(private_topic_value, nullptr);
}
}
TEST_F(PerUserTopicSubscriptionManagerTest,
ShouldDropSavedTopicsOnTokenChange) {
auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);
auto per_user_topic_subscription_manager = BuildRegistrationManager();
EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
AddCorrectSubscriptionResponse("old-token-topic");
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
WaitForTopics(*per_user_topic_subscription_manager, topics);
for (const auto& topic : topics) {
const base::Value::Dict& subscribed_topics = GetSubscribedTopics();
const std::string* private_topic_value =
subscribed_topics.FindString(topic.first);
ASSERT_NE(private_topic_value, nullptr);
EXPECT_EQ("old-token-topic", *private_topic_value);
}
EXPECT_EQ(kFakeInstanceIdToken, *pref_service()
->GetDict(kActiveRegistrationTokens)
.FindString(kProjectId));
std::string token = "new-fake-token";
AddCorrectSubscriptionResponse("new-token-topic", token);
per_user_topic_subscription_manager->UpdateSubscribedTopics(topics, token);
WaitForTopics(*per_user_topic_subscription_manager, topics);
EXPECT_EQ(token, *pref_service()
->GetDict(kActiveRegistrationTokens)
.FindString(kProjectId));
for (const auto& topic : topics) {
const base::Value::Dict& subscribed_topics = GetSubscribedTopics();
const std::string* private_topic_value =
subscribed_topics.FindString(topic.first);
ASSERT_NE(private_topic_value, nullptr);
EXPECT_EQ("new-token-topic", *private_topic_value);
}
}
TEST_F(PerUserTopicSubscriptionManagerTest,
ShouldDeleteTopicsFromPrefsWhenUnsubscribeRequestFails) {
auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);
AddCorrectSubscriptionResponse();
auto per_user_topic_subscription_manager = BuildRegistrationManager();
EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
topics, RequestType::kSubscribe, Status::Success()));
WaitForTopics(*per_user_topic_subscription_manager, topics);
// Unsubscribe from some topics.
auto unsubscribed_topics = GetSequenceOfTopics(3);
auto still_subscribed_topics =
GetSequenceOfTopicsStartingAt(3, kInvalidationTopicsCount - 3);
// Without configuring the unsubscription response, the unsubscription
// requests will not finish.
per_user_topic_subscription_manager->UpdateSubscribedTopics(
still_subscribed_topics, kFakeInstanceIdToken);
ExpectNoSubscriptionRequestsFinished();
// Topics should be removed from prefs even though the unsubscribe requests
// have not finished.
for (const auto& topic : unsubscribed_topics) {
const base::Value::Dict& subscribed_topics = GetSubscribedTopics();
const base::Value* private_topic_value =
subscribed_topics.Find(topic.first);
ASSERT_EQ(private_topic_value, nullptr);
}
// Check that subscribed topics are still in the prefs.
for (const auto& topic : still_subscribed_topics) {
const base::Value::Dict& subscribed_topics = GetSubscribedTopics();
const std::string* private_topic_value =
subscribed_topics.FindString(topic.first);
ASSERT_NE(private_topic_value, nullptr);
}
}
TEST_F(PerUserTopicSubscriptionManagerTest,
ShouldChangeStatusToEnabledWhenHasNoPendingSubscription) {
BuildRegistrationManager()->UpdateSubscribedTopics(/*topics=*/{},
kFakeInstanceIdToken);
WaitForState(SubscriptionChannelState::ENABLED);
}
TEST_F(PerUserTopicSubscriptionManagerTest,
ShouldChangeStatusToDisabledWhenTopicsRegistrationFailed) {
auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);
AddCorrectSubscriptionResponse();
auto per_user_topic_subscription_manager = BuildRegistrationManager();
ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
WaitForState(SubscriptionChannelState::ENABLED);
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
topics, RequestType::kSubscribe, Status::Success()));
EXPECT_EQ(TopicSetFromTopics(topics),
per_user_topic_subscription_manager->GetSubscribedTopicsForTest());
// Unsubscribe from some topics.
auto temporarily_unsubscribed_topics = GetSequenceOfTopics(3);
auto still_subscribed_topics =
GetSequenceOfTopicsStartingAt(3, kInvalidationTopicsCount - 3);
for (const auto& topic : temporarily_unsubscribed_topics) {
AddCorrectUnSubscriptionResponseForTopic(topic.first);
}
per_user_topic_subscription_manager->UpdateSubscribedTopics(
still_subscribed_topics, kFakeInstanceIdToken);
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
temporarily_unsubscribed_topics, RequestType::kUnsubscribe,
Status::Success()));
// Clear previously configured correct response, so next subscription requests
// will fail. Then attempt to re-subscribe to the temporarily unsubscribed
// topics.
url_loader_factory()->ClearResponses();
url_loader_factory()->AddResponse(
FullSubscriptionUrl(kFakeInstanceIdToken).spec(),
std::string() /* content */, net::HTTP_NOT_FOUND);
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
temporarily_unsubscribed_topics, RequestType::kSubscribe,
Status(StatusCode::FAILED_NON_RETRIABLE, "HTTP Error: 404")));
WaitForState(SubscriptionChannelState::SUBSCRIPTION_FAILURE);
// Configure correct response and attempt again to re-subscribe to the
// temporarily unsubscribed topics.
AddCorrectSubscriptionResponse();
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, kFakeInstanceIdToken);
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
temporarily_unsubscribed_topics, RequestType::kSubscribe,
Status::Success()));
WaitForState(SubscriptionChannelState::ENABLED);
}
TEST_F(PerUserTopicSubscriptionManagerTest, ShouldRecordTokenStateHistogram) {
const char kTokenStateHistogram[] =
"FCMInvalidations.TokenStateOnRegistrationRequest2";
enum class TokenStateOnSubscriptionRequest {
kTokenWasEmpty = 0,
kTokenUnchanged = 1,
kTokenChanged = 2,
kTokenCleared = 3,
};
const TopicMap topics = GetSequenceOfTopics(kInvalidationTopicsCount);
auto per_user_topic_subscription_manager = BuildRegistrationManager();
// Subscribe to some topics (and provide an InstanceID token).
{
base::HistogramTester histograms;
AddCorrectSubscriptionResponse(/*private_topic=*/"", "original_token");
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, "original_token");
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
topics, RequestType::kSubscribe, Status::Success()));
histograms.ExpectUniqueSample(
kTokenStateHistogram, TokenStateOnSubscriptionRequest::kTokenWasEmpty,
1);
}
ASSERT_EQ(TopicSetFromTopics(topics),
per_user_topic_subscription_manager->GetSubscribedTopicsForTest());
ASSERT_TRUE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
// Call UpdateSubscribedTopics again with the same token.
{
base::HistogramTester histograms;
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, "original_token");
// Nothing happens, so no need to wait for anything.
histograms.ExpectUniqueSample(
kTokenStateHistogram, TokenStateOnSubscriptionRequest::kTokenUnchanged,
1);
}
// Topic subscriptions are unchanged.
ASSERT_EQ(TopicSetFromTopics(topics),
per_user_topic_subscription_manager->GetSubscribedTopicsForTest());
ASSERT_TRUE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
// Call UpdateSubscribedTopics again, but now with a different token.
{
base::HistogramTester histograms;
AddCorrectSubscriptionResponse(/*private_topic=*/"", "different_token");
per_user_topic_subscription_manager->UpdateSubscribedTopics(
topics, "different_token");
WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
topics, RequestType::kSubscribe, Status::Success()));
histograms.ExpectUniqueSample(
kTokenStateHistogram, TokenStateOnSubscriptionRequest::kTokenChanged,
1);
}
// Topic subscriptions are still the same (all topics were re-subscribed).
ASSERT_EQ(TopicSetFromTopics(topics),
per_user_topic_subscription_manager->GetSubscribedTopicsForTest());
ASSERT_TRUE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
// Call ClearInstanceIDToken.
{
base::HistogramTester histograms;
per_user_topic_subscription_manager->ClearInstanceIDToken();
histograms.ExpectUniqueSample(
kTokenStateHistogram, TokenStateOnSubscriptionRequest::kTokenCleared,
1);
}
// Topic subscriptions are gone now.
ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
.empty());
ASSERT_TRUE(
per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
}
} // namespace invalidation