chromium/ios/components/security_interstitials/https_only_mode/https_only_mode_blocking_page_unittest.mm

// Copyright 2022 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/components/security_interstitials/https_only_mode/https_only_mode_blocking_page.h"

#import <set>

#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/security_interstitials/core/metrics_helper.h"
#import "ios/components/security_interstitials/https_only_mode/https_only_mode_controller_client.h"
#import "ios/components/security_interstitials/https_only_mode/https_upgrade_service.h"
#import "ios/components/security_interstitials/https_only_mode/https_upgrade_test_util.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 security_interstitials::IOSSecurityInterstitialPage;
using security_interstitials::SecurityInterstitialCommand;
using security_interstitials::MetricsHelper;
using base::test::ios::WaitUntilConditionOrTimeout;
using base::test::ios::kSpinDelaySeconds;

namespace {

// Constants used for testing metrics.
const char kInterstitialDecisionMetric[] =
    "interstitial.https_only_mode.decision";
const char kInterstitialInteractionMetric[] =
    "interstitial.https_only_mode.interaction";

// Creates a HttpsOnlyModeBlockingPage with a given `request_url`.
std::unique_ptr<HttpsOnlyModeBlockingPage> CreateBlockingPage(
    web::WebState* web_state,
    const GURL& request_url,
    HttpsUpgradeService* service) {
  return std::make_unique<HttpsOnlyModeBlockingPage>(
      web_state, request_url, service,
      std::make_unique<HttpsOnlyModeControllerClient>(web_state, request_url,
                                                      "en-US"));
}

// A fake web state that sets the visible URL to the last opened URL.
class FakeWebState : public web::FakeWebState {
 public:
  void OpenURL(const web::WebState::OpenURLParams& params) override {
    SetVisibleURL(params.url);
  }
};

}  // namespace

// Test fixture for HttpsOnlyModeBlockingPage.
class HttpsOnlyModeBlockingPageTest : public PlatformTest {
 public:
  HttpsOnlyModeBlockingPageTest() : url_("http://www.chromium.test") {
    auto navigation_manager = std::make_unique<web::FakeNavigationManager>();
    navigation_manager_ = navigation_manager.get();
    web_state_.SetNavigationManager(std::move(navigation_manager));
    service_ = std::make_unique<FakeHttpsUpgradeService>();
  }

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

  HttpsUpgradeService* service() { return service_.get(); }

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

// Tests that the blocking page handles the proceed command by updating the
// allow list and reloading the page.
TEST_F(HttpsOnlyModeBlockingPageTest, HandleProceedCommand) {
  page_ = CreateBlockingPage(&web_state_, url_, service());
  ASSERT_FALSE(service()->IsHttpAllowedForHost(url_.host()));
  ASSERT_FALSE(navigation_manager_->ReloadWasCalled());

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

  EXPECT_TRUE(service()->IsHttpAllowedForHost(url_.host()));
  EXPECT_TRUE(navigation_manager_->ReloadWasCalled());

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

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

  page_ = CreateBlockingPage(&web_state_, url_, service());

  // 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(kInterstitialDecisionMetric, 2);
  histogram_tester_.ExpectBucketCount(kInterstitialDecisionMetric,
                                      MetricsHelper::DONT_PROCEED, 1);
  histogram_tester_.ExpectBucketCount(kInterstitialDecisionMetric,
                                      MetricsHelper::SHOW, 1);
  histogram_tester_.ExpectTotalCount(kInterstitialInteractionMetric, 1);
  histogram_tester_.ExpectBucketCount(kInterstitialInteractionMetric,
                                      MetricsHelper::TOTAL_VISITS, 1);
}

// Tests that the blocking page handles the don't proceed command by closing the
// WebState if there is no safe NavigationItem to navigate to and unable to go
// back.
TEST_F(HttpsOnlyModeBlockingPageTest,
       HandleDontProceedCommandWithoutSafeUrlClose) {
  page_ = CreateBlockingPage(&web_state_, url_, service());
  ASSERT_FALSE(navigation_manager_->CanGoBack());

  // 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();
  }));

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