chromium/ios/chrome/browser/https_upgrades/model/typed_navigation_upgrade_tab_helper_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/sys_string_conversions.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 "components/strings/grit/components_strings.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 security_interstitials::omnibox_https_upgrades::Event;
using security_interstitials::omnibox_https_upgrades::kEventHistogram;

namespace {

std::string GetURLWithoutScheme(const GURL& url) {
  return url.spec().substr(url.scheme().size() + strlen("://"));
}

}  // namespace

// Tests defaulting typed omnibox navigations to HTTPS.
@interface TypedNavigationUpgradeTestCase : HttpsUpgradeTestCaseBase {
}
@end

@implementation TypedNavigationUpgradeTestCase

- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config;
  config.relaunch_policy = NoForceRelaunchAndResetState;
  config.features_enabled.push_back(omnibox::kDefaultTypedNavigationsToHttps);
  config.features_disabled.push_back(
      security_interstitials::features::kHttpsUpgrades);
  // TODO(crbug.com/335821156): Investigate why rich inline triggers with
  // test_HTTPWithSlowHTTPS_ShouldFallBack and remove this.
  config.features_disabled.push_back(omnibox::kRichAutocompletion);
  return config;
}

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

  // Disable HTTPS-Only Mode.
  [ChromeEarlGrey setBoolValue:NO forUserPref:prefs::kHttpsOnlyModeEnabled];
}

- (void)tearDown {
  [super tearDown];
}

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

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

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

  GREYAssertNil(
      [MetricsAppInterface
           expectCount:1 * repeatCount
             forBucket:static_cast<int>(
                           security_interstitials::omnibox_https_upgrades::
                               Event::kHttpsLoadStarted)
          forHistogram:@(security_interstitials::omnibox_https_upgrades::
                             kEventHistogram)],
      @"Failed to record upgrade attempt");
  GREYAssertNil(
      [MetricsAppInterface
           expectCount:1 * repeatCount
             forBucket:static_cast<int>(
                           security_interstitials::omnibox_https_upgrades::
                               Event::kHttpsLoadSucceeded)
          forHistogram:@(security_interstitials::omnibox_https_upgrades::
                             kEventHistogram)],
      @"Failed to record upgrade attempt");
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"HTTPS Only Mode timer is still running");
  GREYAssert(![HttpsUpgradeAppInterface isOmniboxUpgradeTimerRunning],
             @"Omnibox upgrade timer is still running");

  // HTTPS-Only mode shouldn't handle this navigation.
  GREYAssertNil([MetricsAppInterface
                    expectTotalCount:0
                        forHistogram:@(security_interstitials::https_only_mode::
                                           kEventHistogram)],
                @"HTTPS-Only mode 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::omnibox_https_upgrades::
                                 kEventHistogram)],
      @"Failed to record event histogram");

  GREYAssertNil(
      [MetricsAppInterface
           expectCount:repeatCount
             forBucket:static_cast<int>(
                           security_interstitials::omnibox_https_upgrades::
                               Event::kHttpsLoadStarted)
          forHistogram:@(security_interstitials::omnibox_https_upgrades::
                             kEventHistogram)],
      @"Failed to record upgrade attempt");
  GREYAssertNil(
      [MetricsAppInterface
           expectCount:repeatCount
             forBucket:static_cast<int>(
                           security_interstitials::omnibox_https_upgrades::
                               Event::kHttpsLoadFailedWithCertError)
          forHistogram:@(security_interstitials::omnibox_https_upgrades::
                             kEventHistogram)],
      @"Failed to record fail event");
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"HTTPS Only Mode timer is still running");
  GREYAssert(![HttpsUpgradeAppInterface isOmniboxUpgradeTimerRunning],
             @"Omnibox upgrade timer is still running");

  // HTTPS-Only mode shouldn't handle this navigation.
  GREYAssertNil([MetricsAppInterface
                    expectTotalCount:0
                        forHistogram:@(security_interstitials::https_only_mode::
                                           kEventHistogram)],
                @"HTTPS-Only mode 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::omnibox_https_upgrades::
                                 kEventHistogram)],
      @"Incorrect numbber of records in event histogram");

  GREYAssertNil(
      [MetricsAppInterface
           expectCount:repeatCount
             forBucket:static_cast<int>(
                           security_interstitials::omnibox_https_upgrades::
                               Event::kHttpsLoadStarted)
          forHistogram:@(security_interstitials::omnibox_https_upgrades::
                             kEventHistogram)],
      @"Failed to record upgrade attempt");
  GREYAssertNil(
      [MetricsAppInterface
           expectCount:repeatCount
             forBucket:static_cast<int>(
                           security_interstitials::omnibox_https_upgrades::
                               Event::kHttpsLoadTimedOut)
          forHistogram:@(security_interstitials::omnibox_https_upgrades::
                             kEventHistogram)],
      @"Failed to record fail event");
  GREYAssert(![HttpsUpgradeAppInterface isHttpsOnlyModeTimerRunning],
             @"HTTPS Only Mode timer is still running");
  GREYAssert(![HttpsUpgradeAppInterface isOmniboxUpgradeTimerRunning],
             @"Omnibox upgrade timer is still running");

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

