blob: e3736fc5197319986b32bd00585c803d04438c35 [file]
// 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 <utility>
#include "base/check_deref.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/with_feature_override.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/global_features.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/supervised_user/supervised_user_browsertest_base.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/android/android_parental_controls.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/supervised_user/test_support/features.h"
#include "components/url_matcher/url_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/dns/mock_host_resolver.h"
namespace supervised_user {
namespace {
using ::safe_search_api::ClientClassification;
using ::safe_search_api::URLCheckerClient;
using ::testing::_;
// 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 SupervisedUserBrowserTestBase {
protected:
content::WebContents* web_contents() {
return chrome_test_utils::GetActiveWebContents(this);
}
base::HistogramTester& histogram_tester() { return histogram_tester_; }
private:
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().GetPath() != "/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);
}
base::HistogramTester histogram_tester_;
};
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 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 WithFeatureOverrideAndParamInterface<BootstrapServiceTestCase>,
public SupervisedUserServiceBootstrapAndroidBrowserTestBase {
protected:
SupervisedUserServiceBootstrapAndroidBrowserTest()
: WithFeatureOverrideAndParamInterface<BootstrapServiceTestCase>(
kSupervisedUserUseUrlFilteringService) {
SetInitialSupervisedUserState(
{.android_parental_controls = {
.browser_filter =
GetTestCase().initial_browser_content_filters_value,
.search_filter =
GetTestCase().initial_search_content_filters_value,
}});
}
};
IN_PROC_BROWSER_TEST_P(SupervisedUserServiceBootstrapAndroidBrowserTest,
IncognitoIsBlockedWhenAnyFilterIsEnabled) {
ASSERT_NE(nullptr, GetSupervisedUserService());
policy::IncognitoModeAvailability expected_incognito_mode_availability =
GetTestCase().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(expected_incognito_mode_availability,
static_cast<policy::IncognitoModeAvailability>(
GetProfile()->GetPrefs()->GetInteger(
policy::policy_prefs::kIncognitoModeAvailability)));
}
IN_PROC_BROWSER_TEST_P(SupervisedUserServiceBootstrapAndroidBrowserTest,
SafeSearchIsEnforcedWhenSearchFilterIsEnabled) {
GURL request_url =
embedded_test_server()->GetURL("google.com", "/search?q=cat");
GURL expected_url = GetTestCase().initial_search_content_filters_value
? GURL(request_url.spec() + "&safe=active&ssui=on")
: request_url;
if (GetTestCase().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(GetMockUrlCheckerClient(),
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 (GetTestCase().initial_browser_content_filters_value) {
EXPECT_CALL(GetMockUrlCheckerClient(),
CheckURL(url_matcher::util::Normalize(request_url), _))
.WillOnce([](const GURL& url,
URLCheckerClient::ClientCheckCallback callback) {
std::move(callback).Run(url, ClientClassification::kAllowed);
});
} else {
EXPECT_CALL(GetMockUrlCheckerClient(),
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 (!GetTestCase().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(GetMockUrlCheckerClient(),
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 (GetTestCase().initial_browser_content_filters_value) {
histogram_tester().ExpectBucketCount(
"SupervisedUsers.WebFilterType.LocallySupervised",
WebFilterType::kTryToBlockMatureSites, 1);
} else if (GetTestCase().initial_search_content_filters_value) {
// With url service enabled, when the search filter is enabled and the
// browser filter is disabled, the web filter type indicates that it allows
// all sites.
WebFilterType expected_web_filter_type = IsFeatureEnabled()
? WebFilterType::kAllowAllSites
: WebFilterType::kDisabled;
histogram_tester().ExpectBucketCount(
"SupervisedUsers.WebFilterType.LocallySupervised",
expected_web_filter_type, 1);
} else {
histogram_tester().ExpectTotalCount(
"SupervisedUsers.WebFilterType.LocallySupervised", 0);
}
// This histogram is not recorded for locally supervised users.
histogram_tester().ExpectTotalCount("FamilyUser.WebFilterType", 0);
}
IN_PROC_BROWSER_TEST_P(SupervisedUserServiceBootstrapAndroidBrowserTest,
FamilyLinkOverridesDeviceSupervision) {
bool is_initially_supervised_locally =
GetTestCase().initial_browser_content_filters_value ||
GetTestCase().initial_search_content_filters_value;
// Device supervision is initially enabled/disabled based on the test case,
// but Family Link supervision is always disabled.
ASSERT_EQ(is_initially_supervised_locally,
GetDeviceParentalControls().IsEnabled());
ASSERT_FALSE(IsSubjectToParentalControls(*GetProfile()->GetPrefs()));
// So far there is no trace of any supervision systems conflict.
EXPECT_EQ(0, histogram_tester().GetBucketCount(
"SupervisedUsers.FamilyLinkSupervisionConflict", 1));
EnableParentalControls(*GetProfile()->GetPrefs());
// Finally, local supervision is overridden (browser sees it as disabled),
// Family Link supervision is always enabled, and if there was a conflict,
// it's recorded (possibly multiple times, because changes to both
// SupervisedUserSettingsService and AndroidParentalControls trigger pref
// calculations)
if (is_initially_supervised_locally) {
EXPECT_GT(histogram_tester().GetBucketCount(
"SupervisedUsers.FamilyLinkSupervisionConflict", 1),
0);
} else {
EXPECT_EQ(histogram_tester().GetBucketCount(
"SupervisedUsers.FamilyLinkSupervisionConflict", 1),
0);
}
EXPECT_FALSE(
AreAndroidParentalControlsEffectiveForTesting(*GetProfile()->GetPrefs()));
EXPECT_TRUE(IsSubjectToParentalControls(*GetProfile()->GetPrefs()));
}
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::Combine(testing::Bool(),
testing::ValuesIn(kBootstrapServiceTestCases)),
[](const testing::TestParamInfo<
SupervisedUserServiceBootstrapAndroidBrowserTest::ParamType>& info) {
bool feature_enabled = std::get<0>(info.param);
BootstrapServiceTestCase test_case = std::get<1>(info.param);
return base::StrCat({feature_enabled ? "With" : "Without",
kSupervisedUserUseUrlFilteringService.name, "_",
test_case.test_name});
});
// Tests the aspect where the Family Link supervision is enabled, but the
// content filters are not set.
class SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest
: public base::test::WithFeatureOverride,
public SupervisedUserServiceBootstrapAndroidBrowserTestBase {
protected:
SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest()
: base::test::WithFeatureOverride(kSupervisedUserUseUrlFilteringService) {
SetInitialSupervisedUserState({.family_link_parental_controls = true});
}
};
IN_PROC_BROWSER_TEST_P(
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_P(
SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest,
SafeSitesBlocksPages) {
GURL request_url =
embedded_test_server()->GetURL("/supervised_user/simple.html");
EXPECT_CALL(GetMockUrlCheckerClient(),
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(
SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest,
WebFilterTypeIsRecordedOnce) {
histogram_tester().ExpectBucketCount(
"SupervisedUsers.WebFilterType.FamilyLink",
WebFilterType::kTryToBlockMatureSites, 1);
histogram_tester().ExpectBucketCount(
"FamilyUser.WebFilterType", WebFilterType::kTryToBlockMatureSites, 1);
}
IN_PROC_BROWSER_TEST_P(
SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest,
FamilyLinkIsImmuneToDeviceSupervision) {
// Device supervision is initially disabled and Family Link supervision is
// initially enabled.
ASSERT_FALSE(GetDeviceParentalControls().IsEnabled());
ASSERT_TRUE(IsSubjectToParentalControls(*GetProfile()->GetPrefs()));
// Try turning the knob on the local supervision (browser filtering).
GetDeviceParentalControls().SetBrowserContentFiltersEnabledForTesting(true);
EXPECT_FALSE(
AreAndroidParentalControlsEffectiveForTesting(*GetProfile()->GetPrefs()));
EXPECT_TRUE(IsSubjectToParentalControls(*GetProfile()->GetPrefs()));
histogram_tester().ExpectBucketCount(
"SupervisedUsers.FamilyLinkSupervisionConflict", 1, 1);
// Try turning the knob on the local supervision (search filtering).
GetDeviceParentalControls().SetSearchContentFiltersEnabledForTesting(true);
EXPECT_FALSE(
AreAndroidParentalControlsEffectiveForTesting(*GetProfile()->GetPrefs()));
EXPECT_TRUE(IsSubjectToParentalControls(*GetProfile()->GetPrefs()));
histogram_tester().ExpectBucketCount(
"SupervisedUsers.FamilyLinkSupervisionConflict", 1, 2);
}
INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest);
// Tests the aspect where the Family Link supervision is disabled and the
// content filters are not set.
class SupervisedUserServiceBootstrapAndroidBrowserWithRegularUserTest
: public SupervisedUserServiceBootstrapAndroidBrowserTestBase {};
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(GetMockUrlCheckerClient(),
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(GetMockUrlCheckerClient(),
CheckURL(url_matcher::util::Normalize(url), _))
.Times(0);
EXPECT_TRUE(content::NavigateToURL(web_contents(), url));
}
} // namespace
} // namespace supervised_user