chromium/ios/chrome/browser/https_upgrades/model/https_only_mode_egtest.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 <string>

#import "base/functional/bind.h"
#import "base/strings/escape.h"
#import "base/strings/string_util.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/metrics/histogram_tester.h"
#import "components/omnibox/common/omnibox_features.h"
#import "components/security_interstitials/core/https_only_mode_metrics.h"
#import "components/security_interstitials/core/omnibox_https_upgrade_metrics.h"
#import "ios/chrome/browser/https_upgrades/model/https_upgrade_app_interface.h"
#import "ios/chrome/browser/https_upgrades/model/https_upgrade_test_helper.h"
#import "ios/chrome/browser/metrics/model/metrics_app_interface.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey_ui.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/chrome/test/earl_grey/web_http_server_chrome_test_case.h"
#import "ios/components/security_interstitials/https_only_mode/feature.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/testing/embedded_test_server_handlers.h"
#import "ios/web/common/features.h"
#import "ios/web/public/test/element_selector.h"
#import "net/test/embedded_test_server/default_handlers.h"
#import "net/test/embedded_test_server/http_request.h"
#import "net/test/embedded_test_server/http_response.h"
#import "net/test/embedded_test_server/request_handler_util.h"
#import "ui/base/l10n/l10n_util.h"

using base::test::ios::kWaitForPageLoadTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;

namespace {

const char kInterstitialText[] =
    "You are seeing this warning because this site does not support HTTPS";

enum class TestType {
  kHttpsOnlyMode,
  kHttpsUpgrades,
};

}  // namespace

// Tests for HTTPS-Only Mode.
// TODO(crbug.com/40849153): Remove the "ZZZ" when the bug is fixed.
@interface ZZZ_HttpsOnlyModeTestCase : HttpsUpgradeTestCaseBase {
}
@end

@implementation ZZZ_HttpsOnlyModeTestCase

- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config;
  config.relaunch_policy = NoForceRelaunchAndResetState;
  config.features_enabled.push_back(
      security_interstitials::features::kHttpsOnlyMode);

  config.features_disabled.push_back(
      security_interstitials::features::kHttpsUpgrades);
  // Disable omnibox navigation upgrades.
  // typed_navigation_upgrade_tab_helper_egtest.mm already has a
  // test case with both features enabled.
  // (test_TypeHTTPWithGoodHTTPS_HTTPSOnlyModeEnabled_ShouldUpgrade)
  config.features_disabled.push_back(omnibox::kDefaultTypedNavigationsToHttps);
  return config;
}

- (void)setUp {
  [super setUp];
  [ChromeEarlGrey clearBrowsingHistory];
  [HttpsUpgradeAppInterface clearAllowlist];

  if ([self testType] == TestType::kHttpsOnlyMode) {
    [ChromeEarlGrey setBoolValue:YES forUserPref:prefs::kHttpsOnlyModeEnabled];
  }
}

- (void)tearDown {
  [ChromeEarlGrey setBoolValue:NO forUserPref:prefs::kHttpsOnlyModeEnabled];
  [HttpsUpgradeAppInterface clearAllowlist];

  [super tearDown];
}

// Returns true if the HTTPS-Only Mode interstitial is enabled.
- (bool)isInterstitialEnabled {
  return [self testType] == TestType::kHttpsOnlyMode &&
         [ChromeEarlGrey userBooleanPref:prefs::kHttpsOnlyModeEnabled];
}

- (TestType)testType {
  return TestType::kHttpsOnlyMode;
}

// Asserts that the navigation wasn't upgraded.
- (void)assertNoUpgrade {
  GREYAssertNil([MetricsAppInterface
                    expectTotalCount:0
                        forHistogram:@(security_interstitials::https_only_mode::
                                           kEventHistogram)],
                @"Shouldn't record event histogram");
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"Timer is still running");
  GREYAssert(![HttpsUpgradeAppInterface isOmniboxUpgradeTimerRunning],
             @"Omnibox upgrade timer is unexpectedly running");

  // Omnibox HTTPS Upgrades shouldn't handle this navigation.
  GREYAssertNil(
      [MetricsAppInterface
          expectTotalCount:0
              forHistogram:@(security_interstitials::omnibox_https_upgrades::
                                 kEventHistogram)],
      @"Omnibox HTTPS Upgrades unexpectedly recorded a histogram event");
}

