chromium/ios/web_view/internal/safe_browsing/cwv_unsafe_url_handler_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/web_view/internal/safe_browsing/cwv_unsafe_url_handler_internal.h"

#import "base/functional/bind.h"
#import "base/functional/callback.h"
#import "base/functional/callback_helpers.h"
#import "components/safe_browsing/ios/browser/safe_browsing_url_allow_list.h"
#import "components/security_interstitials/core/unsafe_resource.h"
#import "ios/web/public/test/fakes/fake_navigation_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "net/base/apple/url_conversions.h"
#import "services/network/public/mojom/fetch_api.mojom.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"

namespace ios_web_view {

class CWVUnsafeURLHandlerTest : public PlatformTest {
 public:
  CWVUnsafeURLHandlerTest(const CWVUnsafeURLHandlerTest&) = delete;
  CWVUnsafeURLHandlerTest& operator=(const CWVUnsafeURLHandlerTest&) = delete;

 protected:
  CWVUnsafeURLHandlerTest() {
    auto navigation_manager = std::make_unique<web::FakeNavigationManager>();
    web_state_.SetNavigationManager(std::move(navigation_manager));

    SafeBrowsingUrlAllowList::CreateForWebState(&web_state_);
  }

  CWVUnsafeURLHandler* CreateHandler(
      const GURL& request_url,
      safe_browsing::SBThreatType threat_type,
      base::OnceCallback<void(NSString*)> callback) {
    security_interstitials::UnsafeResource unsafe_resource;
    unsafe_resource.url = request_url;
    unsafe_resource.threat_type = threat_type;
    unsafe_resource.weak_web_state = web_state_.GetWeakPtr();
    return [[CWVUnsafeURLHandler alloc] initWithWebState:&web_state_
                                          unsafeResource:unsafe_resource
                                            htmlCallback:std::move(callback)];
  }

  web::FakeNavigationManager* GetNavigationManager() {
    return static_cast<web::FakeNavigationManager*>(
        web_state_.GetNavigationManager());
  }

