| // 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 "chrome/browser/supervised_user/classify_url_navigation_throttle.h" |
| |
| #include <map> |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/supervised_user/supervised_user_service_factory.h" |
| #include "chrome/browser/supervised_user/supervised_user_content_filters_service_factory.h" |
| #include "chrome/browser/supervised_user/supervised_user_settings_service_factory.h" |
| #include "chrome/browser/supervised_user/supervised_user_test_util.h" |
| #include "chrome/test/base/chrome_render_view_host_test_harness.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "components/safe_search_api/fake_url_checker_client.h" |
| #include "components/supervised_user/core/browser/supervised_user_preferences.h" |
| #include "components/supervised_user/core/browser/supervised_user_service.h" |
| #include "components/supervised_user/core/browser/supervised_user_test_environment.h" |
| #include "components/supervised_user/core/browser/supervised_user_url_filter.h" |
| #include "components/supervised_user/core/browser/supervised_user_utils.h" |
| #include "components/supervised_user/core/common/features.h" |
| #include "components/supervised_user/core/common/supervised_user_constants.h" |
| #include "components/supervised_user/test_support/kids_management_api_server_mock.h" |
| #include "components/supervised_user/test_support/supervised_user_url_filter_test_utils.h" |
| #include "content/public/browser/navigation_throttle.h" |
| #include "content/public/test/mock_navigation_handle.h" |
| #include "content/public/test/mock_navigation_throttle_registry.h" |
| #include "content/public/test/navigation_simulator.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace supervised_user { |
| |
| namespace { |
| |
| static const char* kExampleURL = "https://example.com/"; |
| static const char* kExample1URL = "https://example1.com/"; |
| static const char* kExample2URL = "https://example2.com/"; |
| |
| void ExpectNoLatencyRecorded(base::HistogramTester* tester) { |
| tester->ExpectTotalCount(kClassifiedEarlierThanContentResponseHistogramName, |
| /*expected_count=*/0); |
| tester->ExpectTotalCount(kClassifiedLaterThanContentResponseHistogramName, |
| /*expected_count=*/0); |
| } |
| |
| class MockSupervisedUserURLFilter : public SupervisedUserURLFilter { |
| public: |
| explicit MockSupervisedUserURLFilter( |
| PrefService& prefs, |
| std::unique_ptr<SupervisedUserURLFilter::Delegate> delegate, |
| std::unique_ptr<safe_search_api::URLCheckerClient> checker_client) |
| : SupervisedUserURLFilter(prefs, |
| std::move(delegate), |
| std::move(checker_client)) {} |
| MOCK_METHOD(bool, |
| RunAsyncChecker, |
| (const GURL& url, ResultCallback callback)); |
| }; |
| |
| std::unique_ptr<KeyedService> BuildTestSupervisedUserService( |
| content::BrowserContext* browser_context) { |
| Profile* profile = Profile::FromBrowserContext(browser_context); |
| std::unique_ptr<SupervisedUserServicePlatformDelegate> platform_delegate = |
| std::make_unique<SupervisedUserServicePlatformDelegate>(*profile); |
| signin::IdentityManager* identity_manager = |
| IdentityManagerFactory::GetForProfile(profile); |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory = |
| profile->GetDefaultStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess(); |
| return std::make_unique<supervised_user::TestSupervisedUserService>( |
| 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<MockSupervisedUserURLFilter>( |
| *profile->GetPrefs(), std::make_unique<FakeURLFilterDelegate>(), |
| std::make_unique< |
| supervised_user::KidsChromeManagementURLCheckerClient>( |
| identity_manager, url_loader_factory, *profile->GetPrefs(), |
| platform_delegate->GetCountryCode(), |
| platform_delegate->GetChannel())), |
| std::make_unique<SupervisedUserServicePlatformDelegate>(*profile)); |
| } |
| |
| class ClassifyUrlNavigationThrottleTest |
| : public ChromeRenderViewHostTestHarness { |
| protected: |
| void SetUp() override { |
| ChromeRenderViewHostTestHarness::SetUp(); |
| EnableParentalControls(*profile()->GetPrefs()); |
| } |
| |
| TestingProfile::TestingFactories GetTestingFactories() const override { |
| return {TestingProfile::TestingFactory{ |
| SupervisedUserServiceFactory::GetInstance(), |
| base::BindRepeating(&BuildTestSupervisedUserService)}}; |
| } |
| |
| std::unique_ptr<content::MockNavigationThrottleRegistry> |
| CreateNavigationThrottle(const std::vector<GURL> redirects) { |
| CHECK_GT(redirects.size(), 0U) << "At least one url is required"; |
| |
| redirects_ = redirects; |
| current_url_it_ = redirects_.begin(); |
| |
| navigation_handle_ = |
| std::make_unique<::testing::NiceMock<content::MockNavigationHandle>>( |
| *current_url_it_, main_rfh()); |
| |
| // Note: this creates the throttle regardless the supervision status of the |
| // user. |
| auto registry = std::make_unique<content::MockNavigationThrottleRegistry>( |
| navigation_handle_.get(), |
| content::MockNavigationThrottleRegistry::RegistrationMode::kHold); |
| ClassifyUrlNavigationThrottle::MaybeCreateAndAdd(*registry.get()); |
| |
| if (!registry->throttles().empty()) { |
| // Add mock handlers for resume & cancel deferred. |
| registry->throttles().back()->set_resume_callback_for_testing( |
| base::BindLambdaForTesting([&]() { resume_called_ = true; })); |
| } |
| return registry; |
| } |
| |
| std::unique_ptr<content::MockNavigationThrottleRegistry> |
| CreateNavigationThrottle(const GURL& url) { |
| return CreateNavigationThrottle(std::vector<GURL>({url})); |
| } |
| |
| // Advances the pointer of the current url internally and synchronizes the |
| // navigation_handle_ accordingly: updating both the url and the redirect |
| // chain that led to it. |
| void AdvanceRedirect() { |
| current_url_it_++; |
| |
| // CHECK_NE doesn't support std::vector::iterator comparison. |
| CHECK_NE(redirects_.end() - current_url_it_, 0) |
| << "Can't advance past last redirect"; |
| |
| std::vector<GURL> redirect_chain; |
| for (auto it = redirects_.begin(); it != current_url_it_; ++it) { |
| redirect_chain.push_back(*it); |
| } |
| |
| navigation_handle_->set_url(*current_url_it_); |
| navigation_handle_->set_redirect_chain(redirect_chain); |
| } |
| |
| MockSupervisedUserURLFilter* GetSupervisedUserURLFilter() { |
| // Cast is safe: MockSupervisedUserURLFilter is created with TestingProfile, |
| // as a component of TestSupervisedUserService. |
| return static_cast<MockSupervisedUserURLFilter*>( |
| SupervisedUserServiceFactory::GetForProfile(profile())->GetURLFilter()); |
| } |
| |
| TestSupervisedUserService* GetSupervisedUserService() { |
| // Cast is safe: TestSupervisedUserService is created with TestingProfile |
| // (see ::GetTestingFactories()). |
| return static_cast<TestSupervisedUserService*>( |
| SupervisedUserServiceFactory::GetForProfile(profile())); |
| } |
| |
| base::HistogramTester* histogram_tester() { return &histogram_tester_; } |
| bool resume_called() const { return resume_called_; } |
| |
| private: |
| std::unique_ptr<content::MockNavigationHandle> navigation_handle_; |
| base::HistogramTester histogram_tester_; |
| bool resume_called_ = false; |
| |
| std::vector<GURL> redirects_; |
| std::vector<GURL>::iterator current_url_it_; |
| }; |
| |
| // This test is used to test the behavior of the throttle when the user is not |
| // supervised - all navigations are allowed, but no metrics recorded. |
| class ClassifyUrlNavigationThrottleUnsupervisedUserTest |
| : public ClassifyUrlNavigationThrottleTest { |
| protected: |
| void SetUp() override { ChromeRenderViewHostTestHarness::SetUp(); } |
| }; |
| |
| TEST_F(ClassifyUrlNavigationThrottleUnsupervisedUserTest, |
| WillNotRegisterThrottle) { |
| EXPECT_TRUE(CreateNavigationThrottle(GURL(kExampleURL))->throttles().empty()); |
| } |
| |
| TEST_F(ClassifyUrlNavigationThrottleTest, AllowedUrlsRecordedInAllowBucket) { |
| GURL allowed_url(kExampleURL); |
| supervised_user_test_util::SetManualFilterForHost( |
| profile(), allowed_url.host(), /*allowlist=*/true); |
| |
| std::unique_ptr<content::MockNavigationThrottleRegistry> registry = |
| CreateNavigationThrottle(allowed_url); |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillStartRequest()); |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillProcessResponse()); |
| |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kAllow, 1); |
| histogram_tester()->ExpectTotalCount( |
| kClassifiedEarlierThanContentResponseHistogramName, |
| /*expected_count(grew by)*/ 1); |
| } |
| |
| TEST_F(ClassifyUrlNavigationThrottleTest, |
| BlocklistedUrlsRecordedInBlockManualBucket) { |
| GURL blocked_url(kExampleURL); |
| supervised_user_test_util::SetManualFilterForHost( |
| profile(), blocked_url.host(), /*allowlist=*/false); |
| ASSERT_TRUE(GetSupervisedUserURLFilter() |
| ->GetFilteringBehavior(blocked_url) |
| .IsBlocked()); |
| |
| std::unique_ptr<content::MockNavigationThrottleRegistry> registry = |
| CreateNavigationThrottle(blocked_url); |
| ASSERT_EQ(content::NavigationThrottle::DEFER, |
| registry->throttles().back()->WillStartRequest()); |
| |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kBlockManual, 1); |
| |
| // Since this is not a success path, no latency metric is recorded. |
| ExpectNoLatencyRecorded(histogram_tester()); |
| } |
| |
| TEST_F(ClassifyUrlNavigationThrottleTest, |
| AllSitesBlockedRecordedInBlockNotInAllowlistBucket) { |
| supervised_user_test_util::SetWebFilterType(profile(), |
| WebFilterType::kCertainSites); |
| |
| std::unique_ptr<content::MockNavigationThrottleRegistry> registry = |
| CreateNavigationThrottle(GURL(kExampleURL)); |
| ASSERT_EQ(content::NavigationThrottle::DEFER, |
| registry->throttles().back()->WillStartRequest()); |
| |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kBlockNotInAllowlist, 1); |
| |
| // Since this is not a success path, no latency metric is recorded. |
| ExpectNoLatencyRecorded(histogram_tester()); |
| // As a result, the navigation is not resumed |
| EXPECT_FALSE(resume_called()); |
| } |
| |
| enum class SupervisionMode { |
| kSupervisedByFamilyLink, |
| #if BUILDFLAG(IS_ANDROID) |
| kLocalSupervision, |
| #endif // BUILDFLAG(IS_ANDROID) |
| }; |
| |
| struct AsyncCheckerTestCase { |
| std::string name; |
| SupervisionMode mode; |
| }; |
| |
| class ClassifyUrlNavigationThrottleAsyncCheckerTest |
| : public ClassifyUrlNavigationThrottleTest, |
| public ::testing::WithParamInterface<AsyncCheckerTestCase> { |
| protected: |
| void SetUp() override { |
| // Consciously bypasses direct superclass SetUp to avoid enabling Family |
| // Link parental controls for all requested supervision modes. |
| ChromeRenderViewHostTestHarness::SetUp(); |
| switch (GetParam().mode) { |
| case SupervisionMode::kSupervisedByFamilyLink: |
| EnableParentalControls(*profile()->GetPrefs()); |
| break; |
| #if BUILDFLAG(IS_ANDROID) |
| case SupervisionMode::kLocalSupervision: |
| GetSupervisedUserService() |
| ->browser_content_filters_observer_weak_ptr() |
| ->SetEnabled(true); |
| break; |
| #endif // BUILDFLAG(IS_ANDROID) |
| } |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_{ |
| kPropagateDeviceContentFiltersToSupervisedUser}; |
| #endif // BUILDFLAG(IS_ANDROID) |
| }; |
| |
| TEST_P(ClassifyUrlNavigationThrottleAsyncCheckerTest, |
| BlockedMatureSitesRecordedInBlockSafeSitesBucket) { |
| ON_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .WillByDefault([](const GURL& url, |
| MockSupervisedUserURLFilter::ResultCallback callback) { |
| std::move(callback).Run({url, FilteringBehavior::kBlock, |
| FilteringBehaviorReason::ASYNC_CHECKER}); |
| return true; |
| }); |
| EXPECT_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(GURL(kExampleURL), testing::_)) |
| .Times(1); |
| std::unique_ptr<content::MockNavigationThrottleRegistry> registry = |
| CreateNavigationThrottle(GURL(kExampleURL)); |
| ASSERT_EQ(content::NavigationThrottle::DEFER, |
| registry->throttles().back()->WillStartRequest()); |
| |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kBlockSafeSites, 1); |
| |
| // Since this is not a success path, no latency metric is recorded. |
| ExpectNoLatencyRecorded(histogram_tester()); |
| // As a result, the navigation is not resumed |
| EXPECT_FALSE(resume_called()); |
| } |
| |
| TEST_P(ClassifyUrlNavigationThrottleAsyncCheckerTest, |
| ClassificationIsFasterThanHttp) { |
| MockSupervisedUserURLFilter::ResultCallback check; |
| ON_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .WillByDefault( |
| [&check](const GURL& url, |
| MockSupervisedUserURLFilter::ResultCallback callback) { |
| check = std::move(callback); |
| return false; |
| }); |
| EXPECT_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(GURL(kExampleURL), testing::_)) |
| .Times(1); |
| |
| std::unique_ptr<content::MockNavigationThrottleRegistry> registry = |
| CreateNavigationThrottle(GURL(kExampleURL)); |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillStartRequest()); |
| |
| // Check is not completed yet |
| EXPECT_TRUE(check); |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kAllow, 0); |
| |
| // Before the throttle will be notified that the content is ready, complete |
| // the check |
| std::move(check).Run({GURL(kExampleURL), FilteringBehavior::kAllow, |
| FilteringBehaviorReason::ASYNC_CHECKER}); |
| |
| // Throttle is not blocked |
| EXPECT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillProcessResponse()); |
| |
| // As a result, the navigation hadn't had to be resumed |
| EXPECT_FALSE(resume_called()); |
| |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kAllow, 1); |
| // Since the throttle had to wait for checks to complete, it recorded a |
| // corresponding metric. |
| histogram_tester()->ExpectTotalCount( |
| kClassifiedEarlierThanContentResponseHistogramName, |
| /*expected_count=*/1); |
| } |
| |
| TEST_P(ClassifyUrlNavigationThrottleAsyncCheckerTest, |
| ClassificationIsSlowerThanHttp) { |
| MockSupervisedUserURLFilter::ResultCallback check; |
| ON_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .WillByDefault( |
| [&check](const GURL& url, |
| MockSupervisedUserURLFilter::ResultCallback callback) { |
| check = std::move(callback); |
| return false; |
| }); |
| EXPECT_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(GURL(kExampleURL), testing::_)) |
| .Times(1); |
| |
| std::unique_ptr<content::MockNavigationThrottleRegistry> registry = |
| CreateNavigationThrottle(GURL(kExampleURL)); |
| |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillStartRequest()); |
| |
| // At this point, check was not completed. |
| EXPECT_TRUE(check); |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kAllow, 0); |
| |
| // But will block at process response because the check is still |
| // pending and no filtering was completed. |
| EXPECT_EQ(content::NavigationThrottle::DEFER, |
| registry->throttles().back()->WillProcessResponse()); |
| |
| // Now complete the outstanding check |
| std::move(check).Run({GURL(kExampleURL), FilteringBehavior::kAllow, |
| FilteringBehaviorReason::ASYNC_CHECKER}); |
| |
| // As a result, the navigation is resumed (and three checks registered) |
| EXPECT_TRUE(resume_called()); |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kAllow, 1); |
| // Since the throttle had to wait for checks to complete, it recorded a |
| // corresponding metric. |
| histogram_tester()->ExpectTotalCount( |
| kClassifiedLaterThanContentResponseHistogramName, |
| /*expected_count=*/1); |
| } |
| |
| // Checks a scenario where the classification responses arrive in reverse order: |
| // Last check is completed first but is blocking, and first check is completed |
| // after it and is not blocking. Both checks complete after the response was |
| // ready for processing. |
| TEST_P(ClassifyUrlNavigationThrottleAsyncCheckerTest, |
| ReverseOrderOfResponsesAfterContentIsReady) { |
| std::vector<MockSupervisedUserURLFilter::ResultCallback> checks; |
| // Check for the first url that will complete last. |
| ON_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .WillByDefault( |
| [&checks](const GURL& url, |
| MockSupervisedUserURLFilter::ResultCallback callback) { |
| checks.push_back(std::move(callback)); |
| return false; |
| }); |
| EXPECT_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .Times(2); |
| |
| std::unique_ptr<content::MockNavigationThrottleRegistry> registry = |
| CreateNavigationThrottle({GURL(kExampleURL), GURL(kExample1URL)}); |
| |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillStartRequest()); |
| AdvanceRedirect(); |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillRedirectRequest()); |
| // As expected, the process navigation is deferred. |
| EXPECT_EQ(content::NavigationThrottle::DEFER, |
| registry->throttles().back()->WillProcessResponse()); |
| |
| // Resolve pending checks in reverse order, so that block for 2nd request |
| // comes first. |
| std::move(checks[1]).Run({GURL(kExample1URL), FilteringBehavior::kBlock, |
| FilteringBehaviorReason::ASYNC_CHECKER}); |
| std::move(checks[0]).Run({GURL(kExampleURL), FilteringBehavior::kAllow, |
| FilteringBehaviorReason::ASYNC_CHECKER}); |
| |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kAllow, 1); |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kBlockSafeSites, 1); |
| |
| // Since this is not a success path, no latency metric is recorded. |
| ExpectNoLatencyRecorded(histogram_tester()); |
| EXPECT_FALSE(resume_called()); |
| } |
| |
| const AsyncCheckerTestCase kAsyncCheckerTestCases[] = { |
| {.name = "SupervisedByFamilyLink", |
| .mode = SupervisionMode::kSupervisedByFamilyLink} |
| #if BUILDFLAG(IS_ANDROID) |
| , |
| {.name = "LocalSupervision", .mode = SupervisionMode::kLocalSupervision} |
| #endif // BUILDFLAG(IS_ANDROID) |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| , |
| ClassifyUrlNavigationThrottleAsyncCheckerTest, |
| testing::ValuesIn(kAsyncCheckerTestCases), |
| [](const testing::TestParamInfo<AsyncCheckerTestCase>& info) { |
| return info.param.name; |
| }); |
| |
| struct TestCase { |
| std::string name; |
| std::vector<std::string> redirect_chain; |
| }; |
| |
| class ClassifyUrlNavigationThrottleParallelizationTest |
| : public ClassifyUrlNavigationThrottleTest, |
| public testing::WithParamInterface<TestCase> { |
| protected: |
| static const std::vector<GURL> GetRedirectChain() { |
| CHECK_EQ(GetParam().redirect_chain.size(), 3U) |
| << "Tests assume one request and two redirects"; |
| std::vector<GURL> urls; |
| for (const auto& redirect : GetParam().redirect_chain) { |
| urls.push_back(GURL(redirect)); |
| } |
| return urls; |
| } |
| }; |
| |
| TEST_P(ClassifyUrlNavigationThrottleParallelizationTest, |
| ClassificationIsFasterThanHttp) { |
| std::vector<MockSupervisedUserURLFilter::ResultCallback> checks; |
| ON_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .WillByDefault( |
| [&checks](const GURL& url, |
| MockSupervisedUserURLFilter::ResultCallback callback) { |
| checks.push_back(std::move(callback)); |
| // Asynchronous behavior all the time. |
| return false; |
| }); |
| EXPECT_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .Times(3); |
| |
| // This navigation is a 3-piece redirect chain on the same URL: |
| std::unique_ptr<content::MockNavigationThrottleRegistry> registry = |
| CreateNavigationThrottle(GetRedirectChain()); |
| |
| // It will allow request and two redirects to pass... |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillStartRequest()); |
| AdvanceRedirect(); |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillRedirectRequest()); |
| AdvanceRedirect(); |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillRedirectRequest()); |
| |
| // No checks are completed yet |
| EXPECT_THAT(checks, testing::SizeIs(3)); |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kAllow, 0); |
| |
| // Before the throttle will be notified that the content is ready, complete |
| // all checks |
| for (auto& check : checks) { |
| std::move(check).Run({GURL(kExampleURL), FilteringBehavior::kAllow, |
| FilteringBehaviorReason::ASYNC_CHECKER}); |
| } |
| |
| // Throttle is not blocked |
| EXPECT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillProcessResponse()); |
| |
| // As a result, the navigation hadn't had to be resumed |
| EXPECT_FALSE(resume_called()); |
| |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kAllow, 3); |
| // Since the throttle had to wait for checks to complete, it recorded a |
| // corresponding metric. |
| histogram_tester()->ExpectTotalCount( |
| kClassifiedEarlierThanContentResponseHistogramName, |
| /*expected_count=*/1); |
| } |
| |
| TEST_P(ClassifyUrlNavigationThrottleParallelizationTest, |
| OutOfOrderClassification) { |
| std::vector<MockSupervisedUserURLFilter::ResultCallback> checks; |
| ON_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .WillByDefault( |
| [&checks](const GURL& url, |
| MockSupervisedUserURLFilter::ResultCallback callback) { |
| checks.push_back(std::move(callback)); |
| // Asynchronous behavior all the time. |
| return false; |
| }); |
| EXPECT_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .Times(3); |
| |
| // This navigation is a 3-piece redirect chain on the same URL: |
| std::unique_ptr<content::MockNavigationThrottleRegistry> registry = |
| CreateNavigationThrottle(GetRedirectChain()); |
| |
| // It will allow request and two redirects to pass... |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillStartRequest()); |
| AdvanceRedirect(); |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillRedirectRequest()); |
| AdvanceRedirect(); |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillRedirectRequest()); |
| |
| // No checks are completed yet |
| EXPECT_THAT(checks, testing::SizeIs(3)); |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kAllow, 0); |
| |
| // Before the throttle will be notified that the content is ready, complete |
| // all checks but from the back. |
| for (auto it = checks.rbegin(); it != checks.rend(); ++it) { |
| std::move(*it).Run({GURL(kExampleURL), FilteringBehavior::kAllow, |
| FilteringBehaviorReason::ASYNC_CHECKER}); |
| // Classification still not complete. |
| histogram_tester()->ExpectTotalCount( |
| kClassifiedEarlierThanContentResponseHistogramName, |
| /*expected_count=*/0); |
| } |
| |
| // Throttle is not blocked |
| EXPECT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillProcessResponse()); |
| |
| // As a result, the navigation hadn't had to be resumed |
| EXPECT_FALSE(resume_called()); |
| |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kAllow, 3); |
| // Since the throttle had to wait for checks to complete, it recorded a |
| // corresponding metric. |
| histogram_tester()->ExpectTotalCount( |
| kClassifiedEarlierThanContentResponseHistogramName, |
| /*expected_count=*/1); |
| } |
| |
| TEST_P(ClassifyUrlNavigationThrottleParallelizationTest, |
| ClassificationIsSlowerThanHttp) { |
| std::vector<MockSupervisedUserURLFilter::ResultCallback> checks; |
| ON_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .WillByDefault( |
| [&checks](const GURL& url, |
| MockSupervisedUserURLFilter::ResultCallback callback) { |
| checks.push_back(std::move(callback)); |
| // Asynchronous behavior all the time. |
| return false; |
| }); |
| EXPECT_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .Times(3); |
| |
| // This navigation is a 3-piece redirect chain on the same URL: |
| std::unique_ptr<content::MockNavigationThrottleRegistry> registry = |
| CreateNavigationThrottle(GetRedirectChain()); |
| |
| // It will allow request and two redirects to pass... |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillStartRequest()); |
| AdvanceRedirect(); |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillRedirectRequest()); |
| AdvanceRedirect(); |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillRedirectRequest()); |
| |
| // At this point, no check was completed. |
| EXPECT_THAT(checks, testing::SizeIs(3)); |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kAllow, 0); |
| |
| // Complete two last checks |
| std::move(checks[1]).Run({GURL(kExampleURL), FilteringBehavior::kAllow, |
| FilteringBehaviorReason::ASYNC_CHECKER}); |
| std::move(checks[2]).Run({GURL(kExampleURL), FilteringBehavior::kAllow, |
| FilteringBehaviorReason::ASYNC_CHECKER}); |
| |
| // Now two out of three checks are complete |
| EXPECT_THAT(checks, testing::SizeIs(3)); |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kAllow, 2); |
| |
| // But will block at process response because one check is still |
| // pending and no filtering was completed. |
| EXPECT_EQ(content::NavigationThrottle::DEFER, |
| registry->throttles().back()->WillProcessResponse()); |
| |
| // Now complete the outstanding check |
| std::move(checks[0]).Run({GURL(kExampleURL), FilteringBehavior::kAllow, |
| FilteringBehaviorReason::ASYNC_CHECKER}); |
| |
| // As a result, the navigation is resumed (and three checks registered) |
| EXPECT_TRUE(resume_called()); |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kAllow, 3); |
| // Since the throttle had to wait for checks to complete, it recorded a |
| // corresponding metric. |
| histogram_tester()->ExpectTotalCount( |
| kClassifiedLaterThanContentResponseHistogramName, |
| /*expected_count=*/1); |
| } |
| |
| TEST_P(ClassifyUrlNavigationThrottleParallelizationTest, |
| ShortCircuitsSynchronousBlock) { |
| bool first_check = false; |
| ON_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .WillByDefault( |
| [&first_check](const GURL& url, |
| MockSupervisedUserURLFilter::ResultCallback callback) { |
| if (!first_check) { |
| std::move(callback).Run({url, FilteringBehavior::kAllow, |
| FilteringBehaviorReason::ASYNC_CHECKER}); |
| first_check = true; |
| return true; |
| } |
| |
| // Subsequent checks are synchronous blocks. |
| std::move(callback).Run({url, FilteringBehavior::kBlock, |
| FilteringBehaviorReason::ASYNC_CHECKER}); |
| return true; |
| }); |
| EXPECT_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .Times(2); |
| |
| // This navigation is a 3-piece redirect chain on the same URL: |
| std::unique_ptr<content::MockNavigationThrottleRegistry> registry = |
| CreateNavigationThrottle(GetRedirectChain()); |
| |
| // It will DEFER at 2nd request (1st redirect). |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillStartRequest()); |
| AdvanceRedirect(); |
| ASSERT_EQ(content::NavigationThrottle::DEFER, |
| registry->throttles().back()->WillRedirectRequest()); |
| |
| // And one completed block from safe-sites (async checker) |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kBlockSafeSites, 1); |
| |
| // As a result, the navigation is not resumed |
| EXPECT_FALSE(resume_called()); |
| // Since this is not a success path, no latency metric is recorded. |
| ExpectNoLatencyRecorded(histogram_tester()); |
| } |
| |
| TEST_P(ClassifyUrlNavigationThrottleParallelizationTest, |
| HandlesLateAsynchronousBlock) { |
| std::vector<MockSupervisedUserURLFilter::ResultCallback> checks; |
| bool first_check_completed = false; |
| ON_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .WillByDefault([&checks, &first_check_completed]( |
| const GURL& url, |
| MockSupervisedUserURLFilter::ResultCallback callback) { |
| // First check is synchronous allow |
| if (!first_check_completed) { |
| first_check_completed = true; |
| std::move(callback).Run({url, FilteringBehavior::kAllow, |
| FilteringBehaviorReason::ASYNC_CHECKER}); |
| return true; |
| } |
| // Subsequent checks are asynchronous |
| checks.push_back(std::move(callback)); |
| return false; |
| }); |
| |
| EXPECT_CALL(*GetSupervisedUserURLFilter(), |
| RunAsyncChecker(testing::_, testing::_)) |
| .Times(3); |
| |
| // This navigation is a 3-piece redirect chain on the same URL: |
| std::unique_ptr<content::MockNavigationThrottleRegistry> registry = |
| CreateNavigationThrottle(GetRedirectChain()); |
| |
| // It proceed all three request/redirects. |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillStartRequest()); |
| AdvanceRedirect(); |
| |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillRedirectRequest()); |
| AdvanceRedirect(); |
| |
| ASSERT_EQ(content::NavigationThrottle::PROCEED, |
| registry->throttles().back()->WillRedirectRequest()); |
| |
| // There will be two pending checks (first was synchronous) |
| EXPECT_THAT(checks, testing::SizeIs(2)); |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kAllow, 1); |
| |
| // Http server completes first |
| EXPECT_EQ(content::NavigationThrottle::DEFER, |
| registry->throttles().back()->WillProcessResponse()); |
| |
| // Complete first pending check |
| std::move(checks.front()) |
| .Run({GURL(kExampleURL), FilteringBehavior::kBlock, |
| FilteringBehaviorReason::ASYNC_CHECKER}); |
| |
| // Now two out of three checks are complete |
| EXPECT_THAT(checks, testing::SizeIs(2)); |
| histogram_tester()->ExpectBucketCount( |
| kSupervisedUserTopLevelURLFilteringResultHistogramName, |
| SupervisedUserFilterTopLevelResult::kBlockSafeSites, 1); |
| |
| // As a result, the navigation is not resumed |
| EXPECT_FALSE(resume_called()); |
| // Since this is not a success path, no latency metric is recorded. |
| ExpectNoLatencyRecorded(histogram_tester()); |
| } |
| |
| const TestCase kTestCases[] = { |
| {.name = "TwoRedirects", |
| .redirect_chain = {kExampleURL, kExample1URL, kExample2URL}}, |
| {.name = "TwoIdenticalRedirects", |
| .redirect_chain = {kExampleURL, kExampleURL, kExampleURL}}}; |
| |
| INSTANTIATE_TEST_SUITE_P(, |
| ClassifyUrlNavigationThrottleParallelizationTest, |
| testing::ValuesIn(kTestCases), |
| [](const testing::TestParamInfo<TestCase>& info) { |
| return info.param.name; |
| }); |
| |
| } // namespace |
| } // namespace supervised_user |