// Asserts that the metrics are properly recorded for a successful upgrade.
- (void)assertSuccessfulUpgrade {
  GREYAssertNil([MetricsAppInterface
                    expectTotalCount:2
                        forHistogram:@(security_interstitials::https_only_mode::
                                           kEventHistogram)],
                @"Failed to record event histogram");

  GREYAssertNil([MetricsAppInterface
                     expectCount:1
                       forBucket:static_cast<int>(
                                     security_interstitials::https_only_mode::
                                         Event::kUpgradeAttempted)
                    forHistogram:@(security_interstitials::https_only_mode::
                                       kEventHistogram)],
                @"Failed to record upgrade attempt");
  GREYAssertNil([MetricsAppInterface
                     expectCount:1
                       forBucket:static_cast<int>(
                                     security_interstitials::https_only_mode::
                                         Event::kUpgradeSucceeded)
                    forHistogram:@(security_interstitials::https_only_mode::
                                       kEventHistogram)],
                @"Failed to record upgrade attempt");

  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"Timer is still running");
  GREYAssert(![HttpsUpgradeAppInterface isOmniboxUpgradeTimerRunning],
             @"Omnibox upgrade timer is unexpectedly running");

  // Omnibox HTTPS Upgrades shouldn't handle this navigation.
  GREYAssertNil(
      [MetricsAppInterface
          expectTotalCount:0
              forHistogram:@(security_interstitials::omnibox_https_upgrades::
                                 kEventHistogram)],
      @"Omnibox HTTPS Upgrades unexpectedly recorded a histogram event");
}

// Asserts that the metrics are properly recorded for a failed upgrade.
// repeatCount is the expected number of times the upgrade failed.
- (void)assertFailedUpgrade:(int)repeatCount {
  GREYAssertNil([MetricsAppInterface
                    expectTotalCount:(repeatCount * 2)
                        forHistogram:@(security_interstitials::https_only_mode::
                                           kEventHistogram)],
                @"Failed to record event histogram");

  GREYAssertNil([MetricsAppInterface
                     expectCount:repeatCount
                       forBucket:static_cast<int>(
                                     security_interstitials::https_only_mode::
                                         Event::kUpgradeAttempted)
                    forHistogram:@(security_interstitials::https_only_mode::
                                       kEventHistogram)],
                @"Failed to record upgrade attempt");
  GREYAssertNil([MetricsAppInterface
                     expectCount:repeatCount
                       forBucket:static_cast<int>(
                                     security_interstitials::https_only_mode::
                                         Event::kUpgradeFailed)
                    forHistogram:@(security_interstitials::https_only_mode::
                                       kEventHistogram)],
                @"Failed to record fail event");
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"Timer is still running");
  GREYAssert(![HttpsUpgradeAppInterface isOmniboxUpgradeTimerRunning],
             @"Omnibox upgrade timer is unexpectedly running");

  // Omnibox HTTPS Upgrades shouldn't handle this navigation.
  GREYAssertNil(
      [MetricsAppInterface
          expectTotalCount:0
              forHistogram:@(security_interstitials::omnibox_https_upgrades::
                                 kEventHistogram)],
      @"Omnibox HTTPS Upgrades unexpectedly recorded a histogram event");
}

// Asserts that the metrics are properly recorded for a timed-out upgrade.
// repeatCount is the expected number of times the upgrade failed.
- (void)assertTimedOutUpgrade:(int)repeatCount {
  GREYAssertNil([MetricsAppInterface
                    expectTotalCount:(repeatCount * 2)
                        forHistogram:@(security_interstitials::https_only_mode::
                                           kEventHistogram)],
                @"Incorrect number of records in event histogram");

  GREYAssertNil([MetricsAppInterface
                     expectCount:repeatCount
                       forBucket:static_cast<int>(
                                     security_interstitials::https_only_mode::
                                         Event::kUpgradeAttempted)
                    forHistogram:@(security_interstitials::https_only_mode::
                                       kEventHistogram)],
                @"Failed to record upgrade attempt");
  GREYAssertNil([MetricsAppInterface
                     expectCount:repeatCount
                       forBucket:static_cast<int>(
                                     security_interstitials::https_only_mode::
                                         Event::kUpgradeTimedOut)
                    forHistogram:@(security_interstitials::https_only_mode::
                                       kEventHistogram)],
                @"Failed to record fail event");
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"Timer is still running");
  GREYAssert(![HttpsUpgradeAppInterface isOmniboxUpgradeTimerRunning],
             @"Omnibox upgrade timer is unexpectedly running");

  // Omnibox HTTPS Upgrades shouldn't handle this navigation.
  GREYAssertNil(
      [MetricsAppInterface
          expectTotalCount:0
              forHistogram:@(security_interstitials::omnibox_https_upgrades::
                                 kEventHistogram)],
      @"Omnibox HTTPS Upgrades unexpectedly recorded a histogram event");
}

#pragma mark - Tests