#pragma mark - Tests

// Navigate to an HTTP URL. Since it's not typed in the omnibox, it shouldn't
// be upgraded to HTTPS.
- (void)test_NoTyping_ShouldNotUpgrade {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.goodHTTPSServer->port()
                                      useFakeHTTPS:true];

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

// Type an HTTP URL with scheme. It shouldn't be upgraded to HTTPS.
- (void)test_TypeFullHTTPURL_ShouldNotUpgrade {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.goodHTTPSServer->port()
                                      useFakeHTTPS:true];

  // Go to a web page to have a normal location bar.
  [ChromeEarlGrey loadURL:GURL("data:text/html,Blank Page")];
  [ChromeEarlGrey waitForWebStateContainingText:"Blank Page"];

  GURL testURL = self.goodHTTPSServer->GetURL("/");
  [ChromeEarlGreyUI typeTextInOmnibox:testURL.spec() andPressEnter:YES];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTPS_RESPONSE"];
  [self assertNoUpgrade];
}

// Type an HTTPS URL with scheme. It shouldn't be upgraded.
- (void)test_TypeFullHTTPSURL_ShouldNotUpgrade {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.goodHTTPSServer->port()
                                      useFakeHTTPS:true];

  // Go to a web page to have a normal location bar.
  [ChromeEarlGrey loadURL:GURL("data:text/html,Blank Page")];
  [ChromeEarlGrey waitForWebStateContainingText:"Blank Page"];

  GURL testURL = self.testServer->GetURL("/");
  [ChromeEarlGreyUI typeTextInOmnibox:testURL.spec() andPressEnter:YES];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  [self assertNoUpgrade];
}

// Type an HTTP URL without scheme. The navigation should be upgraded to HTTPS
// which should load successfully.
- (void)test_TypeHTTPWithGoodHTTPS_ShouldUpgrade {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.goodHTTPSServer->port()
                                      useFakeHTTPS:true];

  // Go to a web page to have a normal location bar.
  [ChromeEarlGrey loadURL:GURL("data:text/html,Blank Page")];
  [ChromeEarlGrey waitForWebStateContainingText:"Blank Page"];

  GURL testURL = self.testServer->GetURL("/");
  std::string text = GetURLWithoutScheme(testURL);

  // Type the URL in the omnibox.
  [ChromeEarlGreyUI typeTextInOmnibox:text andPressEnter:YES];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTPS_RESPONSE"];
  [self assertSuccessfulUpgrade:1];

  // Load an interim data URL to clear the "HTTP_RESPONSE" text.
  [ChromeEarlGrey loadURL:GURL("data:text/html,Blank Page")];
  [ChromeEarlGrey waitForWebStateContainingText:"Blank Page"];

  // Type again. Normally, Omnibox should remember the successful HTTPS
  // navigation and not attempt to upgrade again. We are using a faux-HTTPS
  // server in tests which serves an http:// URL, so it will get upgraded again.
  [ChromeEarlGreyUI typeTextInOmnibox:text andPressEnter:YES];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTPS_RESPONSE"];
  [self assertSuccessfulUpgrade:2];
}

