// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "base/barrier_callback.h"
#include "base/barrier_closure.h"
#include "base/check_deref.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/notreached.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/ash/floating_sso/cookie_sync_conversions.h"
#include "chrome/browser/ash/floating_sso/cookie_sync_test_util.h"
#include "chrome/browser/ash/floating_sso/floating_sso_service.h"
#include "chrome/browser/ash/floating_sso/floating_sso_service_factory.h"
#include "chrome/browser/ash/floating_sso/floating_sso_sync_bridge.h"
#include "chrome/browser/policy/policy_test_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sync/data_type_store_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/keyed_service/core/dependency_graph.h"
#include "components/keyed_service/core/keyed_service_base_factory.h"
#include "components/policy/policy_constants.h"
#include "components/prefs/pref_service.h"
#include "components/sync/base/pref_names.h"
#include "components/sync/model/client_tag_based_data_type_processor.h"
#include "components/sync/model/data_type_store.h"
#include "components/sync/model/data_type_store_service.h"
#include "components/sync/model/entity_change.h"
#include "components/sync/protocol/cookie_specifics.pb.h"
#include "components/sync/test/mock_data_type_local_change_processor.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_test.h"
#include "net/cookies/cookie_util.h"
#include "services/network/public/mojom/cookie_manager.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
namespace ash::floating_sso {
namespace {
using testing::_;
// Cookie that passes the Floating SSO filters.
constexpr char kStandardCookieLine[] = "CookieName=CookieValue; max-age=3600";
constexpr char kCookieName[] = "CookieName";
// Unique key for standard cookie (kStandardCookieLine and kNonGoogleURL).
// Has cross-site ancestor (true), name (CookieName), domain + path
// (example.com/), kSecure scheme (2), port (8888).
constexpr char kCookieUniqueKey[] = "trueCookieNameexample.com/28888";
class CookieChangeListener : public network::mojom::CookieChangeListener {
public:
// Create a change listener for all cookies.
CookieChangeListener(
network::mojom::CookieManager* cookie_manager,
base::RepeatingCallback<void(const net::CookieChangeInfo&)> callback)
: callback_(std::move(callback)), receiver_(this) {
cookie_manager->AddGlobalChangeListener(
receiver_.BindNewPipeAndPassRemote());
}
// Create a change listener limited to a specific URL.
CookieChangeListener(
network::mojom::CookieManager* cookie_manager,
const GURL& url,
base::RepeatingCallback<void(const net::CookieChangeInfo&)> callback)
: callback_(std::move(callback)), receiver_(this) {
cookie_manager->AddCookieChangeListener(
url, std::nullopt, receiver_.BindNewPipeAndPassRemote());
}
// network::mojom::CookieChangeListener:
void OnCookieChange(const net::CookieChangeInfo& change) override {
callback_.Run(change);
}
private:
base::RepeatingCallback<void(const net::CookieChangeInfo&)> callback_;
mojo::Receiver<network::mojom::CookieChangeListener> receiver_;
};
std::optional<net::CanonicalCookie> GetCookie(
network::mojom::CookieManager* cookie_manager,
const std::string& name) {
base::test::TestFuture<const std::vector<net::CanonicalCookie>&> future;
cookie_manager->GetAllCookies(future.GetCallback());
for (const net::CanonicalCookie& cookie : future.Take()) {
if (cookie.Name() == name) {
return std::make_optional<net::CanonicalCookie>(cookie);
}
}
return std::nullopt;
}
// Returns if the cookie was added successfully.
bool SetCookie(network::mojom::CookieManager* cookie_manager,
const GURL& url,
const net::CanonicalCookie& cookie) {
base::test::TestFuture<net::CookieAccessResult> future;
cookie_manager->SetCanonicalCookie(cookie, url,
net::CookieOptions::MakeAllInclusive(),
future.GetCallback());
return future.Take().status.IsInclude();
}
// Returns if the cookie was added successfully.
bool SetCookie(network::mojom::CookieManager* cookie_manager,
const net::CanonicalCookie& cookie) {
GURL url = net::cookie_util::SimulatedCookieSource(cookie, "https");
return SetCookie(cookie_manager, url, cookie);
}
// Returns if the cookie was added successfully.
bool SetCookie(network::mojom::CookieManager* cookie_manager,
const GURL& url,
const std::string& cookie_line) {
auto cookie = net::CanonicalCookie::CreateForTesting(
url, cookie_line, base::Time::Now(),
/*server_time=*/std::nullopt,
/*cookie_partition_key=*/std::nullopt, net::CookieSourceType::kOther);
return SetCookie(cookie_manager, url, *cookie);
}
// Returns the number of deleted cookies.
uint32_t DeleteCookies(network::mojom::CookieManager* cookie_manager,
network::mojom::CookieDeletionFilter filter) {
base::test::TestFuture<uint32_t> future;
cookie_manager->DeleteCookies(
network::mojom::CookieDeletionFilter::New(filter), future.GetCallback());
return future.Get();
}
} // namespace
class FloatingSsoTest : public policy::PolicyTest {
public:
FloatingSsoTest() {
feature_list_.InitAndEnableFeature(ash::features::kFloatingSso);
}
~FloatingSsoTest() override = default;
void SetUpOnMainThread() override {
network::mojom::NetworkContext* network_context =
profile()->GetDefaultStoragePartition()->GetNetworkContext();
network_context->GetCookieManager(
cookie_manager_.BindNewPipeAndPassReceiver());
}
protected:
void SetFloatingSsoEnabledPolicy(bool policy_value) {
policy::PolicyTest::SetPolicy(&policies_, policy::key::kFloatingSsoEnabled,
base::Value(policy_value));
provider_.UpdateChromePolicy(policies_);
}
void SetSyncCookiesPref(bool pref_value) {
profile()->GetPrefs()->SetBoolean(syncer::prefs::internal::kSyncCookies,
pref_value);
}
void SetSyncDisabledPolicy(bool policy_value) {
policy::PolicyTest::SetPolicy(&policies_, policy::key::kSyncDisabled,
base::Value(policy_value));
provider_.UpdateChromePolicy(policies_);
}
void SetFloatingSsoDomainBlocklistPolicy(const std::string& domain) {
base::Value::List domains;
domains.Append(domain);
policy::PolicyTest::SetPolicy(&policies_,
policy::key::kFloatingSsoDomainBlocklist,
base::Value(std::move(domains)));
provider_.UpdateChromePolicy(policies_);
}
void SetFloatingSsoDomainBlocklistExceptionsPolicy(
const std::string& domain) {
base::Value::List domains;
if (!domain.empty()) {
domains.Append(domain);
}
policy::PolicyTest::SetPolicy(
&policies_, policy::key::kFloatingSsoDomainBlocklistExceptions,
base::Value(std::move(domains)));
provider_.UpdateChromePolicy(policies_);
}
void EnableAllFloatingSsoSettings() {
SetFloatingSsoEnabledPolicy(/*policy_value=*/true);
SetSyncCookiesPref(/*pref_value=*/true);
SetSyncDisabledPolicy(/*policy_value=*/false);
}
bool IsFloatingSsoServiceRegistered() {
std::vector<raw_ptr<DependencyNode, VectorExperimental>> nodes;
const bool success = BrowserContextDependencyManager::GetInstance()
->GetDependencyGraphForTesting()
.GetConstructionOrder(&nodes);
EXPECT_TRUE(success);
return base::Contains(
nodes, "FloatingSsoService",
[](const DependencyNode* node) -> std::string_view {
return static_cast<const KeyedServiceBaseFactory*>(node)->name();
});
}
Profile* profile() { return browser()->profile(); }
FloatingSsoService& floating_sso_service() {
return CHECK_DEREF(FloatingSsoServiceFactory::GetForProfile(profile()));
}
network::mojom::CookieManager* cookie_manager() {
return cookie_manager_.get();
}
const FloatingSsoSyncBridge::CookieSpecificsEntries& GetStoreEntries() {
return floating_sso_service()
.GetBridgeForTesting()
->CookieSpecificsInStore();
}
void AddCookieAndWaitForCommit(network::mojom::CookieManager* cookie_manager,
const GURL& url,
const std::string& cookie_line) {
// Used for waiting for the store commit to be finalized.
base::test::TestFuture<void> commit_future;
floating_sso_service()
.GetBridgeForTesting()
->SetOnStoreCommitCallbackForTest(commit_future.GetRepeatingCallback());
// Used for waiting for the cookie change event (INSERTED) to be dispatched.
base::test::TestFuture<const net::CookieChangeInfo&> cookie_change_future;
CookieChangeListener listener(cookie_manager, url,
cookie_change_future.GetRepeatingCallback());
// Add cookie.
ASSERT_TRUE(SetCookie(cookie_manager, url, cookie_line));
EXPECT_EQ(cookie_change_future.Take().cause,
net::CookieChangeCause::INSERTED);
commit_future.Get();
}
void DeleteCookieAndWaitForCommit(
network::mojom::CookieManager* cookie_manager,
const GURL& url,
const std::string& cookie_name) {
// Used for waiting for the store commit to be finalized.
base::test::TestFuture<void> commit_future;
floating_sso_service()
.GetBridgeForTesting()
->SetOnStoreCommitCallbackForTest(commit_future.GetRepeatingCallback());
// Used for waiting for the cookie change event (EXPLICIT) to be dispatched.
base::test::TestFuture<const net::CookieChangeInfo&> cookie_change_future;
CookieChangeListener listener(cookie_manager, url,
cookie_change_future.GetRepeatingCallback());
// Delete cookie.
network::mojom::CookieDeletionFilter filter;
filter.cookie_name = cookie_name;
ASSERT_EQ(DeleteCookies(cookie_manager, filter), 1u);
EXPECT_EQ(cookie_change_future.Take().cause,
net::CookieChangeCause::EXPLICIT);
commit_future.Get();
}
void UpdateCookieAndWaitForCommit(
network::mojom::CookieManager* cookie_manager,
const GURL& url,
const std::string& cookie_name) {
// Used for waiting for the two store commits to be finalized.
base::test::TestFuture<void> commit_future;
floating_sso_service()
.GetBridgeForTesting()
->SetOnStoreCommitCallbackForTest(base::BarrierClosure(
/*num_callbacks=*/2, commit_future.GetRepeatingCallback()));
// Used for waiting for the cookie change events (OVERWRITE, INSERTED) to be
// dispatched.
base::test::TestFuture<std::vector<net::CookieChangeInfo>>
cookie_change_future;
CookieChangeListener listener(
cookie_manager, url,
base::BarrierCallback<const net::CookieChangeInfo&>(
/*num_callbacks=*/2, cookie_change_future.GetRepeatingCallback()));
// Update cookie.
auto cookie = GetCookie(cookie_manager, cookie_name);
ASSERT_TRUE(cookie.has_value());
cookie.value().SetLastAccessDate(base::Time::Now());
cookie_manager->SetCanonicalCookie(*cookie, url,
net::CookieOptions::MakeAllInclusive(),
base::DoNothing());
// Updating an existing cookie is a two-phase delete + insert operation, so
// two cookie change events are triggered.
EXPECT_THAT(cookie_change_future.Take(),
testing::ElementsAre(
testing::Field("cause", &net::CookieChangeInfo::cause,
net::CookieChangeCause::OVERWRITE),
testing::Field("cause", &net::CookieChangeInfo::cause,
net::CookieChangeCause::INSERTED)));
commit_future.Get();
}
mojo::Remote<network::mojom::CookieManager> cookie_manager_;
base::test::ScopedFeatureList feature_list_;
const GURL kNonGoogleURL = GURL("https://example.com:8888");
policy::PolicyMap policies_;
};
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, ServiceRegistered) {
ASSERT_TRUE(IsFloatingSsoServiceRegistered());
}
// FloatingSsoEnabled policy is disabled, cookies are not added to the store.
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, FloatingSsoPolicyDisabled) {
auto& service = floating_sso_service();
SetFloatingSsoEnabledPolicy(/*policy_value=*/false);
SetSyncCookiesPref(/*pref_value=*/true);
SetSyncDisabledPolicy(/*policy_value=*/false);
ASSERT_FALSE(service.IsBoundToCookieManagerForTesting());
ASSERT_TRUE(SetCookie(cookie_manager(), kNonGoogleURL, kStandardCookieLine));
// Cookie is not added to store because the FloatingSsoEnabled policy is
// disabled.
auto store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 0u);
}
// SyncCookies pref (user toggle) is disabled, cookies are not added to the
// store.
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, SyncCookiesPrefDisabled) {
auto& service = floating_sso_service();
SetFloatingSsoEnabledPolicy(/*policy_value=*/true);
SetSyncCookiesPref(/*pref_value=*/false);
SetSyncDisabledPolicy(/*policy_value=*/false);
ASSERT_FALSE(service.IsBoundToCookieManagerForTesting());
ASSERT_TRUE(SetCookie(cookie_manager(), kNonGoogleURL, kStandardCookieLine));
// Cookie is not added to store because the SyncCookies pref is disabled.
auto store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 0u);
}
// SyncDisabled policy is enabled, cookies are not added to the store.
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, SyncDisabled) {
auto& service = floating_sso_service();
SetFloatingSsoEnabledPolicy(/*policy_value=*/true);
SetSyncCookiesPref(/*pref_value=*/true);
SetSyncDisabledPolicy(/*policy_value=*/true);
ASSERT_FALSE(service.IsBoundToCookieManagerForTesting());
ASSERT_TRUE(SetCookie(cookie_manager(), kNonGoogleURL, kStandardCookieLine));
// Cookie is not added to store because the SyncCookies pref is disabled.
auto store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 0u);
}
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, FloatingSsoEnabled) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
AddCookieAndWaitForCommit(cookie_manager(), kNonGoogleURL,
kStandardCookieLine);
// Cookie is added to store.
auto store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 1u);
EXPECT_TRUE(store_entries.contains(kCookieUniqueKey));
}
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, FloatingSsoStopsListeningAndResumes) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
AddCookieAndWaitForCommit(cookie_manager(), kNonGoogleURL,
kStandardCookieLine);
// Cookie is added to store.
const auto& store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 1u);
EXPECT_TRUE(store_entries.contains(kCookieUniqueKey));
SetFloatingSsoEnabledPolicy(/*policy_value=*/false);
base::test::TestFuture<const net::CookieChangeInfo&> cookie_change_future;
CookieChangeListener listener(cookie_manager(), kNonGoogleURL,
cookie_change_future.GetRepeatingCallback());
// Add new cookie.
ASSERT_TRUE(SetCookie(cookie_manager(), kNonGoogleURL,
"CookieNameNew=CookieValueNew; max-age=3600"));
EXPECT_EQ(cookie_change_future.Take().cause,
net::CookieChangeCause::INSERTED);
EXPECT_EQ(store_entries.size(), 1u);
// We fetch and commit both cookies again, so we need to wait for 2 commits.
base::test::TestFuture<void> commit_future;
floating_sso_service().GetBridgeForTesting()->SetOnStoreCommitCallbackForTest(
base::BarrierClosure(
/*num_callbacks=*/2, commit_future.GetRepeatingCallback()));
// Re-enabling means that the cookies are fetched again and committed to the
// store.
SetFloatingSsoEnabledPolicy(/*policy_value=*/true);
commit_future.Get();
EXPECT_EQ(store_entries.size(), 2u);
}
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, FiltersOutGoogleCookies) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("https://google.com"),
kStandardCookieLine));
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("https://accounts.google.com"),
kStandardCookieLine));
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("https://youtube.com"),
kStandardCookieLine));
// Cookies are not added to store.
auto store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 0u);
}
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, FiltersOutSessionCookies) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
ASSERT_TRUE(
SetCookie(cookie_manager(), kNonGoogleURL, "CookieName=CookieValue"));
// Cookie is not added to store.
auto store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 0u);
}
IN_PROC_BROWSER_TEST_F(FloatingSsoTest,
FiltersCookiesWithBlocklistBasicPattern) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
SetFloatingSsoDomainBlocklistPolicy("example.com");
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("https://example.com"),
kStandardCookieLine));
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("http://example.com"),
kStandardCookieLine));
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("https://www.example.com"),
kStandardCookieLine));
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("https://sub.www.example.com"),
kStandardCookieLine));
// Cookies are not added to store.
auto store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 0u);
}
IN_PROC_BROWSER_TEST_F(FloatingSsoTest,
FiltersCookiesWithBlocklistSubdomainPattern) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
SetFloatingSsoDomainBlocklistPolicy("mail.example.com");
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("https://mail.example.com"),
kStandardCookieLine));
// Cookie is not added to store.
const auto& store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 0u);
// Other subdomains are not filtered.
AddCookieAndWaitForCommit(cookie_manager(), GURL("http://example.com"),
kStandardCookieLine);
AddCookieAndWaitForCommit(cookie_manager(), GURL("https://www.example.com"),
kStandardCookieLine);
// Cookies are added to store.
EXPECT_EQ(store_entries.size(), 2u);
}
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, FiltersCookiesWithBlocklistDotPattern) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
SetFloatingSsoDomainBlocklistPolicy(".example.com");
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("https://example.com"),
kStandardCookieLine));
// Cookie is not added to store.
const auto& store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 0u);
// Subdomains are not filtered.
AddCookieAndWaitForCommit(cookie_manager(), GURL("https://mail.example.com"),
kStandardCookieLine);
// Cookie is added to store.
EXPECT_EQ(store_entries.size(), 1u);
}
IN_PROC_BROWSER_TEST_F(FloatingSsoTest,
AllowSpecificDomainsWithWildcardBlocking) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
SetFloatingSsoDomainBlocklistPolicy("*");
SetFloatingSsoDomainBlocklistExceptionsPolicy("example.com");
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("https://mail.com"),
kStandardCookieLine));
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("https://test.com"),
kStandardCookieLine));
// Cookies are not added to store.
const auto& store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 0u);
// Allows domains in exceptions.
AddCookieAndWaitForCommit(cookie_manager(), GURL("https://example.com"),
kStandardCookieLine);
// Cookie is added to store.
EXPECT_EQ(store_entries.size(), 1u);
}
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, ExceptionListTakesPrecedence) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
SetFloatingSsoDomainBlocklistPolicy("example.com");
SetFloatingSsoDomainBlocklistExceptionsPolicy("example.com");
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
AddCookieAndWaitForCommit(cookie_manager(), GURL("https://example.com"),
kStandardCookieLine);
// Cookie is added to store.
auto store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 1u);
}
IN_PROC_BROWSER_TEST_F(FloatingSsoTest,
FiltersOutGoogleCookiesDespiteException) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
SetFloatingSsoDomainBlocklistExceptionsPolicy("google.com");
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("https://google.com"),
kStandardCookieLine));
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("https://accounts.google.com"),
kStandardCookieLine));
// Cookies are not added to store.
auto store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 0u);
}
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, RespectsBlockAndExemptListUpdates) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
SetFloatingSsoDomainBlocklistPolicy("*");
SetFloatingSsoDomainBlocklistExceptionsPolicy("example.com");
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("https://mail.com"),
kStandardCookieLine));
AddCookieAndWaitForCommit(cookie_manager(), GURL("https://example.com"),
kStandardCookieLine);
// Only the example.com cookie is added to the store.
const auto& store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 1u);
constexpr char kExampleCookieUniqueKey[] = "trueCookieNameexample.com/2443";
EXPECT_TRUE(store_entries.contains(kExampleCookieUniqueKey));
// Update the block and exception list.
SetFloatingSsoDomainBlocklistPolicy("example.com");
SetFloatingSsoDomainBlocklistExceptionsPolicy("");
// Changing the cookie URL for mail.com to not trigger an update but an
// insert.
AddCookieAndWaitForCommit(cookie_manager(), GURL("https://sub.mail.com/"),
kStandardCookieLine);
ASSERT_TRUE(SetCookie(cookie_manager(), GURL("https://example.com"),
kStandardCookieLine));
// sub.mail.com cookie is added to store. The store still contains the
// example.com cookie.
EXPECT_EQ(store_entries.size(), 2u);
EXPECT_TRUE(store_entries.contains(kExampleCookieUniqueKey));
EXPECT_TRUE(store_entries.contains("trueCookieNamesub.mail.com/2443"));
}
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, CookiesFromSyncBlocked) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
SetFloatingSsoDomainBlocklistPolicy("example.com");
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
// Set up a listener for cookie change events, we expect to observe one change
// below.
base::test::TestFuture<const net::CookieChangeInfo&> cookie_change_future;
CookieChangeListener listener(cookie_manager(),
cookie_change_future.GetRepeatingCallback());
FloatingSsoSyncBridge& bridge =
CHECK_DEREF(floating_sso_service().GetBridgeForTesting());
// Create two sync changes.
syncer::EntityChangeList change_list;
// Addition of a new persistent cookie with the example.com domain. We expect
// it to not pass our filters because of the blocklist.
change_list.push_back(syncer::EntityChange::CreateAdd(
kUniqueKeysForTests[1],
CreateEntityDataForTest(CreatePredefinedCookieSpecificsForTest(
1, /*creation_time=*/base::Time::Now(), /*persistent=*/true))));
// Addition of a new persistent cookie with the test.com domain. We expect it
// to pass our filters since it does not match the blocklist.
constexpr char kTestCookieUniqueKey[] = "trueCookieNametest.com/2443";
change_list.push_back(syncer::EntityChange::CreateAdd(
kTestCookieUniqueKey,
CreateEntityDataForTest(
CreateCookieSpecificsForTest(kTestCookieUniqueKey, kCookieName,
/*creation_time=*/base::Time::Now(),
/*persistent=*/true, "www.test.com"))));
bridge.ApplyIncrementalSyncChanges(bridge.CreateMetadataChangeList(),
std::move(change_list));
// We expect one cookie to be added (`net::CookieChangeCause::INSERTED`). The
// other cookie doesn't trigger any event because it is filtered out based on
// the blocklist and doesn't get added to the cookie jar.
EXPECT_EQ(cookie_change_future.Take().cause,
net::CookieChangeCause::INSERTED);
}
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, KeepsThirdPartyCookies) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
AddCookieAndWaitForCommit(
cookie_manager(), kNonGoogleURL,
"CookieName=CookieValue; SameSite=None; Secure; max-age=3600");
// Cookie is added to store.
auto store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 1u);
EXPECT_TRUE(store_entries.contains(kCookieUniqueKey));
}
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, AddsAndDeletesCookiesToStore) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
// Add cookie.
AddCookieAndWaitForCommit(cookie_manager(), kNonGoogleURL,
kStandardCookieLine);
// Cookie is added to store.
const auto& store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 1u);
EXPECT_TRUE(store_entries.contains(kCookieUniqueKey));
// Update cookie.
UpdateCookieAndWaitForCommit(cookie_manager(), kNonGoogleURL, kCookieName);
EXPECT_EQ(store_entries.size(), 1u);
// Delete cookie.
DeleteCookieAndWaitForCommit(cookie_manager(), kNonGoogleURL, kCookieName);
EXPECT_EQ(store_entries.size(), 0u);
}
// Verify that `FloatingSsoService` reacts to cookie changes arriving to
// `FloatingSsoSyncBridge` by applying corresponding changes to the browser.
IN_PROC_BROWSER_TEST_F(FloatingSsoTest, ApplyingChangesFromSync) {
// Populate cookie jar with a cookie. We create a cookie from predefined
// specifics just for convenience (easier to use its sync storage key when we
// need it below).
sync_pb::CookieSpecifics existing_local_cookie_specifics =
CreatePredefinedCookieSpecificsForTest(
0, /*creation_time=*/base::Time::Now(), /*persistent=*/true);
std::unique_ptr<net::CanonicalCookie> existing_local_cookie =
FromSyncProto(existing_local_cookie_specifics);
ASSERT_TRUE(existing_local_cookie);
ASSERT_TRUE(SetCookie(cookie_manager(), *existing_local_cookie));
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
// Set up a listener for cookie change events, we expect to observe two
// changes below.
base::test::TestFuture<std::vector<net::CookieChangeInfo>>
cookie_change_future;
CookieChangeListener listener(
cookie_manager(),
base::BarrierCallback<const net::CookieChangeInfo&>(
/*num_callbacks=*/2, cookie_change_future.GetRepeatingCallback()));
// This change list will only contain a Sync change which shouldn't result in
// any browser changes.
syncer::EntityChangeList no_op_change_list;
// Addition of a new session cookie: we expect that we will not add it
// to the cookie jar since session cookies are not synced by default. Note
// that they might still come from the sync server if in the past they were
// synced based on a different policy configuration.
no_op_change_list.push_back(syncer::EntityChange::CreateAdd(
kUniqueKeysForTests[2],
CreateEntityDataForTest(CreatePredefinedCookieSpecificsForTest(
2, /*creation_time=*/base::Time::Now(), /*persistent=*/false))));
FloatingSsoSyncBridge& bridge =
CHECK_DEREF(floating_sso_service().GetBridgeForTesting());
// Apply `no_op_change_list`. We do it before applying other changes to be
// sure that in case of a bug the cookie change from this call would appear by
// the time we check all observed cookie changes at the end of the test.
bridge.ApplyIncrementalSyncChanges(bridge.CreateMetadataChangeList(),
std::move(no_op_change_list));
// Create two sync changes which will affect the browser.
syncer::EntityChangeList change_list;
// Deletion of a cookie we added at the start of the test. We expect it to
// eventually generate a `net::CookieChangeCause::EXPLICIT` event.
change_list.push_back(syncer::EntityChange::CreateDelete(
existing_local_cookie_specifics.unique_key()));
// Addition of a new persistent cookie: we expect it to pass our filters and
// eventually generate a `net::CookieChangeCause::INSERTED` event.
change_list.push_back(syncer::EntityChange::CreateAdd(
kUniqueKeysForTests[1],
CreateEntityDataForTest(CreatePredefinedCookieSpecificsForTest(
1, /*creation_time=*/base::Time::Now(), /*persistent=*/true))));
// Apply changes via the bridge - this should in turn notify
// `FloatingSsoService`, which is expected to notify the cookie manager.
bridge.ApplyIncrementalSyncChanges(bridge.CreateMetadataChangeList(),
std::move(change_list));
// We expect one cookie to be deleted (corresponds to
// `net::CookieChangeCause::EXPLICIT`) and one cookie to be added
// (`net::CookieChangeCause::INSERTED`).
EXPECT_THAT(cookie_change_future.Take(),
testing::UnorderedElementsAre(
testing::Field("cause", &net::CookieChangeInfo::cause,
net::CookieChangeCause::EXPLICIT),
testing::Field("cause", &net::CookieChangeInfo::cause,
net::CookieChangeCause::INSERTED)));
}
class FloatingSsoWithFloatingWorkspaceTest : public FloatingSsoTest {
public:
FloatingSsoWithFloatingWorkspaceTest() {
feature_list_.Reset();
feature_list_.InitWithFeatures(
{features::kFloatingSso, features::kFloatingWorkspaceV2},
/*disabled_features=*/{});
}
void SetUp() override {
// Disable Floating Workspace functionality because there is something in
// the implementation that is making this test crash.
// TODO(b/354907485): Investigate what is causing the crash and remove this
// command line argument.
base::CommandLine::ForCurrentProcess()->AppendSwitch(
ash::switches::kSafeMode);
PolicyTest::SetUp();
}
protected:
void EnableFloatingWorkspace() {
policy::PolicyTest::SetPolicy(&policies_,
policy::key::kFloatingWorkspaceV2Enabled,
base::Value(true));
provider_.UpdateChromePolicy(policies_);
}
};
IN_PROC_BROWSER_TEST_F(FloatingSsoWithFloatingWorkspaceTest,
KeepsSessionCookiesIfFloatingWorkspaceEnabled) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
EnableFloatingWorkspace();
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
AddCookieAndWaitForCommit(cookie_manager(), kNonGoogleURL,
"CookieName=CookieValue");
auto store_entries = GetStoreEntries();
EXPECT_EQ(store_entries.size(), 1u);
}
// Defines mock versions of `AddOrUpdateCookie` and `DeleteCookie` which are
// the main methods to notify the bridge about local changes. This class allows
// to test how `FloatingSsoService` calls those methods of the bridge.
class MockFloatingSsoSyncBridge : public FloatingSsoSyncBridge {
public:
explicit MockFloatingSsoSyncBridge(
std::unique_ptr<syncer::DataTypeLocalChangeProcessor> change_processor,
syncer::OnceDataTypeStoreFactory create_store_callback)
: FloatingSsoSyncBridge(std::move(change_processor),
std::move(create_store_callback)) {}
~MockFloatingSsoSyncBridge() override = default;
MOCK_METHOD(void,
AddOrUpdateCookie,
(const sync_pb::CookieSpecifics& specifics),
(override));
MOCK_METHOD(void, DeleteCookie, (const std::string& storage_key), (override));
};
class FloatingSsoWithMockedBridgeTest : public FloatingSsoTest {
public:
void SetUpInProcessBrowserTestFixture() override {
FloatingSsoTest::SetUpInProcessBrowserTestFixture();
create_services_subscription_ =
BrowserContextDependencyManager::GetInstance()
->RegisterCreateServicesCallbackForTesting(
base::BindRepeating(&FloatingSsoWithMockedBridgeTest::
OnWillCreateBrowserContextServices,
base::Unretained(this)));
}
void OnWillCreateBrowserContextServices(content::BrowserContext* context) {
FloatingSsoServiceFactory::GetInstance()->SetTestingFactory(
context, base::BindOnce([](content::BrowserContext* context)
-> std::unique_ptr<KeyedService> {
Profile* profile = Profile::FromBrowserContext(context);
return std::make_unique<FloatingSsoService>(
profile->GetPrefs(),
std::make_unique<testing::NiceMock<MockFloatingSsoSyncBridge>>(
std::make_unique<syncer::ClientTagBasedDataTypeProcessor>(
syncer::COOKIES, base::DoNothing()),
DataTypeStoreServiceFactory::GetForProfile(profile)
->GetStoreFactory()),
profile->GetDefaultStoragePartition()
->GetCookieManagerForBrowserProcess());
}));
}
void SetUpOnMainThread() override {
FloatingSsoTest::SetUpOnMainThread();
// Wait until the bridge finishes reading initial data from the store.
ASSERT_TRUE(base::test::RunUntil(
[&] { return bridge().IsInitialDataReadFinishedForTest(); }));
}
// Add a cookie as if requested by the Sync server, i.e. by calling
// `ApplyIncrementalSyncChanges` on the bridge.
void AddCookieSyncRequest(const sync_pb::CookieSpecifics& specifics) {
base::test::TestFuture<const net::CookieChangeInfo&> cookie_change_future;
CookieChangeListener listener(cookie_manager(),
cookie_change_future.GetRepeatingCallback());
syncer::EntityChangeList addition_list;
addition_list.push_back(syncer::EntityChange::CreateAdd(
specifics.unique_key(), CreateEntityDataForTest(specifics)));
bridge().ApplyIncrementalSyncChanges(bridge().CreateMetadataChangeList(),
std::move(addition_list));
// Wait for the change to be noticed by the browser.
ASSERT_EQ(cookie_change_future.Take().cause,
net::CookieChangeCause::INSERTED);
}
// Remove a cookie as if requested by the Sync server, i.e. by calling
// `ApplyIncrementalSyncChanges` on the bridge.
void RemoveCookieSyncRequest(const sync_pb::CookieSpecifics& specifics) {
base::test::TestFuture<const net::CookieChangeInfo&> cookie_change_future;
CookieChangeListener listener(cookie_manager(),
cookie_change_future.GetRepeatingCallback());
syncer::EntityChangeList deletion_list;
deletion_list.push_back(
syncer::EntityChange::CreateDelete(specifics.unique_key()));
bridge().ApplyIncrementalSyncChanges(bridge().CreateMetadataChangeList(),
std::move(deletion_list));
// Wait for the change to be noticed by the browser.
ASSERT_EQ(cookie_change_future.Take().cause,
net::CookieChangeCause::EXPLICIT);
}
testing::NiceMock<MockFloatingSsoSyncBridge>& bridge() {
return static_cast<testing::NiceMock<MockFloatingSsoSyncBridge>&>(
*floating_sso_service().GetBridgeForTesting());
}
private:
base::CallbackListSubscription create_services_subscription_;
};
IN_PROC_BROWSER_TEST_F(FloatingSsoWithMockedBridgeTest,
NoOpChangesAreNotPassedToBridge) {
auto& service = floating_sso_service();
EnableAllFloatingSsoSettings();
ASSERT_TRUE(service.IsBoundToCookieManagerForTesting());
// Below we will add and then delete a cookie via calls to
// `ApplyIncrementalSyncChanges` method of the bridge. Since
// `FloatingSsoService` observes all cookie changes, it could in theory notify
// the bridge about these changes. Check that this doesn't happen (because the
// service should not ask the bridge to perform no-op changes).
EXPECT_CALL(bridge(), AddOrUpdateCookie).Times(0);
EXPECT_CALL(bridge(), DeleteCookie).Times(0);
const sync_pb::CookieSpecifics specifics =
CreatePredefinedCookieSpecificsForTest(
0, /*creation_time=*/base::Time::Now(), /*persistent=*/true);
AddCookieSyncRequest(specifics);
RemoveCookieSyncRequest(specifics);
}
} // namespace ash::floating_sso