// Disable HTTPS-Only Mode and navigate to an HTTP URL directly.
// - If HTTPS Upgrades is disabled, this should load the HTTP URL even though
//   the upgraded HTTPS version serves good SSL.
// - Otherwise, it should load the HTTPS URL.
- (void)test_HttpsOnlyModeDisabled {
  [ChromeEarlGrey setBoolValue:NO forUserPref:prefs::kHttpsOnlyModeEnabled];

  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.goodHTTPSServer->port()
                                      useFakeHTTPS:true];

  [ChromeEarlGrey loadURL:GURL("chrome://version")];
  [ChromeEarlGrey waitForWebStateContainingText:"Revision"];

  GURL testURL = self.testServer->GetURL("/");
  [ChromeEarlGrey loadURL:testURL];

  if ([self testType] == TestType::kHttpsUpgrades) {
    [ChromeEarlGrey waitForWebStateContainingText:"HTTPS_RESPONSE"];
    [self assertSuccessfulUpgrade];
  } else {
    [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
    [self assertNoUpgrade];
  }
}

// Tests that navigations to localhost URLs aren't upgraded.
- (void)test_Localhost_ShouldNotUpgrade {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.goodHTTPSServer->port()
                                      useFakeHTTPS:true];

  GURL testURL = self.testServer->GetURL("/");
  GURL::Replacements replacements;
  replacements.SetHostStr("localhost");
  GURL localhostURL = testURL.ReplaceComponents(replacements);

  [ChromeEarlGrey loadURL:localhostURL];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  [self assertNoUpgrade];
}

// Navigate to an HTTPS URL directly. The navigation shouldn't be upgraded.
- (void)test_GoodHTTPS_ShouldNotUpgrade {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.goodHTTPSServer->port()
                                      useFakeHTTPS:true];

  GURL testURL = self.goodHTTPSServer->GetURL("/");
  [ChromeEarlGrey loadURL:testURL];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTPS_RESPONSE"];
  [self assertNoUpgrade];
}

// Navigate to an HTTP URL directly. The upgraded HTTPS version serves good SSL.
// This should end up loading the HTTPS version of the URL.
- (void)test_HTTPWithGoodHTTPS_ShouldUpgrade {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.goodHTTPSServer->port()
                                      useFakeHTTPS:true];

  GURL testURL = self.testServer->GetURL("/");
  [ChromeEarlGrey loadURL:testURL];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTPS_RESPONSE"];
  [self assertSuccessfulUpgrade];
}

// Navigate to an HTTP URL by clicking a link. This should end up loading the
// HTTPS version of the URL.
- (void)test_HTTPWithGoodHTTPS_LinkClick_ShouldUpgrade {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.goodHTTPSServer->port()
                                      useFakeHTTPS:true];
  int HTTPPort = self.testServer->port();

  GURL testURL(base::StringPrintf(
      "data:text/html,"
      "<a href='http://127.0.0.1:%d/good-https' id='link'>Link</a><br>READY",
      HTTPPort));
  [ChromeEarlGrey loadURL:testURL];
  [ChromeEarlGrey waitForWebStateContainingText:"READY"];

  // Click on the http link. Should load the https URL.
  [ChromeEarlGrey tapWebStateElementWithID:@"link"];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTPS_RESPONSE"];
  [self assertSuccessfulUpgrade];
}

// Navigate to an HTTP URL by posting a form. This should not be upgraded to
// HTTPS.
- (void)test_HTTPWithGoodHTTPS_Post_ShouldNotUpgrade {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.goodHTTPSServer->port()
                                      useFakeHTTPS:true];
  int HTTPPort = self.testServer->port();

  GURL testURL(base::StringPrintf(
      "data:text/html,"
      "<form method='POST' action='http://127.0.0.1:%d/good-https' id='myform'>"
      "<input name='test' value='test'><br>"
      "<input type='submit' id='submit-btn' value='Submit'></form><br>READY",
      HTTPPort));
  [ChromeEarlGrey loadURL:testURL];
  [ChromeEarlGrey waitForWebStateContainingText:"READY"];

  // Post the form. Should load the http URL.
  [ChromeEarlGrey tapWebStateElementWithID:@"submit-btn"];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  [self assertNoUpgrade];
}

// Navigate to an HTTP URL directly. The upgraded HTTPS version serves good SSL
// which redirects to the original HTTP URL. This should show the interstitial.
- (void)test_HTTPSRedirectsToHTTP_ShouldFallback {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.goodHTTPSServer->port()
                                      useFakeHTTPS:true];

  [ChromeEarlGrey loadURL:GURL("chrome://version")];
  [ChromeEarlGrey waitForWebStateContainingText:"Revision"];

  GURL targetURL = self.testServer->GetURL("/");
  GURL upgradedURL =
      self.goodHTTPSServer->GetURL("/?redirect=" + targetURL.spec());
  const std::string port_str = base::NumberToString(self.testServer->port());
  GURL::Replacements replacements;
  replacements.SetPortStr(port_str);
  GURL testURL = upgradedURL.ReplaceComponents(replacements);

  [ChromeEarlGrey loadURL:testURL];
  if ([self isInterstitialEnabled]) {
    [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
    [self assertFailedUpgrade:1];

    // Click through the interstitial. This should load the HTTP page.
    [ChromeEarlGrey tapWebStateElementWithID:@"proceed-button"];
  }

  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"Timer is still running");

  // Going back should go to chrome://version.
  [ChromeEarlGrey goBack];
  [ChromeEarlGrey waitForWebStateContainingText:"Revision"];
  [self assertFailedUpgrade:1];
}

