| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/safe_browsing/content/browser/async_check_tracker.h" |
| |
| #include "base/functional/callback_helpers.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/mock_callback.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "build/build_config.h" |
| #include "components/safe_browsing/content/browser/base_ui_manager.h" |
| #include "components/safe_browsing/content/browser/url_checker_holder.h" |
| #include "components/safe_browsing/core/browser/db/v4_protocol_manager_util.h" |
| #include "components/safe_browsing/core/browser/safe_browsing_url_checker_impl.h" |
| #include "components/safe_browsing/core/common/features.h" |
| #include "components/security_interstitials/core/unsafe_resource.h" |
| #include "components/security_interstitials/core/unsafe_resource_locator.h" |
| #include "content/public/test/mock_navigation_handle.h" |
| #include "content/public/test/test_renderer_host.h" |
| |
| using security_interstitials::UnsafeResourceLocator; |
| |
| namespace safe_browsing { |
| |
| namespace { |
| |
| using security_interstitials::UnsafeResource; |
| |
| class MockUIManager : public BaseUIManager { |
| public: |
| MockUIManager() : BaseUIManager() {} |
| |
| MockUIManager(const MockUIManager&) = delete; |
| MockUIManager& operator=(const MockUIManager&) = delete; |
| |
| void DisplayBlockingPage(const UnsafeResource& resource) override { |
| display_blocking_page_called_times_++; |
| displayed_resource_ = resource; |
| } |
| |
| int DisplayBlockingPageCalledTimes() { |
| return display_blocking_page_called_times_; |
| } |
| |
| UnsafeResource GetDisplayedResource() { return displayed_resource_; } |
| |
| protected: |
| ~MockUIManager() override = default; |
| |
| private: |
| int display_blocking_page_called_times_ = 0; |
| UnsafeResource displayed_resource_; |
| }; |
| |
| constexpr int kLocalNavigationTimestampsSizeThreshold = 5; |
| |
| } // namespace |
| |
| class AsyncCheckTrackerTest : public content::RenderViewHostTestHarness { |
| protected: |
| AsyncCheckTrackerTest() |
| : RenderViewHostTestHarness( |
| content::BrowserTaskEnvironment::REAL_IO_THREAD, |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME) { |
| std::vector<base::test::FeatureRef> enabled = {}; |
| #if BUILDFLAG(IS_ANDROID) |
| enabled.push_back(kSafeBrowsingSyncCheckerCheckAllowlist); |
| #endif |
| feature_list_.InitWithFeatures(enabled, {}); |
| } |
| |
| void SetUp() override { |
| content::RenderViewHostTestHarness::SetUp(); |
| |
| url_ = GURL("https://example.com/"); |
| EXPECT_CALL(mock_web_contents_getter_, Run()) |
| .WillRepeatedly(testing::Return(nullptr)); |
| ui_manager_ = base::MakeRefCounted<MockUIManager>(); |
| tracker_ = AsyncCheckTracker::GetOrCreateForWebContents( |
| web_contents(), ui_manager_.get(), |
| /*should_sync_checker_check_allowlist=*/false); |
| } |
| |
| void TearDown() override { |
| tracker_ = nullptr; |
| content::RenderViewHostTestHarness::TearDown(); |
| } |
| |
| void CallDidFinishNavigation(content::MockNavigationHandle& handle, |
| bool has_committed) { |
| handle.set_has_committed(has_committed); |
| tracker_->DidFinishNavigation(&handle); |
| task_environment()->RunUntilIdle(); |
| } |
| |
| void CallPendingCheckerCompleted(int64_t navigation_id, |
| bool proceed, |
| bool has_post_commit_interstitial_skipped, |
| bool all_checks_completed) { |
| if (!proceed) { |
| // This mocks how BaseUIManager caches unsafe resource if |
| // load_post_commit_error_page is false. |
| UnsafeResource resource; |
| resource.url = url_; |
| resource.threat_type = SBThreatType::SB_THREAT_TYPE_URL_PHISHING; |
| resource.navigation_id = navigation_id; |
| ui_manager_->AddUnsafeResource(url_, resource); |
| } |
| UrlCheckerHolder::OnCompleteCheckResult result( |
| proceed, /*showed_interstitial=*/true, |
| has_post_commit_interstitial_skipped, |
| SafeBrowsingUrlCheckerImpl::PerformedCheck::kUrlRealTimeCheck, |
| all_checks_completed); |
| tracker_->PendingCheckerCompleted(navigation_id, result); |
| task_environment()->RunUntilIdle(); |
| } |
| |
| void CallTransferUrlChecker(int64_t navigation_id) { |
| auto checker = std::make_unique<UrlCheckerHolder>( |
| /*delegate_getter=*/base::NullCallback(), content::FrameTreeNodeId(), |
| navigation_id, mock_web_contents_getter_.Get(), |
| /*complete_callback=*/base::NullCallback(), |
| /*url_real_time_lookup_enabled=*/false, |
| /*can_check_db=*/true, |
| /*can_check_high_confidence_allowlist=*/true, |
| /*url_lookup_service_metric_suffix=*/"", |
| /*url_lookup_service=*/nullptr, |
| /*hash_realtime_service=*/nullptr, |
| /*hash_realtime_selection=*/ |
| hash_realtime_utils::HashRealTimeSelection::kNone, |
| /*is_async_check=*/true, /*check_allowlist_before_hash_database=*/false, |
| SessionID::InvalidValue(), /*referring_app_info=*/std::nullopt); |
| checker->AddUrlInRedirectChainForTesting(url_); |
| tracker_->TransferUrlChecker(std::move(checker)); |
| } |
| |
| base::test::ScopedFeatureList feature_list_; |
| GURL url_; |
| base::MockCallback<base::RepeatingCallback<content::WebContents*()>> |
| mock_web_contents_getter_; |
| scoped_refptr<MockUIManager> ui_manager_; |
| raw_ptr<AsyncCheckTracker> tracker_; |
| }; |
| |
| TEST_F(AsyncCheckTrackerTest, |
| DisplayBlockingPageNotCalled_PendingCheckNotFound) { |
| content::MockNavigationHandle handle(url_, main_rfh()); |
| // This can happen when the complete callback is scheduled before the checker |
| // is scheduled to be deleted on the UI thread. Mock this scenario by not |
| // calling CallTransferUrlChecker. |
| CallPendingCheckerCompleted(handle.GetNavigationId(), /*proceed=*/false, |
| /*has_post_commit_interstitial_skipped=*/true, |
| /*all_checks_completed=*/true); |
| CallDidFinishNavigation(handle, /*has_committed=*/true); |
| EXPECT_EQ(ui_manager_->DisplayBlockingPageCalledTimes(), 0); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, |
| DisplayBlockingPageNotCalled_PendingCheckNotCompleted) { |
| content::MockNavigationHandle handle(url_, main_rfh()); |
| CallTransferUrlChecker(handle.GetNavigationId()); |
| CallDidFinishNavigation(handle, /*has_committed=*/true); |
| EXPECT_EQ(ui_manager_->DisplayBlockingPageCalledTimes(), 0); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, |
| DisplayBlockingPageNotCalled_PendingCheckProceed) { |
| base::HistogramTester histograms; |
| content::MockNavigationHandle handle(url_, main_rfh()); |
| CallTransferUrlChecker(handle.GetNavigationId()); |
| CallPendingCheckerCompleted(handle.GetNavigationId(), /*proceed=*/true, |
| /*has_post_commit_interstitial_skipped=*/false, |
| /*all_checks_completed=*/true); |
| CallDidFinishNavigation(handle, /*has_committed=*/true); |
| EXPECT_EQ(ui_manager_->DisplayBlockingPageCalledTimes(), 0); |
| |
| histograms.ExpectTotalCount( |
| "SafeBrowsing.AsyncCheck.HasPostCommitInterstitialSkipped", |
| /*expected_count=*/0); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, |
| DisplayBlockingPageNotCalled_PostCommitInterstitialNotSkipped) { |
| base::HistogramTester histograms; |
| content::MockNavigationHandle handle(url_, main_rfh()); |
| CallTransferUrlChecker(handle.GetNavigationId()); |
| CallPendingCheckerCompleted(handle.GetNavigationId(), /*proceed=*/false, |
| /*has_post_commit_interstitial_skipped=*/false, |
| /*all_checks_completed=*/true); |
| CallDidFinishNavigation(handle, /*has_committed=*/true); |
| EXPECT_EQ(ui_manager_->DisplayBlockingPageCalledTimes(), 0); |
| |
| histograms.ExpectUniqueSample( |
| "SafeBrowsing.AsyncCheck.HasPostCommitInterstitialSkipped", |
| /*sample=*/false, |
| /*expected_bucket_count=*/1); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, |
| DisplayBlockingPageNotCalled_NavigationNotCommitted) { |
| content::MockNavigationHandle handle(url_, main_rfh()); |
| CallTransferUrlChecker(handle.GetNavigationId()); |
| CallPendingCheckerCompleted(handle.GetNavigationId(), /*proceed=*/false, |
| /*has_post_commit_interstitial_skipped=*/true, |
| /*all_checks_completed=*/true); |
| CallDidFinishNavigation(handle, /*has_committed=*/false); |
| EXPECT_EQ(ui_manager_->DisplayBlockingPageCalledTimes(), 0); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, DisplayBlockingPageCalled) { |
| base::HistogramTester histograms; |
| content::MockNavigationHandle handle(url_, main_rfh()); |
| CallTransferUrlChecker(handle.GetNavigationId()); |
| CallPendingCheckerCompleted(handle.GetNavigationId(), /*proceed=*/false, |
| /*has_post_commit_interstitial_skipped=*/true, |
| /*all_checks_completed=*/true); |
| CallDidFinishNavigation(handle, /*has_committed=*/true); |
| EXPECT_EQ(ui_manager_->DisplayBlockingPageCalledTimes(), 1); |
| UnsafeResource resource = ui_manager_->GetDisplayedResource(); |
| EXPECT_EQ(resource.threat_type, SBThreatType::SB_THREAT_TYPE_URL_PHISHING); |
| EXPECT_EQ(resource.url, url_); |
| EXPECT_EQ(resource.rfh_locator.render_process_id, |
| main_rfh()->GetGlobalId().child_id); |
| EXPECT_EQ(resource.rfh_locator.render_frame_token, |
| main_rfh()->GetFrameToken().value()); |
| |
| histograms.ExpectUniqueSample( |
| "SafeBrowsing.AsyncCheck.HasPostCommitInterstitialSkipped", |
| /*sample=*/true, |
| /*expected_bucket_count=*/1); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, |
| DisplayBlockingPageCalled_DidFinishNavigationCalledFirst) { |
| content::MockNavigationHandle handle(url_, main_rfh()); |
| CallTransferUrlChecker(handle.GetNavigationId()); |
| CallDidFinishNavigation(handle, /*has_committed=*/true); |
| // Usually has_post_commit_interstitial_skipped is false if |
| // DidFinishNavigation is already called. It can be true if |
| // DidFinishNavigation happens to be called between when |
| // PendingCheckerCompleted is scheduled and when it is run. |
| CallPendingCheckerCompleted(handle.GetNavigationId(), /*proceed=*/false, |
| /*has_post_commit_interstitial_skipped=*/true, |
| /*all_checks_completed=*/true); |
| EXPECT_EQ(ui_manager_->DisplayBlockingPageCalledTimes(), 1); |
| UnsafeResource resource = ui_manager_->GetDisplayedResource(); |
| EXPECT_EQ(resource.threat_type, SBThreatType::SB_THREAT_TYPE_URL_PHISHING); |
| EXPECT_EQ(resource.url, url_); |
| EXPECT_EQ(resource.rfh_locator.render_process_id, |
| main_rfh()->GetGlobalId().child_id); |
| EXPECT_EQ(resource.rfh_locator.render_frame_token, |
| main_rfh()->GetFrameToken().value()); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, IsMainPageLoadPending) { |
| base::HistogramTester histograms; |
| content::MockNavigationHandle handle(web_contents()); |
| auto rfh_locator = UnsafeResourceLocator::CreateForFrameTreeNodeId( |
| main_rfh()->GetFrameTreeNodeId().value()); |
| |
| AsyncCheckTracker* tracker = |
| AsyncCheckTracker::FromWebContents(web_contents()); |
| EXPECT_TRUE(AsyncCheckTracker::IsMainPageLoadPending( |
| rfh_locator, handle.GetNavigationId(), |
| SBThreatType::SB_THREAT_TYPE_URL_PHISHING)); |
| |
| tracker->DidFinishNavigation(&handle); |
| // The navigation is not committed. |
| EXPECT_TRUE(AsyncCheckTracker::IsMainPageLoadPending( |
| rfh_locator, handle.GetNavigationId(), |
| SBThreatType::SB_THREAT_TYPE_URL_PHISHING)); |
| histograms.ExpectUniqueSample( |
| "SafeBrowsing.AsyncCheck.CommittedNavigationIdsSize", |
| /*sample=*/0, |
| /*expected_count=*/1); |
| |
| handle.set_has_committed(true); |
| tracker->DidFinishNavigation(&handle); |
| EXPECT_FALSE(AsyncCheckTracker::IsMainPageLoadPending( |
| rfh_locator, handle.GetNavigationId(), |
| SBThreatType::SB_THREAT_TYPE_URL_PHISHING)); |
| histograms.ExpectBucketCount( |
| "SafeBrowsing.AsyncCheck.CommittedNavigationIdsSize", |
| /*sample=*/1, |
| /*expected_count=*/1); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, IsMainPageLoadPending_NoNavigationId) { |
| content::MockNavigationHandle handle(web_contents()); |
| auto rfh_locator = UnsafeResourceLocator::CreateForFrameTreeNodeId( |
| main_rfh()->GetFrameTreeNodeId().value()); |
| EXPECT_TRUE(AsyncCheckTracker::IsMainPageLoadPending( |
| rfh_locator, /*navigation_id=*/std::nullopt, |
| SBThreatType::SB_THREAT_TYPE_URL_PHISHING)); |
| |
| // If there is no navigation id associated with the resource, whether the |
| // main page load is pending is determined by |
| // UnsafeResource::IsMainPageLoadPendingWithSyncCheck. |
| EXPECT_FALSE(AsyncCheckTracker::IsMainPageLoadPending( |
| rfh_locator, |
| /*navigation_id=*/std::nullopt, |
| SBThreatType::SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING)); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, |
| IsMainPageLoadPending_DeleteExpiredNavigationTimestamps) { |
| tracker_->SetNavigationTimestampsSizeThresholdForTesting( |
| kLocalNavigationTimestampsSizeThreshold); |
| auto rfh_locator = UnsafeResourceLocator::CreateForFrameTreeNodeId( |
| main_rfh()->GetFrameTreeNodeId().value()); |
| |
| std::vector<int64_t> old_navigation_ids; |
| for (int i = 0; i < kLocalNavigationTimestampsSizeThreshold; i++) { |
| content::MockNavigationHandle handle(url_, main_rfh()); |
| old_navigation_ids.push_back(handle.GetNavigationId()); |
| CallDidFinishNavigation(handle, /*has_committed=*/true); |
| } |
| for (int64_t id : old_navigation_ids) { |
| EXPECT_FALSE(AsyncCheckTracker::IsMainPageLoadPending( |
| rfh_locator, id, SBThreatType::SB_THREAT_TYPE_URL_PHISHING)); |
| } |
| |
| task_environment()->FastForwardBy(base::Seconds(180)); |
| content::MockNavigationHandle recent_handle1(url_, main_rfh()); |
| CallDidFinishNavigation(recent_handle1, /*has_committed=*/true); |
| for (int64_t id : old_navigation_ids) { |
| EXPECT_FALSE(AsyncCheckTracker::IsMainPageLoadPending( |
| rfh_locator, id, SBThreatType::SB_THREAT_TYPE_URL_PHISHING)); |
| } |
| |
| task_environment()->FastForwardBy(base::Seconds(1)); |
| content::MockNavigationHandle recent_handle2(url_, main_rfh()); |
| CallDidFinishNavigation(recent_handle2, /*has_committed=*/true); |
| // The old navigation timestamps have been cleaned up, so the function returns |
| // true. |
| for (int64_t id : old_navigation_ids) { |
| EXPECT_TRUE(AsyncCheckTracker::IsMainPageLoadPending( |
| rfh_locator, id, SBThreatType::SB_THREAT_TYPE_URL_PHISHING)); |
| } |
| |
| // The recent timestamps should not be cleaned up. |
| EXPECT_FALSE(AsyncCheckTracker::IsMainPageLoadPending( |
| rfh_locator, recent_handle1.GetNavigationId(), |
| SBThreatType::SB_THREAT_TYPE_URL_PHISHING)); |
| EXPECT_FALSE(AsyncCheckTracker::IsMainPageLoadPending( |
| rfh_locator, recent_handle2.GetNavigationId(), |
| SBThreatType::SB_THREAT_TYPE_URL_PHISHING)); |
| } |
| |
| TEST_F( |
| AsyncCheckTrackerTest, |
| IsMainPageLoadPending_DeleteExpiredNavigationTimestamps_NotReachingThreshold) { |
| tracker_->SetNavigationTimestampsSizeThresholdForTesting( |
| kLocalNavigationTimestampsSizeThreshold); |
| auto rfh_locator = UnsafeResourceLocator::CreateForFrameTreeNodeId( |
| main_rfh()->GetFrameTreeNodeId().value()); |
| |
| content::MockNavigationHandle handle(url_, main_rfh()); |
| CallDidFinishNavigation(handle, /*has_committed=*/true); |
| EXPECT_FALSE(AsyncCheckTracker::IsMainPageLoadPending( |
| rfh_locator, handle.GetNavigationId(), |
| SBThreatType::SB_THREAT_TYPE_URL_PHISHING)); |
| |
| task_environment()->FastForwardBy(base::Seconds(181)); |
| content::MockNavigationHandle recent_handle(url_, main_rfh()); |
| CallDidFinishNavigation(recent_handle, /*has_committed=*/true); |
| // The timestamp has expired but not cleaned up, because the size of the |
| // timestamps has not reached the threshold. |
| EXPECT_FALSE(AsyncCheckTracker::IsMainPageLoadPending( |
| rfh_locator, handle.GetNavigationId(), |
| SBThreatType::SB_THREAT_TYPE_URL_PHISHING)); |
| |
| for (int i = 0; i < kLocalNavigationTimestampsSizeThreshold - 1; i++) { |
| content::MockNavigationHandle new_handle(url_, main_rfh()); |
| CallDidFinishNavigation(new_handle, /*has_committed=*/true); |
| } |
| // The size of the timestamps has reached the threshold, so the old timestamp |
| // is cleaned up. |
| EXPECT_TRUE(AsyncCheckTracker::IsMainPageLoadPending( |
| rfh_locator, handle.GetNavigationId(), |
| SBThreatType::SB_THREAT_TYPE_URL_PHISHING)); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, IsPlatformEligibleForSyncCheckerCheckAllowlist) { |
| #if BUILDFLAG(IS_ANDROID) |
| EXPECT_TRUE( |
| AsyncCheckTracker::IsPlatformEligibleForSyncCheckerCheckAllowlist()); |
| #else |
| EXPECT_FALSE( |
| AsyncCheckTracker::IsPlatformEligibleForSyncCheckerCheckAllowlist()); |
| #endif |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, GetShouldSyncCheckerCheckAllowlist) { |
| std::unique_ptr<content::WebContents> web_contents = CreateTestWebContents(); |
| auto* tracker = AsyncCheckTracker::GetOrCreateForWebContents( |
| web_contents.get(), ui_manager_.get(), |
| /*should_sync_checker_check_allowlist=*/true); |
| EXPECT_TRUE(tracker->should_sync_checker_check_allowlist()); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, GetBlockedPageCommittedTimestamp) { |
| content::MockNavigationHandle handle(web_contents()); |
| UnsafeResource resource; |
| resource.threat_type = SBThreatType::SB_THREAT_TYPE_URL_PHISHING; |
| resource.rfh_locator = UnsafeResourceLocator::CreateForFrameTreeNodeId( |
| main_rfh()->GetFrameTreeNodeId().value()); |
| resource.navigation_id = handle.GetNavigationId(); |
| |
| AsyncCheckTracker* tracker = |
| AsyncCheckTracker::FromWebContents(web_contents()); |
| EXPECT_FALSE(AsyncCheckTracker::GetBlockedPageCommittedTimestamp(resource) |
| .has_value()); |
| |
| tracker->DidFinishNavigation(&handle); |
| // The navigation is not committed. |
| EXPECT_FALSE(AsyncCheckTracker::GetBlockedPageCommittedTimestamp(resource) |
| .has_value()); |
| |
| handle.set_has_committed(true); |
| tracker->DidFinishNavigation(&handle); |
| EXPECT_TRUE(AsyncCheckTracker::GetBlockedPageCommittedTimestamp(resource) |
| .has_value()); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, |
| PendingCheckersManagement_TransferWithSameNavigationId) { |
| EXPECT_EQ(tracker_->PendingCheckersSizeForTesting(), 0u); |
| CallTransferUrlChecker(/*navigation_id=*/1); |
| EXPECT_EQ(tracker_->PendingCheckersSizeForTesting(), 1u); |
| CallTransferUrlChecker(/*navigation_id=*/2); |
| EXPECT_EQ(tracker_->PendingCheckersSizeForTesting(), 2u); |
| // Transfer a checker with the same navigation id. This scenario can be |
| // triggered by HTTP client hints. |
| CallTransferUrlChecker(/*navigation_id=*/2); |
| // The previous checker should be deleted. The deletion should happen on the |
| // UI thread. |
| EXPECT_EQ(tracker_->PendingCheckersSizeForTesting(), 2u); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, |
| PendingCheckersManagement_DeleteOldCheckersAfterDidFinishNavigation) { |
| base::HistogramTester histograms; |
| content::MockNavigationHandle handle_1(url_, main_rfh()); |
| content::MockNavigationHandle handle_2(url_, main_rfh()); |
| content::MockNavigationHandle handle_3(url_, main_rfh()); |
| CallTransferUrlChecker(handle_1.GetNavigationId()); |
| histograms.ExpectUniqueSample("SafeBrowsing.AsyncCheck.PendingCheckersSize", |
| /*sample=*/1, |
| /*expected_count=*/1); |
| CallTransferUrlChecker(handle_2.GetNavigationId()); |
| histograms.ExpectBucketCount("SafeBrowsing.AsyncCheck.PendingCheckersSize", |
| /*sample=*/2, |
| /*expected_count=*/1); |
| CallTransferUrlChecker(handle_3.GetNavigationId()); |
| histograms.ExpectBucketCount("SafeBrowsing.AsyncCheck.PendingCheckersSize", |
| /*sample=*/3, |
| /*expected_count=*/1); |
| EXPECT_EQ(tracker_->PendingCheckersSizeForTesting(), 3u); |
| |
| // Only the third navigation is committed successfully. |
| CallDidFinishNavigation(handle_1, /*has_committed=*/false); |
| CallDidFinishNavigation(handle_2, /*has_committed=*/false); |
| CallDidFinishNavigation(handle_3, /*has_committed=*/true); |
| // Only keep the checker for the committed navigation. |
| EXPECT_EQ(tracker_->PendingCheckersSizeForTesting(), 1u); |
| |
| CallPendingCheckerCompleted(handle_3.GetNavigationId(), /*proceed=*/false, |
| /*has_post_commit_interstitial_skipped=*/true, |
| /*all_checks_completed=*/true); |
| // The remaining checker is deleted because proceed is false. |
| EXPECT_EQ(tracker_->PendingCheckersSizeForTesting(), 0u); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, |
| PendingCheckersManagement_CheckerNotDeletedIfAllChecksCompletedFalse) { |
| CallTransferUrlChecker(/*navigation_id=*/1); |
| EXPECT_EQ(tracker_->PendingCheckersSizeForTesting(), 1u); |
| |
| CallPendingCheckerCompleted(/*navigation_id=*/1, /*proceed=*/true, |
| /*has_post_commit_interstitial_skipped=*/false, |
| /*all_checks_completed=*/false); |
| // If all_checks_completed is false, the checker should be kept alive to |
| // receive upcoming result from the checker. This scenario can happen if there |
| // are server redirects. |
| EXPECT_EQ(tracker_->PendingCheckersSizeForTesting(), 1u); |
| |
| CallPendingCheckerCompleted(/*navigation_id=*/1, /*proceed=*/true, |
| /*has_post_commit_interstitial_skipped=*/false, |
| /*all_checks_completed=*/true); |
| EXPECT_EQ(tracker_->PendingCheckersSizeForTesting(), 0u); |
| } |
| |
| TEST_F(AsyncCheckTrackerTest, |
| PendingCheckersManagement_DestructWithPendingCheckers) { |
| CallTransferUrlChecker(/*navigation_id=*/1); |
| CallTransferUrlChecker(/*navigation_id=*/2); |
| EXPECT_EQ(tracker_->PendingCheckersSizeForTesting(), 2u); |
| |
| tracker_ = nullptr; |
| DeleteContents(); |
| // Tracker is deleted together with the WebContents. pending checkers that the |
| // tracker currently owns should also be deleted on the UI thread. |
| } |
| |
| class AsyncCheckTrackerTestObserver : public AsyncCheckTracker::Observer { |
| public: |
| void OnAsyncSafeBrowsingCheckCompleted() override { |
| async_check_completed_times_++; |
| } |
| void OnAsyncSafeBrowsingCheckTrackerDestructed() override { |
| tracker_destructed_times_++; |
| } |
| int AsyncCheckCompletedTimes() { return async_check_completed_times_; } |
| int TrackerDestructedTimes() { return tracker_destructed_times_; } |
| |
| private: |
| int async_check_completed_times_ = 0; |
| int tracker_destructed_times_ = 0; |
| }; |
| |
| class AsyncCheckTrackerObserverTest : public AsyncCheckTrackerTest { |
| protected: |
| AsyncCheckTrackerTestObserver observer_; |
| }; |
| |
| TEST_F(AsyncCheckTrackerObserverTest, OnAsyncSafeBrowsingCheckCompleted) { |
| tracker_->AddObserver(&observer_); |
| |
| CallTransferUrlChecker(/*navigation_id=*/1); |
| CallPendingCheckerCompleted(/*navigation_id=*/1, /*proceed=*/true, |
| /*has_post_commit_interstitial_skipped=*/false, |
| /*all_checks_completed=*/false); |
| // Observer not notified, because all_checks_completed is false. |
| EXPECT_EQ(observer_.AsyncCheckCompletedTimes(), 0); |
| |
| CallPendingCheckerCompleted(/*navigation_id=*/1, /*proceed=*/true, |
| /*has_post_commit_interstitial_skipped=*/false, |
| /*all_checks_completed=*/true); |
| EXPECT_EQ(observer_.AsyncCheckCompletedTimes(), 1); |
| CallTransferUrlChecker(/*navigation_id=*/2); |
| CallPendingCheckerCompleted(/*navigation_id=*/2, /*proceed=*/true, |
| /*has_post_commit_interstitial_skipped=*/false, |
| /*all_checks_completed=*/true); |
| EXPECT_EQ(observer_.AsyncCheckCompletedTimes(), 2); |
| |
| tracker_->RemoveObserver(&observer_); |
| |
| CallTransferUrlChecker(/*navigation_id=*/3); |
| CallPendingCheckerCompleted(/*navigation_id=*/3, /*proceed=*/true, |
| /*has_post_commit_interstitial_skipped=*/false, |
| /*all_checks_completed=*/true); |
| // Observer not notified, because it has removed itself. |
| EXPECT_EQ(observer_.AsyncCheckCompletedTimes(), 2); |
| } |
| |
| TEST_F(AsyncCheckTrackerObserverTest, AsyncCheckTrackerDeletedWhileObserving) { |
| std::unique_ptr<content::WebContents> web_contents = CreateTestWebContents(); |
| auto* tracker = AsyncCheckTracker::GetOrCreateForWebContents( |
| web_contents.get(), ui_manager_.get(), |
| /*should_sync_checker_check_allowlist=*/false); |
| tracker->AddObserver(&observer_); |
| EXPECT_TRUE(observer_.IsInObserverList()); |
| |
| web_contents.reset(); |
| // Ensure that the observer is auto removed after the tracker is deleted. |
| EXPECT_FALSE(observer_.IsInObserverList()); |
| EXPECT_EQ(observer_.TrackerDestructedTimes(), 1); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| class AsyncCheckTrackerSyncCheckerCheckAllowlistDisabledTest |
| : public AsyncCheckTrackerTest { |
| protected: |
| AsyncCheckTrackerSyncCheckerCheckAllowlistDisabledTest() |
| : AsyncCheckTrackerTest() { |
| feature_list_.InitAndDisableFeature(kSafeBrowsingSyncCheckerCheckAllowlist); |
| } |
| |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| TEST_F(AsyncCheckTrackerSyncCheckerCheckAllowlistDisabledTest, |
| IsPlatformEligibleForSyncCheckerCheckAllowlist) { |
| EXPECT_FALSE( |
| AsyncCheckTracker::IsPlatformEligibleForSyncCheckerCheckAllowlist()); |
| } |
| |
| #endif |
| |
| } // namespace safe_browsing |