// Same as test_TypeHTTPWithGoodHTTPS_ShouldUpgrade but with HTTPS-Only Mode
// enabled.
- (void)test_TypeHTTPWithGoodHTTPS_HTTPSOnlyModeEnabled_ShouldUpgrade {
  // Enable HTTPS-Only Mode.
  [ChromeEarlGrey setBoolValue:YES forUserPref:prefs::kHttpsOnlyModeEnabled];

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

  // Go to a web page to have a normal location bar.
  [ChromeEarlGrey loadURL:GURL("data:text/html,Blank Page")];
  [ChromeEarlGrey waitForWebStateContainingText:"Blank Page"];

  GURL testURL = self.testServer->GetURL("/");
  std::string text = GetURLWithoutScheme(testURL);

  // Type the URL in the omnibox.
  [ChromeEarlGreyUI typeTextInOmnibox:text andPressEnter:YES];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTPS_RESPONSE"];
  [self assertSuccessfulUpgrade:1];

  // Load an interim data URL to clear the "HTTP_RESPONSE" text.
  [ChromeEarlGrey loadURL:GURL("data:text/html,Blank Page")];
  [ChromeEarlGrey waitForWebStateContainingText:"Blank Page"];

  // Type again. Normally, Omnibox should remember the successful HTTPS
  // navigation and not attempt to upgrade again. We are using a faux-HTTPS
  // server in tests which serves an http:// URL, so it will get upgraded again.
  [ChromeEarlGreyUI typeTextInOmnibox:text andPressEnter:YES];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTPS_RESPONSE"];
  [self assertSuccessfulUpgrade:2];

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

// Type an HTTP URL without scheme. The navigation should be upgraded to HTTPS,
// but the HTTPS URL serves bad response. The navigation should fall back to
// HTTP.
- (void)test_HTTPWithBadHTTPS_ShouldFallback {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.badHTTPSServer->port()
                                      useFakeHTTPS:false];
  [HttpsUpgradeAppInterface
      setFallbackHttpPortForTesting:self.testServer->port()];

  // Go to a web page to have a normal location bar.
  [ChromeEarlGrey loadURL:GURL("data:text/html,Blank Page")];
  [ChromeEarlGrey waitForWebStateContainingText:"Blank Page"];

  // Type the URL in the omnibox.
  GURL testURL = self.testServer->GetURL("/");
  std::string text = GetURLWithoutScheme(testURL);

  // Navigation should upgrade but eventually load the HTTP URL.
  [ChromeEarlGreyUI typeTextInOmnibox:text andPressEnter:YES];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  [self assertFailedUpgrade:1];

  // Load an interim data URL to clear the "HTTP_RESPONSE" text.
  [ChromeEarlGrey loadURL:GURL("data:text/html,Blank Page")];
  [ChromeEarlGrey waitForWebStateContainingText:"Blank Page"];

  // Try again. This time the omnibox will find a history match for the http
  // URL and navigate directly to it. Histograms shouldn't change.
  // TODO(crbug.com/40165447): We should try the https URL after a certain
  // time has passed.
  [ChromeEarlGreyUI typeTextInOmnibox:text andPressEnter:YES];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  [self assertFailedUpgrade:1];
}