// Tests that prerendered navigations that should be upgraded are cancelled.
// This test is adapted from testTapPrerenderSuggestions() in
// prerender_egtest.mm.
// TODO(crbug.com/40833424): Reenable.
- (void)DISABLED_test_BadHTTPS_ShouldCancelPrerender {
  // TODO(crbug.com/40553918): Re-enable the test on iPad once the alternate
  // letters problem is fixed.
  if ([ChromeEarlGrey isIPadIdiom]) {
    EARL_GREY_TEST_DISABLED(
        @"Disabled for iPad due to alternate letters educational screen.");
  }

  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.badHTTPSServer->port()
                                      useFakeHTTPS:false];

  [ChromeEarlGrey clearBrowsingHistory];

  // Type the full URL. This will show an interstitial. This adds the URL to
  // history.
  GURL testURL = self.testServer->GetURL("/");
  NSString* pageString = base::SysUTF8ToNSString(testURL.GetContent());
  [[EarlGrey selectElementWithMatcher:chrome_test_util::FakeOmnibox()]
      performAction:grey_tap()];
  [ChromeEarlGrey
      waitForSufficientlyVisibleElementWithMatcher:chrome_test_util::Omnibox()];
  // TODO(crbug.com/40916974): Use simulatePhysicalKeyboardEvent until
  // replaceText can properly handle \n.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::Omnibox()]
      performAction:grey_replaceText(pageString)];
  [ChromeEarlGrey simulatePhysicalKeyboardEvent:@"\n" flags:0];

  [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
  [self assertFailedUpgrade:1];
  GREYAssertEqual(2, _HTTPResponseCounter,
                  @"The server should have responded twice");

  // Click through the interstitial.
  [ChromeEarlGrey tapWebStateElementWithID:@"proceed-button"];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"Timer is still running");
  GREYAssertEqual(3, _HTTPResponseCounter,
                  @"The server should have responded three times");

  // Close all tabs and reopen. This clears the allowlist because it's currently
  // per-tab.
  [[self class] closeAllTabs];
  [ChromeEarlGrey openNewTab];

  // Type the begining of the address to have the autocomplete suggestion.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::FakeOmnibox()]
      performAction:grey_tap()];
  [ChromeEarlGrey
      waitForSufficientlyVisibleElementWithMatcher:chrome_test_util::Omnibox()];
  // Type a single character. This causes two prerender attempts.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::Omnibox()]
      performAction:grey_replaceText([pageString substringToIndex:1])];

  // Wait until prerender request reaches the server.
  bool prerendered = WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
    return self->_HTTPResponseCounter > 3;
  });
  GREYAssertTrue(prerendered, @"Prerender did not happen");

  // Check the histograms. All prerender attempts must be cancelled. Relying on
  // the histogram here isn't great, but there doesn't seem to be a good
  // way of testing that prerenders have been cancelled.
  GREYAssertNil(
      [MetricsAppInterface expectCount:0
                             forBucket:/*PRERENDER_FINAL_STATUS_USED=*/0
                          forHistogram:@"Prerender.FinalStatus"],
      @"Prerender was used");
  // TODO(crbug.com/40825375): Check that the CANCEL bucket has non-zero
  // elements. Not currently supported by MetricsAppInterface.
}

