blob: 7e0d1524dc6de2ed0f71d564f79886c569ef7072 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include "base/functional/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/supervised_user/android/supervised_user_service_platform_delegate.h"
#include "chrome/browser/supervised_user/supervised_user_content_filters_service_factory.h"
#include "chrome/browser/supervised_user/supervised_user_metrics_service_factory.h"
#include "chrome/browser/supervised_user/supervised_user_navigation_observer.h"
#include "chrome/browser/supervised_user/supervised_user_service_factory.h"
#include "chrome/browser/supervised_user/supervised_user_settings_service_factory.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/test/base/android/android_browser_test.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "components/google/core/common/google_switches.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/safe_search_api/url_checker_client.h"
#include "components/supervised_user/core/browser/kids_chrome_management_url_checker_client.h"
#include "components/supervised_user/core/browser/supervised_user_test_environment.h"
#include "components/supervised_user/core/common/features.h"
#include "components/supervised_user/core/common/pref_names.h"
#include "components/supervised_user/core/common/supervised_user_constants.h"
#include "components/url_matcher/url_util.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "net/dns/mock_host_resolver.h"
namespace supervised_user {
namespace {
using ::safe_search_api::ClientClassification;
using ::safe_search_api::URLCheckerClient;
using ::testing::_;
class MockUrlCheckerClient : public URLCheckerClient {
public:
MOCK_METHOD(void, CheckURL, (const GURL& url, ClientCheckCallback callback));
};
// Covers extra behaviors available only in Clank (Android) related to
// bootstrapping the supervised user service with Content Filters Observer (how
// the browser behaves after init, with no further manipulation of the content
// filters). The tests are parametrized so that they also try to "hot start" the
// browser, simulating that the browser thinks that it was previously
// supervised. To see tests that assert dynamic behaviors (when the filters are
// altered after the browser starts and urls are loaded), see
// supervised_user_navigation_observer_android_browsertest.cc
class SupervisedUserServiceBootstrapAndroidBrowserTestBase
: public AndroidBrowserTest {
protected:
// Creates a fake content filters observer bridge for testing, and binds it to
// this test fixture.
virtual std::unique_ptr<ContentFiltersObserverBridge> CreateBridge(
std::string_view setting_name,
base::RepeatingClosure on_enabled,
base::RepeatingClosure on_disabled) = 0;
// Called just before supervised user service is created. Much like
// SetUpLocalStatePrefService, but called after prefs are registered.
virtual void SetUpPrefs(PrefService* local_state) {}
content::WebContents* web_contents() {
return chrome_test_utils::GetActiveWebContents(this);
}
MockUrlCheckerClient* url_checker_client() { return url_checker_client_; }
base::HistogramTester& histogram_tester() { return histogram_tester_; }
private:
void SetUpBrowserContextKeyedServices(
content::BrowserContext* context) override {
AndroidBrowserTest::SetUpBrowserContextKeyedServices(context);
SupervisedUserServiceFactory::GetInstance()->SetTestingFactory(
context, base::BindRepeating(
&SupervisedUserServiceBootstrapAndroidBrowserTestBase::
BuildSupervisedUserService,
base::Unretained(this)));
}
void SetUpOnMainThread() override {
AndroidBrowserTest::SetUpOnMainThread();
// Will resolve google.com to localhost, so the embedded test server can
// serve some valid content for it.
host_resolver()->AddRule("google.com", "127.0.0.1");
embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting(
[](const net::test_server::HttpRequest& request)
-> std::unique_ptr<net::test_server::HttpResponse> {
if (request.GetURL().path() != "/search") {
return nullptr;
}
// HTTP 200 OK with empty response body.
return std::make_unique<net::test_server::BasicHttpResponse>();
}));
CHECK(embedded_test_server()->Start());
}
void SetUpCommandLine(base::CommandLine* command_line) override {
AndroidBrowserTest::SetUpCommandLine(command_line);
// The production code only allows known ports (80 for http and 443 for
// https), but the embedded test server runs on a random port and adds it to
// the url spec.
command_line->AppendSwitch(switches::kIgnoreGooglePortNumbers);
}
// Builds a SupervisedUserService with a fake content filters observer bridge
// that bootstraps with initial values from the test case.
std::unique_ptr<KeyedService> BuildSupervisedUserService(
content::BrowserContext* browser_context) {
Profile* profile = Profile::FromBrowserContext(browser_context);
SetUpPrefs(profile->GetPrefs());
std::unique_ptr<SupervisedUserServicePlatformDelegate> platform_delegate =
std::make_unique<SupervisedUserServicePlatformDelegate>(*profile);
std::unique_ptr<MockUrlCheckerClient> url_checker_client =
std::make_unique<MockUrlCheckerClient>();
url_checker_client_ = url_checker_client.get();
return std::make_unique<SupervisedUserService>(
IdentityManagerFactory::GetForProfile(profile),
profile->GetDefaultStoragePartition()
->GetURLLoaderFactoryForBrowserProcess(),
*profile->GetPrefs(),
*SupervisedUserSettingsServiceFactory::GetInstance()->GetForKey(
profile->GetProfileKey()),
SupervisedUserContentFiltersServiceFactory::GetInstance()->GetForKey(
profile->GetProfileKey()),
SyncServiceFactory::GetInstance()->GetForProfile(profile),
std::make_unique<SupervisedUserURLFilter>(
*profile->GetPrefs(), std::make_unique<FakeURLFilterDelegate>(),
std::move(url_checker_client)),
std::make_unique<SupervisedUserServicePlatformDelegate>(*profile),
base::BindRepeating(
&SupervisedUserServiceBootstrapAndroidBrowserTestBase::CreateBridge,
base::Unretained(this)));
}
base::HistogramTester histogram_tester_;
raw_ptr<MockUrlCheckerClient> url_checker_client_;
base::test::ScopedFeatureList scoped_feature_list_{
kPropagateDeviceContentFiltersToSupervisedUser};
};
struct BootstrapServiceTestCase {
std::string test_name;
// Determines the value of browser device filter on browser startup.
bool initial_browser_content_filters_value;
// Determines the value of search device filter on browser startup.
bool initial_search_content_filters_value;
// Returns the initial value for the given content filters setting.
bool ResolveInitialValueForFilter(std::string_view setting_name) const {
if (setting_name == kBrowserContentFiltersSettingName) {
return initial_browser_content_filters_value;
}
if (setting_name == kSearchContentFiltersSettingName) {
return initial_search_content_filters_value;
}
NOTREACHED() << "Unsupported setting name: " << setting_name;
}
// Returns true if incognito should be blocked based on the initial values of
// the content filters settings.
bool ShouldBlockIncognito() const {
return initial_browser_content_filters_value ||
initial_search_content_filters_value;
}
};
// Tests the aspect where the Family Link supervision is not enabled, but the
// content filters are set.
class SupervisedUserServiceBootstrapAndroidBrowserTest
: public SupervisedUserServiceBootstrapAndroidBrowserTestBase,
public ::testing::WithParamInterface<BootstrapServiceTestCase> {
protected:
std::unique_ptr<ContentFiltersObserverBridge> CreateBridge(
std::string_view setting_name,
base::RepeatingClosure on_enabled,
base::RepeatingClosure on_disabled) override {
return std::make_unique<FakeContentFiltersObserverBridge>(
setting_name, on_enabled, on_disabled,
GetParam().ResolveInitialValueForFilter(setting_name));
}
};
IN_PROC_BROWSER_TEST_P(SupervisedUserServiceBootstrapAndroidBrowserTest,
IncognitoIsBlockedWhenAnyFilterIsEnabled) {
policy::IncognitoModeAvailability expected_incognito_mode_availability =
GetParam().ShouldBlockIncognito()
? policy::IncognitoModeAvailability::kDisabled
: policy::IncognitoModeAvailability::kEnabled;
// TODO(http://crbug.com/433234589): this test could actually try to open
// incognito (to no avail).
EXPECT_EQ(static_cast<policy::IncognitoModeAvailability>(
GetProfile()->GetPrefs()->GetInteger(
policy::policy_prefs::kIncognitoModeAvailability)),
expected_incognito_mode_availability);
}
IN_PROC_BROWSER_TEST_P(SupervisedUserServiceBootstrapAndroidBrowserTest,
SafeSearchIsEnforcedWhenSearchFilterIsEnabled) {
GURL request_url =
embedded_test_server()->GetURL("google.com", "/search?q=cat");
GURL expected_url = GetParam().initial_search_content_filters_value
? GURL(request_url.spec() + "&safe=active&ssui=on")
: request_url;
if (GetParam().initial_browser_content_filters_value) {
// Google search is not on the exempt list of the URL Filter: search
// requests must be explicitly allowed.
EXPECT_CALL(*url_checker_client(),
CheckURL(url_matcher::util::Normalize(expected_url), _))
.WillOnce([](const GURL& url,
URLCheckerClient::ClientCheckCallback callback) {
std::move(callback).Run(url, ClientClassification::kAllowed);
});
}
EXPECT_TRUE(
content::NavigateToURL(web_contents(), request_url, expected_url));
}
IN_PROC_BROWSER_TEST_P(SupervisedUserServiceBootstrapAndroidBrowserTest,
SafeSitesIsEnforcedWhenBrowserFilterIsEnabled) {
GURL request_url =
embedded_test_server()->GetURL("/supervised_user/simple.html");
if (GetParam().initial_browser_content_filters_value) {
EXPECT_CALL(*url_checker_client(),
CheckURL(url_matcher::util::Normalize(request_url), _))
.WillOnce([](const GURL& url,
URLCheckerClient::ClientCheckCallback callback) {
std::move(callback).Run(url, ClientClassification::kAllowed);
});
} else {
EXPECT_CALL(*url_checker_client(),
CheckURL(url_matcher::util::Normalize(request_url), _))
.Times(0);
}
// We assert here (rather than expect) because url checker mock declares the
// requested url as allowed (or never classified) so they should render at all
// times. The core of this test is to count calls to the url checker client.
ASSERT_TRUE(content::NavigateToURL(web_contents(), request_url));
ASSERT_EQ(web_contents()->GetTitle(), u"Supervised User test: simple page");
}
IN_PROC_BROWSER_TEST_P(SupervisedUserServiceBootstrapAndroidBrowserTest,
SafeSitesBlocksPagesWhenEnabled) {
if (!GetParam().initial_browser_content_filters_value) {
GTEST_SKIP() << "This test requires the browser filter to be enabled.";
}
GURL request_url =
embedded_test_server()->GetURL("/supervised_user/simple.html");
EXPECT_CALL(*url_checker_client(),
CheckURL(url_matcher::util::Normalize(request_url), _))
.WillOnce(
[](const GURL& url, URLCheckerClient::ClientCheckCallback callback) {
std::move(callback).Run(url, ClientClassification::kRestricted);
});
// We assert here (rather than expect) because url checker mock declares the
// requested url as blocked. What we do care about is that the classification
// was requested.
ASSERT_FALSE(content::NavigateToURL(web_contents(), request_url));
ASSERT_EQ(web_contents()->GetTitle(), u"Site blocked");
}
IN_PROC_BROWSER_TEST_P(SupervisedUserServiceBootstrapAndroidBrowserTest,
WebFilterTypeIsRecordedOnceWhenBrowserFilterIsEnabled) {
if (GetParam().initial_browser_content_filters_value) {
histogram_tester().ExpectBucketCount(
"SupervisedUsers.WebFilterType.LocallySupervised",
WebFilterType::kTryToBlockMatureSites, 1);
} else if (GetParam().initial_search_content_filters_value) {
histogram_tester().ExpectBucketCount(
"SupervisedUsers.WebFilterType.LocallySupervised",
WebFilterType::kDisabled, 1);
} else {
histogram_tester().ExpectTotalCount(
"SupervisedUsers.WebFilterType.LocallySupervised", 0);
}
// This histogram is not recorded for locally supervised users.
histogram_tester().ExpectTotalCount("FamilyUser.WebFilterType", 0);
}
const BootstrapServiceTestCase kBootstrapServiceTestCases[] = {
{.test_name = "AllFiltersDisabled",
.initial_browser_content_filters_value = false,
.initial_search_content_filters_value = false},
{.test_name = "AllFiltersEnabled",
.initial_browser_content_filters_value = true,
.initial_search_content_filters_value = true},
{.test_name = "SearchFilterEnabled",
.initial_browser_content_filters_value = false,
.initial_search_content_filters_value = true},
{.test_name = "BrowserFilterEnabled",
.initial_browser_content_filters_value = true,
.initial_search_content_filters_value = false}};
INSTANTIATE_TEST_SUITE_P(
,
SupervisedUserServiceBootstrapAndroidBrowserTest,
testing::ValuesIn(kBootstrapServiceTestCases),
[](const testing::TestParamInfo<BootstrapServiceTestCase>& info) {
return info.param.test_name;
});
// Tests the aspect where the Family Link supervision is enabled, but the
// content filters are not set.
class SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest
: public SupervisedUserServiceBootstrapAndroidBrowserTestBase {
protected:
std::unique_ptr<ContentFiltersObserverBridge> CreateBridge(
std::string_view setting_name,
base::RepeatingClosure on_enabled,
base::RepeatingClosure on_disabled) override {
return std::make_unique<FakeContentFiltersObserverBridge>(
setting_name, on_enabled, on_disabled, /*initial_value=*/false);
}
void SetUpPrefs(PrefService* local_state) override {
EnableParentalControls(*local_state);
}
};
IN_PROC_BROWSER_TEST_F(
SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest,
IncognitoIsBlocked) {
// TODO(http://crbug.com/433234589): this test could actually try to open
// incognito (to no avail).
EXPECT_EQ(static_cast<policy::IncognitoModeAvailability>(
GetProfile()->GetPrefs()->GetInteger(
policy::policy_prefs::kIncognitoModeAvailability)),
policy::IncognitoModeAvailability::kDisabled);
}
IN_PROC_BROWSER_TEST_F(
SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest,
SafeSitesBlocksPages) {
GURL request_url =
embedded_test_server()->GetURL("/supervised_user/simple.html");
EXPECT_CALL(*url_checker_client(),
CheckURL(url_matcher::util::Normalize(request_url), _))
.WillOnce(
[](const GURL& url, URLCheckerClient::ClientCheckCallback callback) {
std::move(callback).Run(url, ClientClassification::kRestricted);
});
// We assert here (rather than expect) because url checker mock declares the
// requested url as blocked. What we do care about is that the classification
// was requested.
ASSERT_FALSE(content::NavigateToURL(web_contents(), request_url));
ASSERT_EQ(web_contents()->GetTitle(), u"Site blocked");
}
IN_PROC_BROWSER_TEST_F(
SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest,
WebFilterTypeIsRecordedOnce) {
histogram_tester().ExpectBucketCount(
"SupervisedUsers.WebFilterType.FamilyLink",
WebFilterType::kTryToBlockMatureSites, 1);
histogram_tester().ExpectBucketCount(
"FamilyUser.WebFilterType", WebFilterType::kTryToBlockMatureSites, 1);
}
// Tests the aspect where the Family Link supervision is disabled and the
// content filters are not set.
class SupervisedUserServiceBootstrapAndroidBrowserWithRegularUserTest
: public SupervisedUserServiceBootstrapAndroidBrowserTestBase {
protected:
std::unique_ptr<ContentFiltersObserverBridge> CreateBridge(
std::string_view setting_name,
base::RepeatingClosure on_enabled,
base::RepeatingClosure on_disabled) override {
return std::make_unique<FakeContentFiltersObserverBridge>(
setting_name, on_enabled, on_disabled, /*initial_value=*/false);
}
};
IN_PROC_BROWSER_TEST_F(
SupervisedUserServiceBootstrapAndroidBrowserWithRegularUserTest,
IncognitoIsNotBlocked) {
// TODO(http://crbug.com/433234589): this test could actually try to open
// incognito (to no avail).
EXPECT_EQ(static_cast<policy::IncognitoModeAvailability>(
GetProfile()->GetPrefs()->GetInteger(
policy::policy_prefs::kIncognitoModeAvailability)),
policy::IncognitoModeAvailability::kEnabled);
}
IN_PROC_BROWSER_TEST_F(
SupervisedUserServiceBootstrapAndroidBrowserWithRegularUserTest,
SafeSitesIsNotUsed) {
GURL request_url =
embedded_test_server()->GetURL("/supervised_user/simple.html");
EXPECT_CALL(*url_checker_client(),
CheckURL(url_matcher::util::Normalize(request_url), _))
.Times(0);
ASSERT_TRUE(content::NavigateToURL(web_contents(), request_url));
ASSERT_EQ(web_contents()->GetTitle(), u"Supervised User test: simple page");
}
IN_PROC_BROWSER_TEST_F(
SupervisedUserServiceBootstrapAndroidBrowserWithRegularUserTest,
WebFilterTypeIsNotRecorded) {
histogram_tester().ExpectTotalCount(
"SupervisedUsers.WebFilterType.LocallySupervised", 0);
histogram_tester().ExpectTotalCount("FamilyUser.WebFilterType", 0);
}
IN_PROC_BROWSER_TEST_F(
SupervisedUserServiceBootstrapAndroidBrowserWithRegularUserTest,
SafeSearchIsNotEnforcedAtBrowserLevel) {
GURL url = embedded_test_server()->GetURL("google.com", "/search?q=cat");
EXPECT_CALL(*url_checker_client(),
CheckURL(url_matcher::util::Normalize(url), _))
.Times(0);
EXPECT_TRUE(content::NavigateToURL(web_contents(), url));
}
} // namespace
} // namespace supervised_user