// Type an HTTP URL without scheme. The navigation should be upgraded to HTTPS,
// but the HTTPS URL serves a slow loading response. The upgrade should timeout
// and the navigation should fall back to HTTP.
- (void)test_HTTPWithSlowHTTPS_ShouldFallBack {
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.slowServer->port()
                                      useFakeHTTPS:true];
  [HttpsUpgradeAppInterface
      setFallbackHttpPortForTesting:self.testServer->port()];
  // Set the fallback delay to zero. This will immediately stop the HTTPS
  // upgrade attempt.
  [HttpsUpgradeAppInterface setFallbackDelayForTesting:0];

  // Go to a web page to have a normal location bar.
  [ChromeEarlGrey loadURL:GURL("data:text/html,Blank Page")];
  [ChromeEarlGrey waitForWebStateContainingText:"Blank Page"];

  // Type the URL in the omnibox with an \n at the end.
  GURL testURL = self.testServer->GetURL("/");
  std::string text = GetURLWithoutScheme(testURL);

  // Navigation should upgrade but eventually load the HTTP URL due to slow
  // HTTPS.
  [ChromeEarlGreyUI typeTextInOmnibox:text andPressEnter:YES];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  [self assertTimedOutUpgrade:1];

  // Load an interim data URL to clear the "HTTP_RESPONSE" text.
  [ChromeEarlGrey loadURL:GURL("data:text/html,Blank Page")];
  [ChromeEarlGrey waitForWebStateContainingText:"Blank Page"];

  // Try again. This time the omnibox will find a history match for the http
  // URL and navigate directly to it. Histograms shouldn't change.
  // TODO(crbug.com/40165447): We should try the https URL after a certain
  // time has passed.
  [ChromeEarlGreyUI typeTextInOmnibox:text andPressEnter:YES];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  [self assertTimedOutUpgrade:1];
}

// Regression test for crbug.com/1379605: This test checks that the fallback
// logic can handle redirects from HTTPS to HTTP. The test steps are:
// 1. Type a hostname. This loads an HTTPS URL that redirects to a slow loading
//    HTTP URL.
// 2. Navigation upgrade times out while on the HTTP URL. This initiates a
//    fallback navigation using the HTTP version of the URL in step 1.
// 3. The fallback HTTP URL immediately responds without redirecting, even
//    though it has a ?redirect parameter.
// Disabled due to crbug.com/1469971
- (void)DISABLED_test_HTTPSRedirectsToSlowHTTP_ShouldFallback {
  // Use the faux good HTTPS server and standard HTTP server for the test.
  [HttpsUpgradeAppInterface setHTTPSPortForTesting:self.goodHTTPSServer->port()
                                      useFakeHTTPS:true];
  [HttpsUpgradeAppInterface
      setFallbackHttpPortForTesting:self.testServer->port()];

  // Set the fallback delay to zero. This will immediately stop the HTTPS
  // upgrade attempt.
  [HttpsUpgradeAppInterface setFallbackDelayForTesting:0];

  // Go to a web page to have a normal location bar.
  [ChromeEarlGrey loadURL:GURL("data:text/html,Blank Page")];
  [ChromeEarlGrey waitForWebStateContainingText:"Blank Page"];

  GURL slowHTTPURL = self.slowServer->GetURL("/");
  // Upgraded HTTPS URL is a redirect to the slow HTTP URL.
  GURL upgradedURL =
      self.goodHTTPSServer->GetURL("/?redirect=" + slowHTTPURL.spec());
  std::string text = GetURLWithoutScheme(upgradedURL);

  // Type the URL in the omnibox without a scheme and navigate.
  // Navigation will upgrade to HTTPS, then redirect to slow HTTP, then
  // timeout, then fallback to normal HTTP.
  // The fallback HTTP URL will immediately show a response.
  [ChromeEarlGreyUI typeTextInOmnibox:text andPressEnter:YES];
  [ChromeEarlGrey waitForWebStateContainingText:"HTTP_RESPONSE"];
  [self assertTimedOutUpgrade:1];
}

@end