// Regression test for crbug.com/1379261. Checks that cancelling a prerendered
// navigation doesn't cause a crash. Steps are:
// 1. Disable HTTPS-Only Mode and visit an http:// URL. This puts the
//    URL in browser history.
// 2. Close tabs and reopen. Enable HTTPS-Only Mode.
// 3. Type the first letter of the http:// URL in step 1. This will prerender
//    the http URL.
// 4. Check that the prerender was cancelled properly.
// TODO(crbug.com/40833424): Reenable.
- (void)DISABLED_test_Prerender_CancelShouldNotCrash {
  // TODO(crbug.com/40553918): Re-enable the test on iPad once the alternate
  // letters problem is fixed.
  if ([ChromeEarlGrey isIPadIdiom]) {
    EARL_GREY_TEST_DISABLED(
        @"Disabled for iPad due to alternate letters educational screen.");
  }

  // Step 1: Disable HTTPS-Only Mode and visit an http:// URL. This puts the
  // URL in browser history.
  [ChromeEarlGrey setBoolValue:NO forUserPref:prefs::kHttpsOnlyModeEnabled];
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.badHTTPSServer->port()
                                      useFakeHTTPS:false];
  [ChromeEarlGrey clearBrowsingHistory];

  GURL testURL = self.testServer->GetURL("/");
  NSString* testURLString = base::SysUTF8ToNSString(testURL.GetContent());
  [ChromeEarlGrey loadURL:testURL];
  GREYAssertEqual(1, _HTTPResponseCounter,
                  @"The server should have responded once");
  [ChromeEarlGrey goBack];

  // Step 2: Close tabs and reopen. Enable HTTPS-Only Mode.
  [[self class] closeAllTabs];
  [ChromeEarlGrey openNewTab];
  [ChromeEarlGrey setBoolValue:YES forUserPref:prefs::kHttpsOnlyModeEnabled];

  // Step 3: Type the first letter of the http:// URL in step 1. This will
  // prerender the http URL.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::FakeOmnibox()]
      performAction:grey_tap()];
  [ChromeEarlGrey
      waitForSufficientlyVisibleElementWithMatcher:chrome_test_util::Omnibox()];
  [[EarlGrey selectElementWithMatcher:chrome_test_util::Omnibox()]
      performAction:grey_replaceText([testURLString substringToIndex:1])];

  bool prerendered = WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
    // The first response was for the http:// URL. The remaining responses are
    // prerendered. When the whole test suite runs, we may get more than one
    // prerendered navigation here.
    return self->_HTTPResponseCounter > 1;
  });
  GREYAssertTrue(prerendered, @"Prerender did not happen");

  // Step 4: Check that the prerender was cancelled properly.
  // Check the histograms. All prerender attempts must be cancelled. Relying on
  // the histogram here isn't great, but there doesn't seem to be a good
  // way of testing that prerenders have been cancelled.
  GREYAssertNil(
      [MetricsAppInterface expectCount:0
                             forBucket:/*PRERENDER_FINAL_STATUS_USED=*/0
                          forHistogram:@"Prerender.FinalStatus"],
      @"Prerender was used");
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"Timer is still running");
  GREYAssert(![HttpsUpgradeAppInterface isOmniboxUpgradeTimerRunning],
             @"Omnibox upgrade timer is unexpectedly running");

  // Check that the HTTPS-Only Mode tab helper recorded the prerender
  // cancellation.
  // First server response loaded normally, the rest should be prerenders.
  int prerenderCount = self->_HTTPResponseCounter - 1;
  GREYAssertNil([MetricsAppInterface
                    expectTotalCount:prerenderCount
                        forHistogram:@(security_interstitials::https_only_mode::
                                           kEventHistogram)],
                @"Failed to record event histogram");
  GREYAssertNil([MetricsAppInterface
                     expectCount:prerenderCount
                       forBucket:static_cast<int>(
                                     security_interstitials::https_only_mode::
                                         Event::kPrerenderCancelled)
                    forHistogram:@(security_interstitials::https_only_mode::
                                       kEventHistogram)],
                @"Failed to record prerender cancellation");
}

