blob: 76810e3806c9998695f0f9bfbb36471d7cfde216 [file] [log] [blame]
// Copyright 2018 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 <string>
#include <vector>
#include "base/run_loop.h"
#include "base/stl_util.h"
#include "base/test/scoped_task_environment.h"
#include "base/threading/thread_task_runner_handle.h"
#include "components/invalidation/impl/fake_invalidation_state_tracker.h"
#include "components/invalidation/impl/fcm_invalidation_listener.h"
#include "components/invalidation/impl/json_unsafe_parser.h"
#include "components/invalidation/impl/per_user_topic_registration_manager.h"
#include "components/invalidation/impl/push_client_channel.h"
#include "components/invalidation/impl/unacked_invalidation_set_test_util.h"
#include "components/invalidation/public/invalidation_util.h"
#include "components/invalidation/public/invalidator_state.h"
#include "components/invalidation/public/object_id_invalidation_map.h"
#include "components/invalidation/public/topic_invalidation_map.h"
#include "google/cacheinvalidation/include/types.h"
#include "jingle/notifier/listener/fake_push_client.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace syncer {
namespace {
using invalidation::ObjectId;
const char kPayload1[] = "payload1";
const char kPayload2[] = "payload2";
const int64_t kVersion1 = 1LL;
const int64_t kVersion2 = 2LL;
// Fake invalidation::InvalidationClient implementation that keeps
// track of registered topics and acked handles.
class FakeInvalidationClient : public InvalidationClient {
public:
FakeInvalidationClient() : started_(false) {}
~FakeInvalidationClient() override {}
// invalidation::InvalidationClient implementation.
void Start() override { started_ = true; }
void Stop() override { started_ = false; }
private:
bool started_;
};
// Fake delegate that keeps track of invalidation counts, payloads,
// and state.
class FakeDelegate : public FCMInvalidationListener::Delegate {
public:
explicit FakeDelegate(FCMInvalidationListener* listener)
: state_(TRANSIENT_INVALIDATION_ERROR) {}
~FakeDelegate() override {}
size_t GetInvalidationCount(const Topic& topic) const {
auto it = invalidations_.find(topic);
if (it == invalidations_.end()) {
return 0;
} else {
return it->second.size();
}
}
int64_t GetVersion(const Topic& topic) const {
auto it = invalidations_.find(topic);
if (it == invalidations_.end()) {
ADD_FAILURE() << "No invalidations for topic " << topic;
return 0;
} else {
return it->second.back().version();
}
}
std::string GetPayload(const Topic& topic) const {
auto it = invalidations_.find(topic);
if (it == invalidations_.end()) {
ADD_FAILURE() << "No invalidations for topic " << topic;
return nullptr;
} else {
return it->second.back().payload();
}
}
bool IsUnknownVersion(const Topic& topic) const {
auto it = invalidations_.find(topic);
if (it == invalidations_.end()) {
ADD_FAILURE() << "No invalidations for topic " << topic;
return false;
} else {
return it->second.back().is_unknown_version();
}
}
bool StartsWithUnknownVersion(const Topic& topic) const {
auto it = invalidations_.find(topic);
if (it == invalidations_.end()) {
ADD_FAILURE() << "No invalidations for topic " << topic;
return false;
} else {
return it->second.front().is_unknown_version();
}
}
InvalidatorState GetInvalidatorState() const { return state_; }
void AcknowledgeNthInvalidation(const Topic& topic, size_t n) {
List& list = invalidations_[topic];
auto it = list.begin() + n;
it->Acknowledge();
}
void AcknowledgeAll(const Topic& topic) {
List& list = invalidations_[topic];
for (auto it = list.begin(); it != list.end(); ++it) {
it->Acknowledge();
}
}
void DropNthInvalidation(const Topic& topic, size_t n) {
List& list = invalidations_[topic];
auto it = list.begin() + n;
it->Drop();
dropped_invalidations_map_.erase(topic);
dropped_invalidations_map_.insert(std::make_pair(topic, *it));
}
void RecoverFromDropEvent(const Topic& topic) {
auto it = dropped_invalidations_map_.find(topic);
if (it != dropped_invalidations_map_.end()) {
it->second.Acknowledge();
dropped_invalidations_map_.erase(it);
}
}
// FCMInvalidationListener::Delegate implementation.
void OnInvalidate(const TopicInvalidationMap& invalidation_map) override {
TopicSet topics = invalidation_map.GetTopics();
for (const auto& topic : topics) {
const SingleObjectInvalidationSet& incoming =
invalidation_map.ForTopic(topic);
List& list = invalidations_[topic];
list.insert(list.end(), incoming.begin(), incoming.end());
}
}
void OnInvalidatorStateChange(InvalidatorState state) override {
state_ = state;
}
private:
typedef std::vector<Invalidation> List;
typedef std::map<Topic, List> Map;
typedef std::map<Topic, Invalidation> DropMap;
Map invalidations_;
InvalidatorState state_;
DropMap dropped_invalidations_map_;
};
std::unique_ptr<InvalidationClient> CreateFakeInvalidationClient(
FakeInvalidationClient** fake_invalidation_client,
NetworkChannel* network_channel,
Logger* logger,
InvalidationListener* listener) {
std::unique_ptr<FakeInvalidationClient> fake_client =
std::make_unique<FakeInvalidationClient>();
*fake_invalidation_client = fake_client.get();
return fake_client;
}
class MockRegistrationManager : public PerUserTopicRegistrationManager {
public:
MockRegistrationManager()
: PerUserTopicRegistrationManager(
nullptr /* identity_provider */,
nullptr /* pref_service */,
nullptr /* loader_factory */,
base::BindRepeating(&syncer::JsonUnsafeParser::Parse)) {}
~MockRegistrationManager() override {}
MOCK_METHOD2(UpdateRegisteredTopics,
void(const TopicSet& topics, const std::string& token));
MOCK_METHOD0(Init, void());
};
class FCMInvalidationListenerTest : public testing::Test {
protected:
FCMInvalidationListenerTest()
: kBookmarksTopic_("BOOKMARK"),
kPreferencesTopic_("PREFERENCE"),
kExtensionsTopic_("EXTENSION"),
kAppsTopic_("APP"),
fcm_sync_network_channel_(new FCMSyncNetworkChannel()),
fake_invalidation_client_(nullptr),
listener_(base::WrapUnique(fcm_sync_network_channel_)),
fake_delegate_(&listener_) {}
void SetUp() override {
StartClient();
registred_topics_.insert(kBookmarksTopic_);
registred_topics_.insert(kPreferencesTopic_);
listener_.UpdateRegisteredTopics(registred_topics_);
}
void TearDown() override { StopClient(); }
// Restart client without re-registering topics.
void RestartClient() {
StopClient();
StartClient();
}
void StartClient() {
fake_invalidation_client_ = nullptr;
std::unique_ptr<MockRegistrationManager> mock_registration_manager =
std::make_unique<MockRegistrationManager>();
registration_manager_ = mock_registration_manager.get();
listener_.Start(base::BindOnce(&CreateFakeInvalidationClient,
&fake_invalidation_client_),
&fake_delegate_, std::move(mock_registration_manager));
DCHECK(fake_invalidation_client_);
}
void StopClient() {
// listener_.StopForTest() stops the invalidation scheduler, which
// deletes any pending tasks without running them. Some tasks
// "run and delete" another task, so they must be run in order to
// avoid leaking the inner task. listener_.StopForTest() does not
// schedule any tasks, so it's both necessary and sufficient to
// drain the task queue before calling it.
fake_invalidation_client_ = nullptr;
listener_.StopForTest();
}
size_t GetInvalidationCount(const Topic& topic) const {
return fake_delegate_.GetInvalidationCount(topic);
}
std::string GetPayload(const Topic& topic) const {
return fake_delegate_.GetPayload(topic);
}
int64_t GetVersion(const Topic& topic) const {
return fake_delegate_.GetVersion(topic);
}
bool IsUnknownVersion(const Topic& topic) const {
return fake_delegate_.IsUnknownVersion(topic);
}
bool StartsWithUnknownVersion(const Topic& topic) const {
return fake_delegate_.StartsWithUnknownVersion(topic);
}
void AcknowledgeNthInvalidation(const Topic& topic, size_t n) {
fake_delegate_.AcknowledgeNthInvalidation(topic, n);
}
void DropNthInvalidation(const Topic& topic, size_t n) {
return fake_delegate_.DropNthInvalidation(topic, n);
}
void RecoverFromDropEvent(const Topic& topic) {
return fake_delegate_.RecoverFromDropEvent(topic);
}
InvalidatorState GetInvalidatorState() {
return fake_delegate_.GetInvalidatorState();
}
void AcknowledgeAll(const Topic& topic) {
fake_delegate_.AcknowledgeAll(topic);
}
TopicSet GetRegisteredTopics() const {
return listener_.GetRegisteredIdsForTest();
}
void RegisterAndFireInvalidate(const Topic& topic,
int64_t version,
const std::string& payload) {
FireInvalidate(topic, version, payload);
}
void FireInvalidate(const Topic& topic,
int64_t version,
const std::string& payload) {
listener_.Invalidate(fake_invalidation_client_, payload, topic, topic,
version);
}
void EnableNotifications() {
fcm_sync_network_channel_->NotifyChannelStateChange(INVALIDATIONS_ENABLED);
listener_.InformTokenRecieved(fake_invalidation_client_, "token");
}
void DisableNotifications(InvalidatorState state) {
fcm_sync_network_channel_->NotifyChannelStateChange(state);
}
const Topic kBookmarksTopic_;
const Topic kPreferencesTopic_;
const Topic kExtensionsTopic_;
const Topic kAppsTopic_;
TopicSet registred_topics_;
private:
base::test::ScopedTaskEnvironment task_environment_;
FCMSyncNetworkChannel* fcm_sync_network_channel_;
MockRegistrationManager* registration_manager_;
protected:
// A derrived test needs direct access to this.
FakeInvalidationStateTracker fake_tracker_;
// Tests need to access these directly.
FakeInvalidationClient* fake_invalidation_client_;
FCMInvalidationListener listener_;
private:
FakeDelegate fake_delegate_;
};
// Invalidation tests.
// Fire an invalidation without a payload. It should be processed,
// the payload should remain empty, and the version should be updated.
TEST_F(FCMInvalidationListenerTest, InvalidateNoPayload) {
const Topic& topic = kBookmarksTopic_;
RegisterAndFireInvalidate(topic, kVersion1, std::string());
ASSERT_EQ(1U, GetInvalidationCount(topic));
ASSERT_FALSE(IsUnknownVersion(topic));
EXPECT_EQ(kVersion1, GetVersion(topic));
EXPECT_EQ("", GetPayload(topic));
}
// Fire an invalidation with an empty payload. It should be
// processed, the payload should remain empty, and the version should
// be updated.
TEST_F(FCMInvalidationListenerTest, InvalidateEmptyPayload) {
const Topic& topic = kBookmarksTopic_;
RegisterAndFireInvalidate(topic, kVersion1, std::string());
ASSERT_EQ(1U, GetInvalidationCount(topic));
ASSERT_FALSE(IsUnknownVersion(topic));
EXPECT_EQ(kVersion1, GetVersion(topic));
EXPECT_EQ("", GetPayload(topic));
}
// Fire an invalidation with a payload. It should be processed, and
// both the payload and the version should be updated.
TEST_F(FCMInvalidationListenerTest, InvalidateWithPayload) {
const Topic& topic = kPreferencesTopic_;
RegisterAndFireInvalidate(topic, kVersion1, kPayload1);
ASSERT_EQ(1U, GetInvalidationCount(topic));
ASSERT_FALSE(IsUnknownVersion(topic));
EXPECT_EQ(kVersion1, GetVersion(topic));
EXPECT_EQ(kPayload1, GetPayload(topic));
}
// Fire ten invalidations in a row. All should be received.
TEST_F(FCMInvalidationListenerTest, ManyInvalidations_NoDrop) {
const int kRepeatCount = 10;
const Topic& topic = kPreferencesTopic_;
int64_t initial_version = kVersion1;
for (int64_t i = initial_version; i < initial_version + kRepeatCount; ++i) {
RegisterAndFireInvalidate(topic, i, kPayload1);
}
ASSERT_EQ(static_cast<size_t>(kRepeatCount), GetInvalidationCount(topic));
ASSERT_FALSE(IsUnknownVersion(topic));
EXPECT_EQ(kPayload1, GetPayload(topic));
EXPECT_EQ(initial_version + kRepeatCount - 1, GetVersion(topic));
}
// Fire an invalidation for an unregistered object topic with a payload. It
// should still be processed, and both the payload and the version should be
// updated.
TEST_F(FCMInvalidationListenerTest, InvalidateBeforeRegistration_Simple) {
const Topic kUnregisteredId = "unregistered";
const Topic& topic = kUnregisteredId;
TopicSet topics;
topics.insert(topic);
EXPECT_EQ(0U, GetInvalidationCount(topic));
FireInvalidate(topic, kVersion1, kPayload1);
ASSERT_EQ(0U, GetInvalidationCount(topic));
EnableNotifications();
listener_.Ready(fake_invalidation_client_);
listener_.UpdateRegisteredTopics(topics);
ASSERT_EQ(1U, GetInvalidationCount(topic));
ASSERT_FALSE(IsUnknownVersion(topic));
EXPECT_EQ(kVersion1, GetVersion(topic));
EXPECT_EQ(kPayload1, GetPayload(topic));
}
// Fire ten invalidations before an object registers. Some invalidations will
// be dropped an replaced with an unknown version invalidation.
TEST_F(FCMInvalidationListenerTest, InvalidateBeforeRegistration_Drop) {
const int kRepeatCount =
UnackedInvalidationSet::kMaxBufferedInvalidations + 1;
const Topic kUnregisteredId("unregistered");
const Topic& topic = kUnregisteredId;
TopicSet topics;
topics.insert(topic);
EXPECT_EQ(0U, GetInvalidationCount(topic));
int64_t initial_version = kVersion1;
for (int64_t i = initial_version; i < initial_version + kRepeatCount; ++i) {
FireInvalidate(topic, i, kPayload1);
}
EnableNotifications();
listener_.Ready(fake_invalidation_client_);
listener_.UpdateRegisteredTopics(topics);
ASSERT_EQ(UnackedInvalidationSet::kMaxBufferedInvalidations,
GetInvalidationCount(topic));
}
// Fire an invalidation, then fire another one with a lower version. Both
// should be received.
TEST_F(FCMInvalidationListenerTest, InvalidateVersion) {
const Topic& topic = kPreferencesTopic_;
RegisterAndFireInvalidate(topic, kVersion2, kPayload2);
ASSERT_EQ(1U, GetInvalidationCount(topic));
ASSERT_FALSE(IsUnknownVersion(topic));
EXPECT_EQ(kVersion2, GetVersion(topic));
EXPECT_EQ(kPayload2, GetPayload(topic));
FireInvalidate(topic, kVersion1, kPayload1);
ASSERT_EQ(2U, GetInvalidationCount(topic));
ASSERT_FALSE(IsUnknownVersion(topic));
EXPECT_EQ(kVersion1, GetVersion(topic));
EXPECT_EQ(kPayload1, GetPayload(topic));
}
// Test a simple scenario for multiple IDs.
TEST_F(FCMInvalidationListenerTest, InvalidateMultipleIds) {
RegisterAndFireInvalidate(kBookmarksTopic_, 3, std::string());
ASSERT_EQ(1U, GetInvalidationCount(kBookmarksTopic_));
ASSERT_FALSE(IsUnknownVersion(kBookmarksTopic_));
EXPECT_EQ(3, GetVersion(kBookmarksTopic_));
EXPECT_EQ("", GetPayload(kBookmarksTopic_));
// kExtensionId is not registered, so the invalidation should not get through.
FireInvalidate(kExtensionsTopic_, 2, std::string());
ASSERT_EQ(0U, GetInvalidationCount(kExtensionsTopic_));
}
// Without readying the client, disable notifications, then enable
// them. The listener should still think notifications are disabled.
TEST_F(FCMInvalidationListenerTest, EnableNotificationsNotReady) {
EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, GetInvalidatorState());
DisableNotifications(TRANSIENT_INVALIDATION_ERROR);
EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, GetInvalidatorState());
EnableNotifications();
EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, GetInvalidatorState());
}
// Enable notifications then Ready the invalidation client. The
// delegate should then be ready.
TEST_F(FCMInvalidationListenerTest, EnableNotificationsThenReady) {
EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, GetInvalidatorState());
EnableNotifications();
EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, GetInvalidatorState());
listener_.Ready(fake_invalidation_client_);
EXPECT_EQ(INVALIDATIONS_ENABLED, GetInvalidatorState());
}
// Ready the invalidation client then enable notifications. The
// delegate should then be ready.
TEST_F(FCMInvalidationListenerTest, ReadyThenEnableNotifications) {
EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, GetInvalidatorState());
listener_.Ready(fake_invalidation_client_);
EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, GetInvalidatorState());
EnableNotifications();
EXPECT_EQ(INVALIDATIONS_ENABLED, GetInvalidatorState());
}
// Enable notifications and ready the client. Then disable
// notifications with an auth error and re-enable notifications. The
// delegate should go into an auth error mode and then back out.
TEST_F(FCMInvalidationListenerTest, PushClientAuthError) {
EnableNotifications();
listener_.Ready(fake_invalidation_client_);
EXPECT_EQ(INVALIDATIONS_ENABLED, GetInvalidatorState());
DisableNotifications(INVALIDATION_CREDENTIALS_REJECTED);
EXPECT_EQ(INVALIDATION_CREDENTIALS_REJECTED, GetInvalidatorState());
EnableNotifications();
EXPECT_EQ(INVALIDATIONS_ENABLED, GetInvalidatorState());
}
// A variant of FCMInvalidationListenerTest that starts with some initial
// state. We make not attempt to abstract away the contents of this state. The
// tests that make use of this harness depend on its implementation details.
class FCMInvalidationListenerTest_WithInitialState
: public FCMInvalidationListenerTest {
public:
void SetUp() override {
UnackedInvalidationSet bm_state(ConvertTopicToId(kBookmarksTopic_));
UnackedInvalidationSet ext_state(ConvertTopicToId(kExtensionsTopic_));
Invalidation bm_unknown =
Invalidation::InitUnknownVersion(ConvertTopicToId(kBookmarksTopic_));
Invalidation bm_v100 =
Invalidation::Init(ConvertTopicToId(kBookmarksTopic_), 100, "hundred");
bm_state.Add(bm_unknown);
bm_state.Add(bm_v100);
Invalidation ext_v10 =
Invalidation::Init(ConvertTopicToId(kExtensionsTopic_), 10, "ten");
Invalidation ext_v20 =
Invalidation::Init(ConvertTopicToId(kExtensionsTopic_), 20, "twenty");
ext_state.Add(ext_v10);
ext_state.Add(ext_v20);
initial_state.insert(
std::make_pair(ConvertTopicToId(kBookmarksTopic_), bm_state));
initial_state.insert(
std::make_pair(ConvertTopicToId(kExtensionsTopic_), ext_state));
fake_tracker_.SetSavedInvalidations(initial_state);
FCMInvalidationListenerTest::SetUp();
}
UnackedInvalidationsMap initial_state;
};
} // namespace
} // namespace syncer