blob: f4b7203872832262414afbc7d834ae90c444605f [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 "components/sync_sessions/session_sync_bridge.h"
#include <map>
#include <utility>
#include <vector>
#include "base/bind_helpers.h"
#include "base/json/json_writer.h"
#include "base/memory/weak_ptr.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind_test_util.h"
#include "base/test/mock_callback.h"
#include "components/sync/base/hash_util.h"
#include "components/sync/base/sync_prefs.h"
#include "components/sync/device_info/local_device_info_provider_mock.h"
#include "components/sync/model/data_batch.h"
#include "components/sync/model/metadata_batch.h"
#include "components/sync/model/metadata_change_list.h"
#include "components/sync/model/mock_model_type_change_processor.h"
#include "components/sync/model/model_type_store_test_util.h"
#include "components/sync/model/model_type_sync_bridge.h"
#include "components/sync/model/sync_metadata_store.h"
#include "components/sync/model_impl/client_tag_based_model_type_processor.h"
#include "components/sync/protocol/proto_value_conversions.h"
#include "components/sync/protocol/sync.pb.h"
#include "components/sync/test/test_matchers.h"
#include "components/sync_sessions/favicon_cache.h"
#include "components/sync_sessions/mock_sync_sessions_client.h"
#include "components/sync_sessions/test_matchers.h"
#include "components/sync_sessions/test_synced_window_delegates_getter.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace sync_sessions {
namespace {
using sync_pb::EntityMetadata;
using sync_pb::SessionSpecifics;
using syncer::DataBatch;
using syncer::EntityChangeList;
using syncer::EntityData;
using syncer::IsEmptyMetadataBatch;
using syncer::MetadataBatch;
using syncer::MetadataChangeList;
using syncer::MockModelTypeChangeProcessor;
using testing::Contains;
using testing::ElementsAre;
using testing::Eq;
using testing::InSequence;
using testing::IsEmpty;
using testing::IsNull;
using testing::Matcher;
using testing::Not;
using testing::NotNull;
using testing::Pair;
using testing::Pointee;
using testing::UnorderedElementsAre;
using testing::Return;
using testing::SaveArg;
using testing::SizeIs;
using testing::WithArg;
using testing::_;
const char kLocalSessionTag[] = "sessiontag1";
class MockSessionSyncPrefs : public syncer::SessionSyncPrefs {
public:
MockSessionSyncPrefs() = default;
~MockSessionSyncPrefs() override = default;
MOCK_CONST_METHOD0(GetSyncSessionsGUID, std::string());
MOCK_METHOD1(SetSyncSessionsGUID, void(const std::string& guid));
};
MATCHER_P(EntityDataHasSpecifics, session_specifics_matcher, "") {
return session_specifics_matcher.MatchAndExplain(arg->specifics.session(),
result_listener);
}
syncer::EntityDataPtr SpecificsToEntity(
const sync_pb::SessionSpecifics& specifics) {
syncer::EntityData data;
data.client_tag_hash = syncer::GenerateSyncableHash(
syncer::SESSIONS, SessionStore::GetClientTag(specifics));
*data.specifics.mutable_session() = specifics;
return data.PassToPtr();
}
std::map<std::string, std::unique_ptr<EntityData>> BatchToEntityDataMap(
std::unique_ptr<DataBatch> batch) {
std::map<std::string, std::unique_ptr<EntityData>> storage_key_to_data;
while (batch && batch->HasNext()) {
storage_key_to_data.insert(batch->Next());
}
return storage_key_to_data;
}
syncer::EntityDataPtr CreateTombstone(const std::string& client_tag) {
EntityData tombstone;
tombstone.client_tag_hash =
syncer::GenerateSyncableHash(syncer::SESSIONS, client_tag);
return tombstone.PassToPtr();
}
syncer::CommitResponseData CreateSuccessResponse(
const std::string& client_tag) {
syncer::CommitResponseData response;
response.client_tag_hash =
syncer::GenerateSyncableHash(syncer::SESSIONS, client_tag);
response.sequence_number = 1;
return response;
}
class SessionSyncBridgeTest : public ::testing::Test {
protected:
SessionSyncBridgeTest()
: store_(syncer::ModelTypeStoreTestUtil::CreateInMemoryStoreForTest(
syncer::SESSIONS)),
favicon_cache_(/*favicon_service=*/nullptr,
/*history_service=*/nullptr,
/*max_sync_favicon_limit=*/0) {
ON_CALL(mock_sync_sessions_client_, GetSyncedWindowDelegatesGetter())
.WillByDefault(Return(&window_getter_));
ON_CALL(mock_sync_sessions_client_, GetLocalSessionEventRouter())
.WillByDefault(Return(window_getter_.router()));
ON_CALL(mock_sync_prefs_, GetSyncSessionsGUID())
.WillByDefault(Return(kLocalSessionTag));
// Even if we use NiceMock, let's be strict about errors and let tests
// explicitly list them.
EXPECT_CALL(mock_processor_, ReportError(_)).Times(0);
}
~SessionSyncBridgeTest() override {}
void InitializeBridge() {
// Instantiate the bridge.
bridge_ = std::make_unique<SessionSyncBridge>(
&mock_sync_sessions_client_, &mock_sync_prefs_,
&mock_device_info_provider_,
/*store_factory=*/
syncer::ModelTypeStoreTestUtil::FactoryForForwardingStore(store_.get()),
mock_foreign_sessions_updated_callback_.Get(),
mock_processor_.CreateForwardingProcessor());
real_processor_ =
std::make_unique<syncer::ClientTagBasedModelTypeProcessor>(
syncer::SESSIONS, /*dump_stack=*/base::DoNothing(),
/*commit_only=*/false);
mock_processor_.DelegateCallsByDefaultTo(real_processor_.get());
}
void ShutdownBridge() {
bridge_.reset();
// The mock is still delegating to |real_processor_|, so we reset it too.
ASSERT_TRUE(testing::Mock::VerifyAndClear(&mock_processor_));
real_processor_.reset();
}
void StartSyncing(const std::vector<SessionSpecifics>& remote_data = {}) {
// DeviceInfo is provided when sync is being enabled, which should lead to
// ModelReadyToSync().
mock_device_info_provider_.Initialize(std::make_unique<syncer::DeviceInfo>(
"cache_guid", "Wayne Gretzky's Hacking Box", "Chromium 10k",
"Chrome 10k", sync_pb::SyncEnums_DeviceType_TYPE_LINUX, "device_id"));
base::RunLoop loop;
bridge_->OnSyncStarting(
/*error_handler=*/base::DoNothing(),
base::BindLambdaForTesting(
[&loop](std::unique_ptr<syncer::ActivationContext>) {
loop.Quit();
}));
loop.Run();
sync_pb::ModelTypeState state;
state.set_initial_sync_done(true);
syncer::UpdateResponseDataList initial_updates;
for (const SessionSpecifics& specifics : remote_data) {
initial_updates.push_back(syncer::UpdateResponseData());
initial_updates.back().entity = SpecificsToEntity(specifics);
}
real_processor_->OnUpdateReceived(state, initial_updates);
}
std::map<std::string, std::unique_ptr<EntityData>> GetAllData() {
base::RunLoop loop;
std::unique_ptr<DataBatch> batch;
bridge_->GetAllData(base::BindLambdaForTesting(
[&loop, &batch](std::unique_ptr<DataBatch> input_batch) {
batch = std::move(input_batch);
loop.Quit();
}));
loop.Run();
EXPECT_NE(nullptr, batch);
return BatchToEntityDataMap(std::move(batch));
}
std::map<std::string, std::unique_ptr<EntityData>> GetData(
const std::vector<std::string>& storage_keys) {
base::RunLoop loop;
std::unique_ptr<DataBatch> batch;
bridge_->GetData(
storage_keys,
base::BindLambdaForTesting(
[&loop, &batch](std::unique_ptr<DataBatch> input_batch) {
batch = std::move(input_batch);
loop.Quit();
}));
loop.Run();
EXPECT_NE(nullptr, batch);
return BatchToEntityDataMap(std::move(batch));
}
std::unique_ptr<EntityData> GetData(const std::string& storage_key) {
std::map<std::string, std::unique_ptr<EntityData>> entity_data_map =
GetData(std::vector<std::string>{storage_key});
EXPECT_LE(entity_data_map.size(), 1U);
if (entity_data_map.empty()) {
return nullptr;
}
EXPECT_EQ(storage_key, entity_data_map.begin()->first);
return std::move(entity_data_map.begin()->second);
}
void ResetWindows() { window_getter_.ResetWindows(); }
TestSyncedWindowDelegate* AddWindow(
int window_id,
sync_pb::SessionWindow_BrowserType type =
sync_pb::SessionWindow_BrowserType_TYPE_TABBED) {
return window_getter_.AddWindow(type,
SessionID::FromSerializedValue(window_id));
}
TestSyncedTabDelegate* AddTab(int window_id,
const std::string& url,
int tab_id = SessionID::NewUnique().id()) {
TestSyncedTabDelegate* tab =
window_getter_.AddTab(SessionID::FromSerializedValue(window_id),
SessionID::FromSerializedValue(tab_id));
tab->Navigate(url, base::Time::Now());
return tab;
}
SessionSyncBridge* bridge() { return bridge_.get(); }
syncer::MockModelTypeChangeProcessor& mock_processor() {
return mock_processor_;
}
syncer::ClientTagBasedModelTypeProcessor* real_processor() {
return real_processor_.get();
}
base::MockCallback<base::RepeatingClosure>&
mock_foreign_sessions_updated_callback() {
return mock_foreign_sessions_updated_callback_;
}
private:
base::MessageLoop message_loop_;
const std::unique_ptr<syncer::ModelTypeStore> store_;
// Dependencies.
testing::NiceMock<MockSyncSessionsClient> mock_sync_sessions_client_;
testing::NiceMock<MockSessionSyncPrefs> mock_sync_prefs_;
syncer::LocalDeviceInfoProviderMock mock_device_info_provider_;
testing::NiceMock<MockModelTypeChangeProcessor> mock_processor_;
testing::NiceMock<base::MockCallback<base::RepeatingClosure>>
mock_foreign_sessions_updated_callback_;
TestSyncedWindowDelegatesGetter window_getter_;
FaviconCache favicon_cache_;
std::unique_ptr<SessionSyncBridge> bridge_;
std::unique_ptr<syncer::ClientTagBasedModelTypeProcessor> real_processor_;
};
TEST_F(SessionSyncBridgeTest, ShouldCallDoModelReadyToSyncWhenSyncEnabled) {
EXPECT_CALL(mock_processor(), DoModelReadyToSync(_, _)).Times(0);
InitializeBridge();
EXPECT_CALL(mock_processor(),
DoModelReadyToSync(bridge(), IsEmptyMetadataBatch()));
StartSyncing();
}
// Test that handling of local events (i.e. propagating the local state to
// sync) does not start while a session restore is in progress.
TEST_F(SessionSyncBridgeTest, ShouldDeferLocalEventDueToSessionRestore) {
const int kWindowId = 1000001;
const int kTabId1 = 1000002;
const int kTabId2 = 1000003;
// No notifications expected until OnSessionRestoreComplete().
EXPECT_CALL(mock_processor(), DoPut(_, _, _)).Times(0);
AddWindow(kWindowId)->SetIsSessionRestoreInProgress(true);
// Initial tab should be ignored (not exposed to processor) while session
// restore is in progress.
AddTab(kWindowId, "http://foo.com/", kTabId1);
InitializeBridge();
StartSyncing();
EXPECT_THAT(GetAllData(),
ElementsAre(Pair(
_, EntityDataHasSpecifics(MatchesHeader(kLocalSessionTag,
/*window_ids=*/{},
/*tab_ids=*/{})))));
// Create the actual tab, which should be ignored because session restore
// is in progress.
AddTab(kWindowId, "http://bar.com/", kTabId2);
EXPECT_THAT(GetAllData(), SizeIs(1));
// OnSessionRestoreComplete() should issue three Put() calls, one updating the
// header and one for each of the two added tabs.
EXPECT_CALL(mock_processor(), DoPut(_, _, _)).Times(3);
bridge()->OnSessionRestoreComplete();
EXPECT_THAT(GetAllData(), SizeIs(3));
}
TEST_F(SessionSyncBridgeTest, ShouldCreateHeaderByDefault) {
InitializeBridge();
EXPECT_CALL(mock_processor(), DoModelReadyToSync(_, IsEmptyMetadataBatch()));
StartSyncing();
EXPECT_THAT(GetAllData(), SizeIs(1));
}
// Tests that local windows and tabs that exist at the time the bridge is
// started (e.g. after a Chrome restart) are properly exposed via the bridge's
// GetData() and GetAllData() methods, as well as notified via Put().
TEST_F(SessionSyncBridgeTest, ShouldExposeInitialLocalTabsToProcessor) {
const int kWindowId = 1000001;
const int kTabId1 = 1000002;
const int kTabId2 = 1000003;
AddWindow(kWindowId);
AddTab(kWindowId, "http://foo.com/", kTabId1);
AddTab(kWindowId, "http://bar.com/", kTabId2);
InitializeBridge();
const std::string header_storage_key =
SessionStore::GetHeaderStorageKeyForTest(kLocalSessionTag);
const std::string tab_storage_key1 =
SessionStore::GetTabStorageKeyForTest(kLocalSessionTag, 0);
const std::string tab_storage_key2 =
SessionStore::GetTabStorageKeyForTest(kLocalSessionTag, 1);
EXPECT_CALL(mock_processor(),
DoPut(header_storage_key,
EntityDataHasSpecifics(MatchesHeader(
kLocalSessionTag, {kWindowId}, {kTabId1, kTabId2})),
_));
EXPECT_CALL(mock_processor(),
DoPut(tab_storage_key1,
EntityDataHasSpecifics(
MatchesTab(kLocalSessionTag, kWindowId, kTabId1,
/*tab_node_id=*/_, {"http://foo.com/"})),
_));
EXPECT_CALL(mock_processor(),
DoPut(tab_storage_key2,
EntityDataHasSpecifics(
MatchesTab(kLocalSessionTag, kWindowId, kTabId2,
/*tab_node_id=*/_, {"http://bar.com/"})),
_));
StartSyncing();
EXPECT_THAT(GetData(header_storage_key),
EntityDataHasSpecifics(MatchesHeader(
kLocalSessionTag, {kWindowId}, {kTabId1, kTabId2})));
EXPECT_THAT(
GetAllData(),
UnorderedElementsAre(
Pair(header_storage_key,
EntityDataHasSpecifics(MatchesHeader(
kLocalSessionTag, {kWindowId}, {kTabId1, kTabId2}))),
Pair(tab_storage_key1, EntityDataHasSpecifics(MatchesTab(
kLocalSessionTag, kWindowId, kTabId1,
/*tab_node_id=*/_, {"http://foo.com/"}))),
Pair(tab_storage_key2,
EntityDataHasSpecifics(
MatchesTab(kLocalSessionTag, kWindowId, kTabId2,
/*tab_node_id=*/_, {"http://bar.com/"})))));
}
// Tests that the creation of a new tab while sync is enabled is propagated to:
// 1) The processor, via Put().
// 2) The in-memory representation exposed via GetData().
// 3) The persisted store, exposed via GetAllData().
TEST_F(SessionSyncBridgeTest, ShouldReportLocalTabCreation) {
const int kWindowId = 1000001;
const int kTabId1 = 1000002;
const int kTabId2 = 1000003;
AddWindow(kWindowId);
AddTab(kWindowId, "http://foo.com/", kTabId1);
InitializeBridge();
StartSyncing();
ASSERT_THAT(GetAllData(), SizeIs(2));
EXPECT_CALL(mock_foreign_sessions_updated_callback(), Run()).Times(0);
// Expectations for the processor.
std::string header_storage_key;
std::string tab_storage_key;
// Tab creation triggers an update event due to the tab parented notification,
// so the event handler issues two commits as well (one for tab creation, one
// for tab update). During the first update, however, the tab is not syncable
// and is hence skipped.
testing::Expectation put_transient_header = EXPECT_CALL(
mock_processor(), DoPut(_,
EntityDataHasSpecifics(MatchesHeader(
kLocalSessionTag, {kWindowId}, {kTabId1})),
_));
EXPECT_CALL(mock_processor(),
DoPut(_,
EntityDataHasSpecifics(MatchesHeader(
kLocalSessionTag, {kWindowId}, {kTabId1, kTabId2})),
_))
.After(put_transient_header)
.WillOnce(SaveArg<0>(&header_storage_key));
EXPECT_CALL(mock_processor(),
DoPut(_,
EntityDataHasSpecifics(
MatchesTab(kLocalSessionTag, kWindowId, kTabId2,
/*tab_node_id=*/_, {"http://bar.com/"})),
_))
.WillOnce(SaveArg<0>(&tab_storage_key));
// Create the actual tab, now that we're syncing.
AddTab(kWindowId, "http://bar.com/", kTabId2);
ASSERT_THAT(header_storage_key,
Eq(SessionStore::GetHeaderStorageKeyForTest(kLocalSessionTag)));
ASSERT_THAT(tab_storage_key, Not(IsEmpty()));
// Verify the bridge's state exposed via the getters.
EXPECT_THAT(
GetAllData(),
UnorderedElementsAre(
Pair(header_storage_key,
EntityDataHasSpecifics(MatchesHeader(
kLocalSessionTag, {kWindowId}, {kTabId1, kTabId2}))),
Pair(_, EntityDataHasSpecifics(
MatchesTab(kLocalSessionTag, kWindowId, kTabId1,
/*tab_node_id=*/_, {"http://foo.com/"}))),
Pair(tab_storage_key, EntityDataHasSpecifics(MatchesTab(
kLocalSessionTag, kWindowId, kTabId2,
/*tab_node_id=*/_, {"http://bar.com/"})))));
EXPECT_THAT(GetData(header_storage_key),
EntityDataHasSpecifics(MatchesHeader(
kLocalSessionTag, {kWindowId}, {kTabId1, kTabId2})));
EXPECT_THAT(GetData(tab_storage_key),
EntityDataHasSpecifics(
MatchesTab(kLocalSessionTag, kWindowId, kTabId2,
/*tab_node_id=*/_, {"http://bar.com/"})));
}
TEST_F(SessionSyncBridgeTest, ShouldUpdateIdsDuringRestore) {
const int kWindowId = 1000001;
const int kTabIdBeforeRestore1 = 1000002;
const int kTabIdBeforeRestore2 = 1000003;
const int kTabIdAfterRestore1 = 1000004;
const int kTabIdAfterRestore2 = 1000005;
// Zero is the first assigned tab node ID.
const int kTabNodeId1 = 0;
const int kTabNodeId2 = 1;
AddWindow(kWindowId);
AddTab(kWindowId, "http://foo.com/", kTabIdBeforeRestore1);
AddTab(kWindowId, "http://bar.com/", kTabIdBeforeRestore2);
const std::string header_storage_key =
SessionStore::GetHeaderStorageKeyForTest(kLocalSessionTag);
const std::string tab_storage_key1 =
SessionStore::GetTabStorageKeyForTest(kLocalSessionTag, kTabNodeId1);
const std::string tab_storage_key2 =
SessionStore::GetTabStorageKeyForTest(kLocalSessionTag, kTabNodeId2);
InitializeBridge();
StartSyncing();
ASSERT_THAT(GetData(header_storage_key),
EntityDataHasSpecifics(
MatchesHeader(kLocalSessionTag, {kWindowId},
{kTabIdBeforeRestore1, kTabIdBeforeRestore2})));
ASSERT_THAT(GetData(tab_storage_key1),
EntityDataHasSpecifics(
MatchesTab(kLocalSessionTag, kWindowId, kTabIdBeforeRestore1,
kTabNodeId1, {"http://foo.com/"})));
ASSERT_THAT(GetData(tab_storage_key2),
EntityDataHasSpecifics(
MatchesTab(kLocalSessionTag, kWindowId, kTabIdBeforeRestore2,
kTabNodeId2, {"http://bar.com/"})));
ShutdownBridge();
// Override tabs with placeholder tab delegates.
ResetWindows();
PlaceholderTabDelegate placeholder_tab1(
SessionID::FromSerializedValue(kTabIdAfterRestore1), kTabNodeId1);
PlaceholderTabDelegate placeholder_tab2(
SessionID::FromSerializedValue(kTabIdAfterRestore2), kTabNodeId2);
TestSyncedWindowDelegate* window = AddWindow(kWindowId);
window->OverrideTabAt(0, &placeholder_tab1);
window->OverrideTabAt(1, &placeholder_tab2);
// When the bridge gets restarted, we expected tab IDs being updated, but the
// rest of the information such as navigation URLs should be reused.
EXPECT_CALL(mock_processor(),
DoPut(header_storage_key,
EntityDataHasSpecifics(MatchesHeader(
kLocalSessionTag, {kWindowId},
{kTabIdAfterRestore1, kTabIdAfterRestore2})),
_));
EXPECT_CALL(mock_processor(),
DoPut(tab_storage_key1,
EntityDataHasSpecifics(MatchesTab(
kLocalSessionTag, kWindowId, kTabIdAfterRestore1,
kTabNodeId1, {"http://foo.com/"})),
_));
EXPECT_CALL(mock_processor(),
DoPut(tab_storage_key2,
EntityDataHasSpecifics(MatchesTab(
kLocalSessionTag, kWindowId, kTabIdAfterRestore2,
kTabNodeId2, {"http://bar.com/"})),
_));
// Start the bridge again.
InitializeBridge();
StartSyncing();
EXPECT_THAT(GetData(header_storage_key),
EntityDataHasSpecifics(
MatchesHeader(kLocalSessionTag, {kWindowId},
{kTabIdAfterRestore1, kTabIdAfterRestore2})));
EXPECT_THAT(GetData(tab_storage_key1),
EntityDataHasSpecifics(
MatchesTab(kLocalSessionTag, kWindowId, kTabIdAfterRestore1,
kTabNodeId1, {"http://foo.com/"})));
EXPECT_THAT(GetData(tab_storage_key2),
EntityDataHasSpecifics(
MatchesTab(kLocalSessionTag, kWindowId, kTabIdAfterRestore2,
kTabNodeId2, {"http://bar.com/"})));
EXPECT_THAT(GetAllData(),
UnorderedElementsAre(
Pair(header_storage_key,
EntityDataHasSpecifics(MatchesHeader(
kLocalSessionTag, {kWindowId},
{kTabIdAfterRestore1, kTabIdAfterRestore2}))),
Pair(tab_storage_key1,
EntityDataHasSpecifics(MatchesTab(
kLocalSessionTag, kWindowId, kTabIdAfterRestore1,
kTabNodeId1, {"http://foo.com/"}))),
Pair(tab_storage_key2,
EntityDataHasSpecifics(MatchesTab(
kLocalSessionTag, kWindowId, kTabIdAfterRestore2,
kTabNodeId2, {"http://bar.com/"})))));
}
TEST_F(SessionSyncBridgeTest, ShouldDisableSyncAndReenable) {
const int kWindowId = 1000001;
const int kTabId = 1000002;
AddWindow(kWindowId);
AddTab(kWindowId, "http://foo.com/", kTabId);
InitializeBridge();
StartSyncing();
const std::string header_storage_key =
SessionStore::GetHeaderStorageKeyForTest(kLocalSessionTag);
ASSERT_THAT(GetData(header_storage_key),
EntityDataHasSpecifics(
MatchesHeader(kLocalSessionTag, {kWindowId}, {kTabId})));
ASSERT_THAT(GetAllData(), Not(IsEmpty()));
EXPECT_CALL(mock_processor(), DisableSync());
EXPECT_CALL(mock_processor(), DoModelReadyToSync(_, _)).Times(0);
bridge()->DisableSync();
EXPECT_CALL(mock_processor(),
DoModelReadyToSync(bridge(), IsEmptyMetadataBatch()));
StartSyncing();
ASSERT_THAT(GetData(header_storage_key),
EntityDataHasSpecifics(
MatchesHeader(kLocalSessionTag, {kWindowId}, {kTabId})));
}
// Starting sync with no local data should just store the foreign entities in
// the store and expose them via OpenTabsUIDelegate.
TEST_F(SessionSyncBridgeTest, ShouldMergForeignSession) {
const std::string kForeignSessionTag = "foreignsessiontag";
const std::string kForeignClientName = "Foreign Client Name";
const int kForeignWindowId = 2000001;
const int kForeignTabId = 2000002;
const int kForeignTabNodeId = 2003;
EXPECT_CALL(mock_processor(), UpdateStorageKey(_, _, _)).Times(0);
EXPECT_CALL(mock_processor(), DoPut(_, _, _)).Times(0);
InitializeBridge();
sync_pb::SessionSpecifics foreign_header;
foreign_header.set_session_tag(kForeignSessionTag);
foreign_header.mutable_header()->set_client_name(kForeignClientName);
foreign_header.mutable_header()->set_device_type(
sync_pb::SyncEnums_DeviceType_TYPE_LINUX);
sync_pb::SessionWindow* foreign_window =
foreign_header.mutable_header()->add_window();
foreign_window->set_browser_type(
sync_pb::SessionWindow_BrowserType_TYPE_TABBED);
foreign_window->set_window_id(kForeignWindowId);
foreign_window->add_tab(kForeignTabId);
sync_pb::SessionSpecifics foreign_tab;
foreign_tab.set_session_tag(kForeignSessionTag);
foreign_tab.set_tab_node_id(kForeignTabNodeId);
foreign_tab.mutable_tab()->add_navigation()->set_virtual_url(
"http://baz.com/");
foreign_tab.mutable_tab()->set_window_id(kForeignWindowId);
foreign_tab.mutable_tab()->set_tab_id(kForeignTabId);
EXPECT_CALL(
mock_processor(),
DoPut(_, EntityDataHasSpecifics(MatchesHeader(kLocalSessionTag, _, _)),
_));
EXPECT_CALL(mock_foreign_sessions_updated_callback(), Run());
StartSyncing({foreign_header, foreign_tab});
std::vector<const SyncedSession*> foreign_sessions;
EXPECT_TRUE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions(
&foreign_sessions));
EXPECT_THAT(foreign_sessions,
ElementsAre(MatchesSyncedSession(
kForeignSessionTag,
{{kForeignWindowId, std::vector<int>{kForeignTabId}}})));
}
TEST_F(SessionSyncBridgeTest, ShouldHandleRemoteDeletion) {
const std::string kForeignSessionTag = "foreignsessiontag";
const std::string kForeignClientName = "Foreign Client Name";
const int kForeignWindowId = 2000001;
const int kForeignTabId = 2000002;
const int kForeignTabNodeId = 2003;
InitializeBridge();
sync_pb::SessionSpecifics foreign_header;
foreign_header.set_session_tag(kForeignSessionTag);
foreign_header.mutable_header()->set_client_name(kForeignClientName);
foreign_header.mutable_header()->set_device_type(
sync_pb::SyncEnums_DeviceType_TYPE_LINUX);
sync_pb::SessionWindow* foreign_window =
foreign_header.mutable_header()->add_window();
foreign_window->set_browser_type(
sync_pb::SessionWindow_BrowserType_TYPE_TABBED);
foreign_window->set_window_id(kForeignWindowId);
foreign_window->add_tab(kForeignTabId);
sync_pb::SessionSpecifics foreign_tab;
foreign_tab.set_session_tag(kForeignSessionTag);
foreign_tab.set_tab_node_id(kForeignTabNodeId);
foreign_tab.mutable_tab()->add_navigation()->set_virtual_url(
"http://baz.com/");
foreign_tab.mutable_tab()->set_window_id(kForeignWindowId);
foreign_tab.mutable_tab()->set_tab_id(kForeignTabId);
StartSyncing({foreign_header, foreign_tab});
const sessions::SessionTab* foreign_session_tab = nullptr;
ASSERT_TRUE(bridge()->GetOpenTabsUIDelegate()->GetForeignTab(
kForeignSessionTag, SessionID::FromSerializedValue(kForeignTabId),
&foreign_session_tab));
ASSERT_THAT(foreign_session_tab, NotNull());
std::vector<const SyncedSession*> foreign_sessions;
ASSERT_TRUE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions(
&foreign_sessions));
ASSERT_THAT(foreign_sessions,
ElementsAre(MatchesSyncedSession(
kForeignSessionTag,
{{kForeignWindowId, std::vector<int>{kForeignTabId}}})));
ASSERT_TRUE(real_processor()->IsTrackingMetadata());
// Mimic receiving a remote deletion of the foreign session.
sync_pb::ModelTypeState state;
state.set_initial_sync_done(true);
syncer::UpdateResponseData deletion;
deletion.entity = CreateTombstone(SessionStore::GetClientTag(foreign_header));
deletion.response_version = 2;
EXPECT_CALL(mock_foreign_sessions_updated_callback(), Run());
real_processor()->OnUpdateReceived(state, {deletion});
foreign_session_tab = nullptr;
EXPECT_FALSE(bridge()->GetOpenTabsUIDelegate()->GetForeignTab(
kForeignSessionTag, SessionID::FromSerializedValue(kForeignTabId),
&foreign_session_tab));
EXPECT_FALSE(bridge()->GetOpenTabsUIDelegate()->GetAllForeignSessions(
&foreign_sessions));
}
TEST_F(SessionSyncBridgeTest, ShouldIgnoreRemoteDeletionOfLocalTab) {
const int kWindowId = 1000001;
const int kTabId = 1000002;
const int kTabNodeId = 0;
AddWindow(kWindowId);
AddTab(kWindowId, "http://foo.com/", kTabId);
InitializeBridge();
StartSyncing();
ASSERT_THAT(
GetAllData(),
UnorderedElementsAre(Pair(_, EntityDataHasSpecifics(
MatchesHeader(kLocalSessionTag, _, _))),
Pair(_, EntityDataHasSpecifics(MatchesTab(
kLocalSessionTag, kWindowId, kTabId,
kTabNodeId, {"http://foo.com/"})))));
ASSERT_TRUE(real_processor()->IsTrackingMetadata());
ASSERT_TRUE(real_processor()->HasLocalChangesForTest());
// Mimic receiving a commit ack for both the tab and the header entity,
// because otherwise it will be treated as conflict, and then local wins.
sync_pb::ModelTypeState state;
state.set_initial_sync_done(true);
real_processor()->OnCommitCompleted(
state, {CreateSuccessResponse(SessionStore::GetTabClientTagForTest(
kLocalSessionTag, kTabNodeId)),
CreateSuccessResponse(kLocalSessionTag)});
ASSERT_FALSE(real_processor()->HasLocalChangesForTest());
// Mimic receiving a remote deletion of a local tab.
syncer::UpdateResponseData deletion;
deletion.entity = CreateTombstone(
SessionStore::GetTabClientTagForTest(kLocalSessionTag, kTabNodeId));
deletion.response_version = 2;
real_processor()->OnUpdateReceived(state, {deletion});
// State should remain unchanged (deletion ignored).
EXPECT_THAT(
GetAllData(),
UnorderedElementsAre(Pair(_, EntityDataHasSpecifics(
MatchesHeader(kLocalSessionTag, _, _))),
Pair(_, EntityDataHasSpecifics(MatchesTab(
kLocalSessionTag, kWindowId, kTabId,
kTabNodeId, {"http://foo.com/"})))));
// Mimic a browser restart.
ShutdownBridge();
InitializeBridge();
StartSyncing();
EXPECT_THAT(GetAllData(), SizeIs(2));
}
} // namespace
} // namespace sync_sessions