// Navigate to an HTTP URL and allowlist the URL. Then clear browsing data.
// This should clear the HTTP allowlist.
- (void)test_RemoveBrowsingData_ShouldClearAllowlist {
  if (![self isInterstitialEnabled]) {
    // Only relevant for HTTPS-Only Mode.
    // TODO(crbug.com/40914607): Enable for HTTPS-Upgrades when it implements
    // allowlisting.
    return;
  }
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.badHTTPSServer->port()
                                      useFakeHTTPS:false];

  GURL testURL = self.testServer->GetURL("/");
  [ChromeEarlGrey loadURL:testURL];
  [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
  [self assertFailedUpgrade:1];

  // Click through the interstitial. This should load the HTTP page. Histogram
  // numbers shouldn't change.
  [ChromeEarlGrey tapWebStateElementWithID:@"proceed-button"];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  [self assertFailedUpgrade:1];

  // Reload. Since the URL is now allowlisted, this should immediately load
  // HTTP without trying to upgrade. Histogram numbers shouldn't change.
  [ChromeEarlGrey reload];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  [self assertFailedUpgrade:1];

  // Clear the allowlist by clearing the browsing data. This clears the history
  // programmatically, so it won't automatically reload the tabs.
  [ChromeEarlGrey clearBrowsingHistory];

  // Reloading the should show the interstitial again.
  [ChromeEarlGrey reload];
  [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
  [self assertFailedUpgrade:2];

  // Reload once more.
  [ChromeEarlGrey reload];
  [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
  [self assertFailedUpgrade:3];
}

// Click on the "Learn more" link in the interstitial. This should open a
// new tab.
- (void)test_ClickLearnMore_ShouldOpenNewTab {
  if (![self isInterstitialEnabled]) {
    // Only relevant for HTTPS-Only mode tests.
    return;
  }
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.badHTTPSServer->port()
                                      useFakeHTTPS:false];

  GURL testURL = self.testServer->GetURL("/");
  [ChromeEarlGrey loadURL:testURL];
  [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
  [self assertFailedUpgrade:1];

  // Check tab count prior to tapping the link.
  NSUInteger oldRegularTabCount = [ChromeEarlGrey mainTabCount];
  NSUInteger oldIncognitoTabCount = [ChromeEarlGrey incognitoTabCount];

  [ChromeEarlGrey tapWebStateElementWithID:@"learn-more-link"];

  // A new tab should open after tapping the link.
  [ChromeEarlGrey waitForMainTabCount:oldRegularTabCount + 1];
  [ChromeEarlGrey waitForIncognitoTabCount:oldIncognitoTabCount];
}

// Navigate to an HTTP URL directly. The upgraded HTTPS version serves bad SSL.
// The upgrade will fail and the HTTPS-Only mode interstitial will be shown.
// Reloading the page should show the interstitial again.
- (void)test_BadHTTPS_ReloadInterstitial {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.badHTTPSServer->port()
                                      useFakeHTTPS:false];

  GURL testURL = self.testServer->GetURL("/");
  [ChromeEarlGrey loadURL:testURL];
  if ([self isInterstitialEnabled]) {
    [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
  } else {
    [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  }
  [self assertFailedUpgrade:1];

  [ChromeEarlGrey reload];
  if ([self isInterstitialEnabled]) {
    [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
  } else {
    [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  }
  [self assertFailedUpgrade:2];
}

// Navigate to an HTTP URL directly. The upgraded HTTPS version serves slow SSL.
// The upgrade will fail and the HTTPS-Only mode interstitial will be shown.
// Reloading the page should show the interstitial again.
- (void)test_SlowHTTPS_ReloadInterstitial {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.slowServer->port()
                                      useFakeHTTPS:true];
  // Set the fallback delay to zero. This will immediately stop the HTTPS
  // upgrade attempt.
  [HttpsUpgradeAppInterface setFallbackDelayForTesting:0];

  GURL testURL = self.testServer->GetURL("/");
  [ChromeEarlGrey loadURL:testURL];
  if ([self isInterstitialEnabled]) {
    [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
  } else {
    [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  }
  [self assertTimedOutUpgrade:1];

  [ChromeEarlGrey reload];
  if ([self isInterstitialEnabled]) {
    [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
  } else {
    [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  }
  [self assertTimedOutUpgrade:2];
}

// Navigate to an HTTP URL directly. The upgraded HTTPS version serves bad SSL.
// The upgrade will fail and the HTTPS-Only mode interstitial will be shown.
// Click through the interstitial, then reload the page. The HTTP page should
// be shown.
- (void)test_BadHTTPS_ProceedInterstitial_Allowlisted {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.badHTTPSServer->port()
                                      useFakeHTTPS:false];

  GURL testURL = self.testServer->GetURL("/");
  [ChromeEarlGrey loadURL:testURL];
  if ([self isInterstitialEnabled]) {
    [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
    // Click through the interstitial. This should load the HTTP page.
    [ChromeEarlGrey tapWebStateElementWithID:@"proceed-button"];
  }
  [self assertFailedUpgrade:1];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"Timer is still running");

  // Reload. Since the URL is now allowlisted, this should immediately load
  // HTTP without trying to upgrade.
  [ChromeEarlGrey reload];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];

  if ([self isInterstitialEnabled]) {
    // If HTTPS-Only mode is enabled, clicking through the interstitial will
    // allowlist the site so no new histogram entry will be recorded.
    // Failed upgrades record two entries and this number shouldn't change.
    GREYAssertNil([MetricsAppInterface
                      expectTotalCount:2
                          forHistogram:@(security_interstitials::
                                             https_only_mode::kEventHistogram)],
                  @"Unexpected histogram event recorded.");
  } else {
    // HTTPS-Upgrades will attempt to upgrade and fail for the second time.
    [self assertFailedUpgrade:2];
  }
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"Timer is still running");

  // Open a new tab and go to the same URL. Should load the page without an
  // interstitial.
  [ChromeEarlGrey openNewTab];
  [ChromeEarlGrey loadURL:testURL];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"Timer is still running");
  if ([self isInterstitialEnabled]) {
    [self assertFailedUpgrade:1];
  } else {
    // HTTPS-Upgrades will attempt to upgrade and fail for the third time.
    [self assertFailedUpgrade:3];
  }

  // Open an incognito tab and try there. Should show the interstitial as
  // allowlist decisions don't carry over to incognito.
  [ChromeEarlGrey openNewIncognitoTab];
  // Set the testing information for the incognito tab.
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.badHTTPSServer->port()
                                      useFakeHTTPS:false];
  [ChromeEarlGrey loadURL:testURL];

  if ([self isInterstitialEnabled]) {
    [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
    // Click through the interstitial. This should load the HTTP page.
    [ChromeEarlGrey tapWebStateElementWithID:@"proceed-button"];
    [self assertFailedUpgrade:2];
  } else {
    // HTTPS-Upgrades will attempt to upgrade and fail for the fourth time.
    [self assertFailedUpgrade:4];
  }
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"Timer is still running");

  [ChromeEarlGreyUI reload];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  if ([self isInterstitialEnabled]) {
    // If HTTPS-Only mode is enabled, this should immediately load HTTP without
    // trying to upgrade because it was allowlisted.
    [self assertFailedUpgrade:2];
  } else {
    // HTTPS-Upgrades will attempt to upgrade and fail for the fifth time.
    [self assertFailedUpgrade:5];
  }
}

// Same as testUpgrade_BadHTTPS_ProceedInterstitial_Allowlisted but uses
// a slow HTTPS response instead:
// Navigate to an HTTP URL directly. The upgraded HTTPS version serves a slow
// loading SSL page. The upgrade will be cancelled and the HTTPS-Only mode
// interstitial will be shown. Click through the interstitial, then reload the
// page. The HTTP page should be shown.
- (void)test_SlowHTTPS_ProceedInterstitial_Allowlisted {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.slowServer->port()
                                      useFakeHTTPS:true];
  // Set the fallback delay to zero. This will immediately stop the HTTPS
  // upgrade attempt.
  [HttpsUpgradeAppInterface setFallbackDelayForTesting:0];

  GURL testURL = self.testServer->GetURL("/");
  [ChromeEarlGrey loadURL:testURL];
  if ([self isInterstitialEnabled]) {
    [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
    // Click through the interstitial. This should load the HTTP page.
    [ChromeEarlGrey tapWebStateElementWithID:@"proceed-button"];
  }
  [self assertTimedOutUpgrade:1];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"Timer is still running");

  // Reload.
  [ChromeEarlGrey reload];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  if ([self isInterstitialEnabled]) {
    // If HTTPS-Only mode is enabled, clicking through the interstitial will
    // allowlist the site so no new histogram entry will be recorded.
    // Failed upgrades record two entries and this number shouldn't change.
    GREYAssertNil([MetricsAppInterface
                      expectTotalCount:2
                          forHistogram:@(security_interstitials::
                                             https_only_mode::kEventHistogram)],
                  @"Unexpected histogram event recorded.");
  } else {
    // HTTPS-Upgrades will attempt to upgrade and fail for the second time.
    [self assertTimedOutUpgrade:2];
  }
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"Timer is still running");
}

// Navigate to an HTTP URL directly. The upgraded HTTPS version serves bad SSL.
// The upgrade will fail and the HTTPS-Only mode interstitial will be shown.
// Tap Go back on the interstitial.
- (void)test_BadHTTPS_GoBack {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.badHTTPSServer->port()
                                      useFakeHTTPS:false];

  [ChromeEarlGrey loadURL:GURL("chrome://version")];
  [ChromeEarlGrey waitForWebStateContainingText:"Revision"];

  // Load a site with a bad HTTPS upgrade. This shows an interstitial.
  GURL testURL = self.testServer->GetURL("/");
  [ChromeEarlGrey loadURL:testURL];
  if ([self isInterstitialEnabled]) {
    [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
  } else {
    [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  }
  [self assertFailedUpgrade:1];

  if ([self isInterstitialEnabled]) {
    // Tap "Go back" on the interstitial. This should go back to
    // chrome://version.
    [ChromeEarlGrey tapWebStateElementWithID:@"primary-button"];
  } else {
    [ChromeEarlGrey goBack];
  }
  [ChromeEarlGrey waitForWebStateContainingText:"Revision"];

  // Go forward. Should hit the interstitial again.
  [ChromeEarlGrey goForward];
  if ([self isInterstitialEnabled]) {
    [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
    [self assertFailedUpgrade:2];
  } else {
    [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
    // TODO(crbug.com/40914607): This should equal to 2 instead.
    [self assertFailedUpgrade:1];
  }
}

// Same as testUpgrade_BadHTTPS_GoBack but uses a slow HTTPS response instead:
// Navigate to an HTTP URL directly. The upgraded HTTPS version serves a slow
// loading HTTPS page. The upgrade will be cancelled and the HTTPS-Only mode
// interstitial will be shown. Tap Go back on the interstitial.
- (void)test_SlowHTTPS_GoBack {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.slowServer->port()
                                      useFakeHTTPS:true];
  // Set the fallback delay to zero. This will immediately stop the HTTPS
  // upgrade attempt.
  [HttpsUpgradeAppInterface setFallbackDelayForTesting:0];

  [ChromeEarlGrey loadURL:GURL("chrome://version")];
  [ChromeEarlGrey waitForWebStateContainingText:"Revision"];

  // Load a site with a slow HTTPS upgrade. This shows an interstitial.
  GURL testURL = self.testServer->GetURL("/");
  [ChromeEarlGrey loadURL:testURL];
  if ([self isInterstitialEnabled]) {
    [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
  } else {
    [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  }
  [self assertTimedOutUpgrade:1];

  if ([self isInterstitialEnabled]) {
    // Tap "Go back" on the interstitial. This should go back to
    // chrome://version.
    [ChromeEarlGrey tapWebStateElementWithID:@"primary-button"];
  } else {
    [ChromeEarlGrey goBack];
  }
  [ChromeEarlGrey waitForWebStateContainingText:"Revision"];

  // Go forward. Should hit the interstitial again.
  [ChromeEarlGrey goForward];
  if ([self isInterstitialEnabled]) {
    [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
    [self assertTimedOutUpgrade:2];
  } else {
    [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
    // TODO(crbug.com/40914607): This should equal to 2 instead.
    [self assertTimedOutUpgrade:1];
  }
}

// Navigate to an HTTP URL and click through the interstitial. Then,
// navigate to a new page and go back. This should load the HTTP URL
// without showing the interstitial again.
- (void)test_BadHTTPS_GoBackToAllowlistedSite {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.badHTTPSServer->port()
                                      useFakeHTTPS:false];

  [ChromeEarlGrey loadURL:GURL("about:blank")];

  // Load a site with a bad HTTPS upgrade. This shows an interstitial.
  GURL testURL = self.testServer->GetURL("/");
  [ChromeEarlGrey loadURL:testURL];

  if ([self isInterstitialEnabled]) {
    [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
    [self assertFailedUpgrade:1];
    // Click through the interstitial. This should load the HTTP page.
    [ChromeEarlGrey tapWebStateElementWithID:@"proceed-button"];
  }

  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];

  // Go to a new page.
  [ChromeEarlGrey loadURL:GURL("chrome://version")];
  [ChromeEarlGrey waitForWebStateContainingText:"Revision"];

  // Then go back to the HTTP URL. Since we previously clicked through its
  // interstitial, this should immediately load the HTTP response.
  [ChromeEarlGrey goBack];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  // Histogram numbers shouldn't change.
  [self assertFailedUpgrade:1];
}

// Same as testUpgrade_BadHTTPS_GoBackToAllowlistedSite but uses a slow
// HTTPS response instead:
// Navigate to an HTTP URL with a slow HTTPS upgrade, click through the
// interstitial. Then, navigate to a new page and go back. This should load the
// HTTP URL without showing the interstitial again.
- (void)test_SlowHTTPS_GoBackToAllowlistedSite {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.slowServer->port()
                                      useFakeHTTPS:true];
  // Set the fallback delay to zero. This will immediately stop the HTTPS
  // upgrade attempt.
  [HttpsUpgradeAppInterface setFallbackDelayForTesting:0];

  [ChromeEarlGrey loadURL:GURL("about:blank")];

  // Load a site with a bad HTTPS upgrade. This shows an interstitial.
  GURL testURL = self.testServer->GetURL("/");
  [ChromeEarlGrey loadURL:testURL];

  if ([self isInterstitialEnabled]) {
    [ChromeEarlGrey waitForWebStateContainingText:kInterstitialText];
    [self assertTimedOutUpgrade:1];
    // Click through the interstitial. This should load the HTTP page.
    [ChromeEarlGrey tapWebStateElementWithID:@"proceed-button"];
  }

  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"Timer is still running");

  // Go to a new page.
  [ChromeEarlGrey loadURL:GURL("chrome://version")];
  [ChromeEarlGrey waitForWebStateContainingText:"Revision"];

  // Then go back to the HTTP URL. Since we previously clicked through its
  // interstitial, this should immediately load the HTTP response.
  [ChromeEarlGrey goBack];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  // Histogram numbers shouldn't change.
  [self assertTimedOutUpgrade:1];
}

@end

// Tests for HTTPS-Upgrades feature.
// TODO(crbug.com/40849153): Remove the "ZZZ" when the bug is fixed.
@interface ZZZ_HttpsUpgradesTestCase : ZZZ_HttpsOnlyModeTestCase
@end

@implementation ZZZ_HttpsUpgradesTestCase

- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config = [super appConfigurationForTestCase];
  config.features_disabled = {omnibox::kDefaultTypedNavigationsToHttps};

  config.features_enabled.push_back(
      security_interstitials::features::kHttpsUpgrades);
  return config;
}

// This is currently needed to prevent this test case from being ignored.
- (void)testEmpty {
}

- (TestType)testType {
  return TestType::kHttpsUpgrades;
}

@end