chromium/ios/chrome/browser/safe_browsing/model/safe_browsing_blocking_page_unittest.mm

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/safe_browsing/model/safe_browsing_blocking_page.h"

#import "base/containers/contains.h"
#import "base/memory/raw_ptr.h"
#import "base/strings/string_number_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/values.h"
#import "components/safe_browsing/ios/browser/safe_browsing_url_allow_list.h"
#import "components/security_interstitials/core/metrics_helper.h"
#import "components/security_interstitials/core/unsafe_resource.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/test/fakes/fake_navigation_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/platform_test.h"

using base::test::ios::kSpinDelaySeconds;
using base::test::ios::WaitUntilConditionOrTimeout;
using safe_browsing::SBThreatType;
using security_interstitials::IOSSecurityInterstitialPage;
using security_interstitials::MetricsHelper;
using security_interstitials::SecurityInterstitialCommand;
using security_interstitials::UnsafeResource;

namespace {
// Name of the metric recorded when a malware interstitial is shown or closed.
const char kMalwareDecisionMetric[] = "interstitial.malware.decision";

// Creates an UnsafeResource for `web_state` using `url`.
UnsafeResource CreateResource(web::WebState* web_state, const GURL& url) {
  UnsafeResource resource;
  resource.url = url;
  resource.threat_type =
      safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_MALWARE;
  resource.weak_web_state = web_state->GetWeakPtr();
  resource.threat_source = safe_browsing::ThreatSource::LOCAL_PVER4;
  return resource;
}
}  // namespace

// Test fixture for SafeBrowsingBlockingPage.
class SafeBrowsingBlockingPageTest : public PlatformTest {
 public:
  SafeBrowsingBlockingPageTest()
      : browser_state_(TestChromeBrowserState::Builder().Build()),
        url_("http://www.chromium.test"),
        resource_(CreateResource(&web_state_, url_)) {
    auto navigation_manager = std::make_unique<web::FakeNavigationManager>();
    navigation_manager->SetBrowserState(browser_state_.get());
    navigation_manager_ = navigation_manager.get();
    web_state_.SetNavigationManager(std::move(navigation_manager));
    web_state_.SetBrowserState(browser_state_.get());
    page_ = SafeBrowsingBlockingPage::Create(resource_);
    SafeBrowsingUrlAllowList::CreateForWebState(&web_state_);
    SafeBrowsingUrlAllowList::FromWebState(&web_state_)
        ->AddPendingUnsafeNavigationDecision(url_, resource_.threat_type);
  }

  void SendCommand(SecurityInterstitialCommand command) {
    page_->HandleCommand(command);
  }

 protected:
  web::WebTaskEnvironment task_environment_{
      web::WebTaskEnvironment::MainThreadType::IO};
  std::unique_ptr<ChromeBrowserState> browser_state_;
  web::FakeWebState web_state_;
  raw_ptr<web::FakeNavigationManager> navigation_manager_ = nullptr;
  GURL url_;
  UnsafeResource resource_;
  std::unique_ptr<IOSSecurityInterstitialPage> page_;
  base::HistogramTester histogram_tester_;
};

// Tests that the blocking page generates HTML.
TEST_F(SafeBrowsingBlockingPageTest, GenerateHTML) {
  EXPECT_GT(page_->GetHtmlContents().size(), 0U);
}

// Tests that the blocking page handles the proceed command by updating the
// allow list and reloading the page.
TEST_F(SafeBrowsingBlockingPageTest, HandleProceedCommand) {
  SafeBrowsingUrlAllowList* allow_list =
      SafeBrowsingUrlAllowList::FromWebState(&web_state_);
  ASSERT_FALSE(allow_list->AreUnsafeNavigationsAllowed(url_));
  ASSERT_FALSE(navigation_manager_->ReloadWasCalled());

  // Send the proceed command.
  SendCommand(security_interstitials::CMD_PROCEED);

  std::set<SBThreatType> allowed_threats;
  EXPECT_TRUE(allow_list->AreUnsafeNavigationsAllowed(url_, &allowed_threats));
  EXPECT_TRUE(base::Contains(allowed_threats, resource_.threat_type));
  EXPECT_TRUE(navigation_manager_->ReloadWasCalled());

  // Verify that metrics are recorded correctly.
  histogram_tester_.ExpectTotalCount(kMalwareDecisionMetric, 2);
  histogram_tester_.ExpectBucketCount(kMalwareDecisionMetric,
                                      MetricsHelper::PROCEED, 1);
  histogram_tester_.ExpectBucketCount(kMalwareDecisionMetric,
                                      MetricsHelper::SHOW, 1);
}

// Tests that the blocking page handles the don't proceed command by navigating
// back.
TEST_F(SafeBrowsingBlockingPageTest, HandleDontProceedCommand) {
  // Insert a safe navigation so that the page can navigate back to safety, then
  // add a navigation for the committed interstitial page.
  GURL kSafeUrl("http://www.safe.test");
  navigation_manager_->AddItem(kSafeUrl, ui::PAGE_TRANSITION_TYPED);
  navigation_manager_->AddItem(url_, ui::PAGE_TRANSITION_LINK);
  ASSERT_EQ(1, navigation_manager_->GetLastCommittedItemIndex());
  ASSERT_TRUE(navigation_manager_->CanGoBack());

  // Send the don't proceed command.
  SendCommand(security_interstitials::CMD_DONT_PROCEED);

  // Verify that the NavigationManager has navigated back.
  EXPECT_EQ(0, navigation_manager_->GetLastCommittedItemIndex());
  EXPECT_FALSE(navigation_manager_->CanGoBack());

  // Verify that metrics are recorded correctly.
  histogram_tester_.ExpectTotalCount(kMalwareDecisionMetric, 2);
  histogram_tester_.ExpectBucketCount(kMalwareDecisionMetric,
                                      MetricsHelper::DONT_PROCEED, 1);
  histogram_tester_.ExpectBucketCount(kMalwareDecisionMetric,
                                      MetricsHelper::SHOW, 1);
}

// Tests that the blocking page handles the don't proceed command by closing the
// WebState if there is no safe NavigationItem to navigate back to.
TEST_F(SafeBrowsingBlockingPageTest, HandleDontProceedCommandWithoutSafeItem) {
  // Send the don't proceed command.
  SendCommand(security_interstitials::CMD_DONT_PROCEED);

  // Wait for the WebState to be closed.  The close command run asynchronously
  // on the UI thread, so the runloop needs to be spun before it is handled.
  task_environment_.RunUntilIdle();
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kSpinDelaySeconds, ^{
    return web_state_.IsClosed();
  }));
}

// Tests that the blocking page removes pending allow list decisions if
// destroyed.
TEST_F(SafeBrowsingBlockingPageTest, RemovePendingDecisionsUponDestruction) {
  SafeBrowsingUrlAllowList* allow_list =
      SafeBrowsingUrlAllowList::FromWebState(&web_state_);
  std::set<safe_browsing::SBThreatType> pending_threats;
  ASSERT_TRUE(
      allow_list->IsUnsafeNavigationDecisionPending(url_, &pending_threats));
  ASSERT_EQ(1U, pending_threats.size());
  ASSERT_TRUE(base::Contains(pending_threats, resource_.threat_type));

  page_ = nullptr;

  EXPECT_FALSE(allow_list->IsUnsafeNavigationDecisionPending(url_));
}