  web::FakeWebState web_state_;
};

// Checks that public API agrees with the internal UnsafeResource for unsafe
// loads.
TEST_F(CWVUnsafeURLHandlerTest, InitializationForUnsafeResource) {
  auto request_url = GURL("https://www.chromium.org");
  CWVUnsafeURLHandler* handler = CreateHandler(
      request_url, safe_browsing::SBThreatType::SB_THREAT_TYPE_BILLING,
      base::DoNothing());
  EXPECT_EQ(request_url, net::GURLWithNSURL(handler.mainFrameURL));
  EXPECT_EQ(request_url, net::GURLWithNSURL(handler.requestURL));
  EXPECT_EQ(CWVUnsafeURLThreatTypeBilling, handler.threatType);
}

// Tests that html callback is only invoked once.
TEST_F(CWVUnsafeURLHandlerTest, DisplayHTMLCallbackIsOnlyCalledOnce) {
  __block NSString* callback_html = nil;
  __block int callback_count = 0;
  base::OnceCallback callback = base::BindOnce(^(NSString* html) {
    ++callback_count;
    callback_html = html;
  });
  CWVUnsafeURLHandler* handler = CreateHandler(
      GURL(), safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_PHISHING,
      std::move(callback));

  [handler displayInterstitialPageWithHTML:@"foo"];
  EXPECT_NSEQ(@"foo", callback_html);
  EXPECT_EQ(1, callback_count);

  // Second call should be a no op.
  [handler displayInterstitialPageWithHTML:@"bar"];
  EXPECT_NSEQ(@"foo", callback_html);
  EXPECT_EQ(1, callback_count);
}

// Tests that proceeding will update allow list and reload the web state.
TEST_F(CWVUnsafeURLHandlerTest, ProceedingUpdatesAllowListAndReloadsWebState) {
  auto request_url = GURL("https://www.chromium.org");
  auto threat_type = safe_browsing::SBThreatType::SB_THREAT_TYPE_BILLING;
  CWVUnsafeURLHandler* handler =
      CreateHandler(request_url, threat_type, base::DoNothing());
  SafeBrowsingUrlAllowList* allow_list =
      SafeBrowsingUrlAllowList::FromWebState(&web_state_);
  allow_list->AddPendingUnsafeNavigationDecision(request_url, threat_type);

  [handler proceed];
  EXPECT_TRUE(GetNavigationManager()->ReloadWasCalled());
  EXPECT_TRUE(allow_list->AreUnsafeNavigationsAllowed(request_url));
  EXPECT_FALSE(allow_list->IsUnsafeNavigationDecisionPending(request_url));
}

// Tests that going back will close the web state.
TEST_F(CWVUnsafeURLHandlerTest, GoingBackClosesWebState) {
  auto request_url = GURL("https://www.chromium.org");
  auto threat_type = safe_browsing::SBThreatType::SB_THREAT_TYPE_BILLING;
  CWVUnsafeURLHandler* handler =
      CreateHandler(request_url, threat_type, base::DoNothing());
  SafeBrowsingUrlAllowList* allow_list =
      SafeBrowsingUrlAllowList::FromWebState(&web_state_);
  allow_list->AddPendingUnsafeNavigationDecision(request_url, threat_type);

  [handler goBack];
  EXPECT_FALSE(allow_list->AreUnsafeNavigationsAllowed(request_url));
  EXPECT_FALSE(allow_list->IsUnsafeNavigationDecisionPending(request_url));
  EXPECT_TRUE(web_state_.IsClosed());
}

// Tests that going back will navigate backwards if possible.
TEST_F(CWVUnsafeURLHandlerTest, GoingBackNavigatesBack) {
  GetNavigationManager()->AddItem(GURL("https://www.example.com"),
                                  ui::PAGE_TRANSITION_TYPED);
  GetNavigationManager()->AddItem(GURL("https://www.example2.com"),
                                  ui::PAGE_TRANSITION_TYPED);

  auto request_url = GURL("https://www.chromium.org");
  auto threat_type = safe_browsing::SBThreatType::SB_THREAT_TYPE_BILLING;
  SafeBrowsingUrlAllowList* allow_list =
      SafeBrowsingUrlAllowList::FromWebState(&web_state_);
  allow_list->AddPendingUnsafeNavigationDecision(request_url, threat_type);
  CWVUnsafeURLHandler* handler =
      CreateHandler(request_url, threat_type, base::DoNothing());
  EXPECT_TRUE(GetNavigationManager()->CanGoBack());
  int item_index = GetNavigationManager()->GetLastCommittedItemIndex();
  [handler goBack];
  EXPECT_FALSE(allow_list->IsUnsafeNavigationDecisionPending(request_url));
  EXPECT_EQ(item_index - 1,
            GetNavigationManager()->GetLastCommittedItemIndex());
}

// Tests that deallocation will remove a pending decision.
TEST_F(CWVUnsafeURLHandlerTest, DeallocationRemovesPendingDecision) {
  auto request_url = GURL("https://www.chromium.org");
  auto threat_type = safe_browsing::SBThreatType::SB_THREAT_TYPE_BILLING;
  SafeBrowsingUrlAllowList* allow_list =
      SafeBrowsingUrlAllowList::FromWebState(&web_state_);
  // Manually creating an autorelease pool will ensure handler is released
  // before the end of the test.
  @autoreleasepool {
    __unused CWVUnsafeURLHandler* handler =
        CreateHandler(request_url, threat_type, base::DoNothing());
    allow_list->AddPendingUnsafeNavigationDecision(request_url, threat_type);
    EXPECT_TRUE(allow_list->IsUnsafeNavigationDecisionPending(request_url));
  }
  EXPECT_FALSE(allow_list->IsUnsafeNavigationDecisionPending(request_url));
}

}  // namespace ios_web_view