chromium/ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper_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/components/security_interstitials/lookalikes/lookalike_url_tab_helper.h"

#import "base/memory/raw_ptr.h"
#import "base/test/metrics/histogram_tester.h"
#import "components/lookalikes/core/safety_tip_test_utils.h"
#import "ios/components/security_interstitials/lookalikes/lookalike_url_container.h"
#import "ios/components/security_interstitials/lookalikes/lookalike_url_tab_allow_list.h"
#import "ios/web/public/navigation/web_state_policy_decider.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "net/base/apple/url_conversions.h"
#import "testing/platform_test.h"

class LookalikeUrlTabHelperTest : public PlatformTest {
 protected:
  LookalikeUrlTabHelperTest() {
    LookalikeUrlTabHelper::CreateForWebState(&web_state_);
    LookalikeUrlTabAllowList::CreateForWebState(&web_state_);
    LookalikeUrlContainer::CreateForWebState(&web_state_);
    allow_list_ = LookalikeUrlTabAllowList::FromWebState(&web_state_);
  }

  // Helper function that calls into WebState::ShouldAllowResponse with the
  // given `url` and `for_main_frame`, waits for the callback with the decision
  // to be called, and returns the decision.
  web::WebStatePolicyDecider::PolicyDecision ShouldAllowResponseUrl(
      const GURL& url,
      bool for_main_frame) {
    NSURLResponse* response =
        [[NSURLResponse alloc] initWithURL:net::NSURLWithGURL(url)
                                  MIMEType:@"text/html"
                     expectedContentLength:0
                          textEncodingName:nil];
    __block bool callback_called = false;
    __block web::WebStatePolicyDecider::PolicyDecision policy_decision =
        web::WebStatePolicyDecider::PolicyDecision::Allow();
    auto callback =
        base::BindOnce(^(web::WebStatePolicyDecider::PolicyDecision decision) {
          policy_decision = decision;
          callback_called = true;
        });
    web::WebStatePolicyDecider::ResponseInfo response_info(for_main_frame);
    web_state_.ShouldAllowResponse(response, response_info,
                                   std::move(callback));
    EXPECT_TRUE(callback_called);
    return policy_decision;
  }

  LookalikeUrlTabAllowList* allow_list() { return allow_list_; }

  base::HistogramTester histogram_tester_;
  web::FakeWebState web_state_;

 private:
  raw_ptr<LookalikeUrlTabAllowList> allow_list_;
};

// Tests that ShouldAllowResponse properly blocks lookalike navigations and
// allows subframe navigations, non-HTTP/S navigations, and navigations
// to allowed domains. ShouldAllowRequest should always allow the navigation.
// Also tests that UMA records correctly.
TEST_F(LookalikeUrlTabHelperTest, ShouldAllowResponse) {
  GURL lookalike_url("https://xn--googl-fsa.com/");
  lookalikes::InitializeSafetyTipConfig();

  // Lookalike IDNs should be blocked.
  EXPECT_FALSE(ShouldAllowResponseUrl(lookalike_url, /*main_frame=*/true)
                   .ShouldAllowNavigation());
  histogram_tester_.ExpectUniqueSample(
      lookalikes::kInterstitialHistogramName,
      static_cast<base::HistogramBase::Sample>(
          lookalikes::NavigationSuggestionEvent::kMatchSkeletonTop500),
      1);

  // Non-main frame navigations should be allowed.
  EXPECT_TRUE(ShouldAllowResponseUrl(lookalike_url, /*main_frame=*/false)
                  .ShouldAllowNavigation());

  // Non-HTTP/S navigations should be allowed.
  GURL file_url("file://xn--googl-fsa.com/");
  EXPECT_TRUE(ShouldAllowResponseUrl(file_url, /*main_frame=*/true)
                  .ShouldAllowNavigation());

  // Lookalike IDNs that have been allowlisted should not be blocked.
  allow_list()->AllowDomain("xn--googl-fsa.com");
  EXPECT_TRUE(ShouldAllowResponseUrl(lookalike_url, /*main_frame=*/true)
                  .ShouldAllowNavigation());

  histogram_tester_.ExpectTotalCount(lookalikes::kInterstitialHistogramName, 1);
}

// Tests that ShouldAllowResponse properly allows lookalike navigations
// when the domain has been allowlisted by the Safety Tips component.
TEST_F(LookalikeUrlTabHelperTest, ShouldAllowResponseForAllowlistedDomains) {
  GURL lookalike_url("https://xn--googl-fsa.com/");
  lookalikes::InitializeSafetyTipConfig();
  lookalikes::SetSafetyTipAllowlistPatterns({"xn--googl-fsa.com/"}, {}, {});

  EXPECT_TRUE(ShouldAllowResponseUrl(lookalike_url, /*main_frame=*/true)
                  .ShouldAllowNavigation());
}

// Tests that ShouldAllowResponse properly blocks lookalike navigations
// to IDNs.
TEST_F(LookalikeUrlTabHelperTest, ShouldAllowResponseForPunycode) {
  GURL lookalike_url("https://ɴoτ-τoρ-ďoᛖaiɴ.com/");
  lookalikes::InitializeSafetyTipConfig();

  EXPECT_FALSE(ShouldAllowResponseUrl(lookalike_url, /*main_frame=*/true)
                   .ShouldAllowNavigation());
  std::unique_ptr<LookalikeUrlContainer::LookalikeUrlInfo> lookalike_url_info =
      LookalikeUrlContainer::FromWebState(&web_state_)
          ->ReleaseLookalikeUrlInfo();
  EXPECT_TRUE(lookalike_url_info.get());
}