chromium/ios/components/security_interstitials/safe_browsing/safe_browsing_query_manager_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/safe_browsing/safe_browsing_query_manager.h"

#import <Foundation/Foundation.h>

#import "base/test/scoped_feature_list.h"
#import "components/safe_browsing/core/common/features.h"
#import "components/security_interstitials/core/unsafe_resource.h"
#import "ios/components/security_interstitials/safe_browsing/fake_safe_browsing_client.h"
#import "ios/components/security_interstitials/safe_browsing/fake_safe_browsing_service.h"
#import "ios/web/public/test/fakes/fake_browser_state.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/test/web_task_environment.h"
#import "services/network/public/mojom/fetch_api.mojom.h"
#import "testing/gmock/include/gmock/gmock.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"

using security_interstitials::UnsafeResource;
using testing::_;

namespace {
// Mock observer for tests.
class MockQueryManagerObserver : public SafeBrowsingQueryManager::Observer {
 public:
  MockQueryManagerObserver() {}
  ~MockQueryManagerObserver() override {}

  MOCK_METHOD4(SafeBrowsingQueryFinished,
               void(SafeBrowsingQueryManager*,
                    const SafeBrowsingQueryManager::Query&,
                    const SafeBrowsingQueryManager::Result&,
                    safe_browsing::SafeBrowsingUrlCheckerImpl::PerformedCheck
                        performed_check));

  MOCK_METHOD1(SafeBrowsingSyncQueryFinished,
               void(const SafeBrowsingQueryManager::QueryData&));

  MOCK_METHOD1(SafeBrowsingAsyncQueryFinished,
               void(const SafeBrowsingQueryManager::QueryData&));

  // Override rather than mocking so that the observer can remove itself.
  void SafeBrowsingQueryManagerDestroyed(
      SafeBrowsingQueryManager* manager) override {
    manager_destroyed_ = true;
    manager->RemoveObserver(this);
  }
  bool manager_destroyed() const { return manager_destroyed_; }

 private:
  bool manager_destroyed_ = false;
};

// Verifies the expected values passed to the SafeBrowsingQueryFinished()
// callback.
ACTION_P4(VerifyQueryFinished,
          expected_url,
          expected_http_method,
          is_url_safe) {
  const SafeBrowsingQueryManager::Query& query = arg1;
  EXPECT_EQ(expected_url, query.url);
  EXPECT_EQ(expected_http_method, query.http_method);

  const SafeBrowsingQueryManager::Result& result = arg2;
  EXPECT_EQ(is_url_safe, result.proceed);
  EXPECT_EQ(is_url_safe, !result.show_error_page);
  if (is_url_safe) {
    EXPECT_FALSE(result.resource);
  } else {
    ASSERT_TRUE(result.resource);
    UnsafeResource resource = result.resource.value();
    EXPECT_EQ(expected_url, resource.url);
    EXPECT_NE(safe_browsing::SBThreatType::SB_THREAT_TYPE_SAFE,
              resource.threat_type);
  }
}

// Verifies the expected values passed to the SafeBrowsingSyncQueryFinished
// callback.
ACTION_P4(VerifySyncQueryFinished,
          expected_url,
          expected_http_method,
          is_url_sync_safe,
          is_url_async_safe) {
  const SafeBrowsingQueryManager::QueryData& query_data = arg0;
  const SafeBrowsingQueryManager::Query& query = query_data.query;
  EXPECT_EQ(expected_url, query.url);
  EXPECT_EQ(expected_http_method, query.http_method);

  const SafeBrowsingQueryManager::Result& result = query_data.result;
  if (is_url_sync_safe && is_url_async_safe) {
    EXPECT_FALSE(result.resource);
    EXPECT_TRUE(result.proceed);
    EXPECT_FALSE(result.show_error_page);
  } else if (!is_url_sync_safe) {
    EXPECT_FALSE(result.proceed);
    EXPECT_TRUE(result.show_error_page);
    ASSERT_TRUE(result.resource);
    UnsafeResource resource = result.resource.value();
    EXPECT_EQ(expected_url, resource.url);
    EXPECT_NE(safe_browsing::SBThreatType::SB_THREAT_TYPE_SAFE,
              resource.threat_type);
  }
}

// Verifies the expected values passed to the SafeBrowsingAsyncQueryFinished
// callback.
ACTION_P4(VerifyAsyncQueryFinished,
          expected_url,
          expected_http_method,
          is_url_sync_safe,
          is_url_async_safe) {
  const SafeBrowsingQueryManager::QueryData& query_data = arg0;
  const SafeBrowsingQueryManager::Query& query = query_data.query;
  EXPECT_EQ(expected_url, query.url);
  EXPECT_EQ(expected_http_method, query.http_method);

  const SafeBrowsingQueryManager::Result& result = query_data.result;
  if (is_url_sync_safe && is_url_async_safe) {
    EXPECT_FALSE(result.resource);
    EXPECT_TRUE(result.proceed);
    EXPECT_FALSE(result.show_error_page);
  } else if (!is_url_async_safe) {
    EXPECT_FALSE(result.proceed);
    EXPECT_TRUE(result.show_error_page);
    ASSERT_TRUE(result.resource);
    UnsafeResource resource = result.resource.value();
    EXPECT_EQ(expected_url, resource.url);
    EXPECT_NE(safe_browsing::SBThreatType::SB_THREAT_TYPE_SAFE,
              resource.threat_type);
  }
}
}  // namespace

class SafeBrowsingQueryManagerTest : public PlatformTest {
 protected:
  SafeBrowsingQueryManagerTest()
      : browser_state_(new web::FakeBrowserState()),
        web_state_(std::make_unique<web::FakeWebState>()),
        http_method_("GET") {
    SafeBrowsingQueryManager::CreateForWebState(web_state_.get(), &client_);
    manager()->AddObserver(&observer_);
    web_state_->SetBrowserState(browser_state_.get());
  }

  SafeBrowsingQueryManager* manager() {
    return SafeBrowsingQueryManager::FromWebState(web_state_.get());
  }

  web::WebTaskEnvironment task_environment_{
      web::WebTaskEnvironment::MainThreadType::IO};
  MockQueryManagerObserver observer_;
  std::unique_ptr<web::FakeBrowserState> browser_state_;
  std::unique_ptr<web::FakeWebState> web_state_;
  std::string http_method_;
  FakeSafeBrowsingClient client_;
};

// Tests a query for a safe URL.
TEST_F(SafeBrowsingQueryManagerTest, SafeURLQuery) {
  GURL url("http://chromium.test");
  EXPECT_CALL(observer_, SafeBrowsingQueryFinished(manager(), _, _, _))
      .WillOnce(VerifyQueryFinished(url, http_method_,
                                    /*is_url_safe=*/true));

  // Start a URL check query for the safe URL and run the runloop until the
  // result is received.
  manager()->StartQuery(SafeBrowsingQueryManager::Query(url, http_method_));
  base::RunLoop().RunUntilIdle();
}

// Tests a query for a safe URL completes properly with async check logic.
TEST_F(SafeBrowsingQueryManagerTest, SafeURLQueryWithAsyncRealTimeCheck) {
  base::test::ScopedFeatureList scoped_feature_list_;
  scoped_feature_list_.InitAndEnableFeature(
      safe_browsing::kSafeBrowsingAsyncRealTimeCheck);
  GURL url("http://chromium.test");
  EXPECT_CALL(observer_, SafeBrowsingSyncQueryFinished(_))
      .WillOnce(VerifySyncQueryFinished(url, http_method_,
                                        /*is_url_sync_safe=*/true,
                                        /*is_url_async_safe=*/true));
  EXPECT_CALL(observer_, SafeBrowsingAsyncQueryFinished(_))
      .WillOnce(VerifyAsyncQueryFinished(url, http_method_,
                                         /*is_url_sync_safe=*/true,
                                         /*is_url_async_safe=*/true));

  // Start a URL check query for the safe URL and run the runloop until the
  // result is received.
  manager()->StartQuery(SafeBrowsingQueryManager::Query(url, http_method_));
  base::RunLoop().RunUntilIdle();
}

// Tests a query for an unsafe URL.
TEST_F(SafeBrowsingQueryManagerTest, UnsafeURLQuery) {
  GURL url("http://" + FakeSafeBrowsingService::kUnsafeHost);
  EXPECT_CALL(observer_, SafeBrowsingQueryFinished(manager(), _, _, _))
      .WillOnce(VerifyQueryFinished(url, http_method_,
                                    /*is_url_safe=*/false));

  // Start a URL check query for the unsafe URL and run the runloop until the
  // result is received.  An UnsafeResource is stored before the query finishes
  // to simulate the production behavior that adds a resource that will be used
  // to populate the error page.
  manager()->StartQuery(SafeBrowsingQueryManager::Query(url, http_method_));
  UnsafeResource resource;
  resource.url = url;
  resource.threat_type =
      safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_PHISHING;
  manager()->StoreUnsafeResource(resource);
  base::RunLoop().RunUntilIdle();
}

// Tests a query for an unsafe URL with async checks enabled, where the URL
// is unsafe with both sync and async checks.
TEST_F(SafeBrowsingQueryManagerTest, SyncAndAsyncUnsafeURLQuery) {
  base::test::ScopedFeatureList scoped_feature_list_;
  scoped_feature_list_.InitAndEnableFeature(
      safe_browsing::kSafeBrowsingAsyncRealTimeCheck);
  GURL url("http://" + FakeSafeBrowsingService::kUnsafeHost);
  EXPECT_CALL(observer_, SafeBrowsingSyncQueryFinished(_))
      .WillOnce(VerifySyncQueryFinished(url, http_method_,
                                        /*is_url_sync_safe=*/false,
                                        /*is_url_async_safe=*/false));
  EXPECT_CALL(observer_, SafeBrowsingAsyncQueryFinished(_))
      .WillOnce(VerifyAsyncQueryFinished(url, http_method_,
                                         /*is_url_sync_safe=*/false,
                                         /*is_url_async_safe=*/false));

  // Start a URL check query for the unsafe URL and run the runloop until the
  // results are received.  An UnsafeResource is stored before the query
  // finishes to simulate the production behavior that adds a resource that will
  // be used to populate the error page.
  manager()->StartQuery(SafeBrowsingQueryManager::Query(url, http_method_));
  UnsafeResource resource;
  resource.url = url;
  resource.threat_type =
      safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_PHISHING;
  manager()->StoreUnsafeResource(resource);
  base::RunLoop().RunUntilIdle();
}

// Tests a query for an unsafe URL with async checks enabled, where the URL
// is unsafe with async checks only.
TEST_F(SafeBrowsingQueryManagerTest, AsyncUnsafeURLQuery) {
  base::test::ScopedFeatureList scoped_feature_list_;
  scoped_feature_list_.InitAndEnableFeature(
      safe_browsing::kSafeBrowsingAsyncRealTimeCheck);
  GURL url("http://" + FakeSafeBrowsingService::kAsyncUnsafeHost);
  EXPECT_CALL(observer_, SafeBrowsingSyncQueryFinished(_))
      .WillOnce(VerifySyncQueryFinished(url, http_method_,
                                        /*is_url_sync_safe=*/true,
                                        /*is_url_async_safe=*/false));
  EXPECT_CALL(observer_, SafeBrowsingAsyncQueryFinished(_))
      .WillOnce(VerifyAsyncQueryFinished(url, http_method_,
                                         /*is_url_sync_safe=*/true,
                                         /*is_url_async_safe=*/false));

  // Start a URL check query for the unsafe URL and run the runloop until the
  // results are received.  An UnsafeResource is stored before the query
  // finishes to simulate the production behavior that adds a resource that will
  // be used to populate the error page.
  manager()->StartQuery(SafeBrowsingQueryManager::Query(url, http_method_));
  UnsafeResource resource;
  resource.url = url;
  resource.threat_type =
      safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_PHISHING;
  manager()->StoreUnsafeResource(resource);
  base::RunLoop().RunUntilIdle();
}

// Tests that back-to-back queries for the same unsafe URL correctly sets an
// UnsafeResource on both queries.
TEST_F(SafeBrowsingQueryManagerTest, MultipleUnsafeURLQueries) {
  GURL url("http://" + FakeSafeBrowsingService::kUnsafeHost);
  EXPECT_CALL(observer_, SafeBrowsingQueryFinished(manager(), _, _, _))
      .Times(2)
      .WillRepeatedly(VerifyQueryFinished(url, http_method_,
                                          /*is_url_safe=*/false));

  // Start a URL check query for the unsafe URL and run the runloop until the
  // result is received.  An UnsafeResource is stored before the query finishes
  // to simulate the production behavior that adds a resource that will be used
  // to populate the error page.
  manager()->StartQuery(SafeBrowsingQueryManager::Query(url, http_method_));
  manager()->StartQuery(SafeBrowsingQueryManager::Query(url, http_method_));
  UnsafeResource resource;
  resource.url = url;
  resource.threat_type =
      safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_PHISHING;
  manager()->StoreUnsafeResource(resource);
  manager()->StoreUnsafeResource(resource);
  base::RunLoop().RunUntilIdle();
}

// Tests that StoreUnsafeResource associates the UnsafeResource with all
// queries that match the UnsafeResource's URL.
TEST_F(SafeBrowsingQueryManagerTest, StoreUnsafeResourceMultipleQueries) {
  GURL url("http://" + FakeSafeBrowsingService::kUnsafeHost);
  EXPECT_CALL(observer_, SafeBrowsingQueryFinished(manager(), _, _, _))
      .Times(2)
      .WillRepeatedly(VerifyQueryFinished(url, http_method_,
                                          /*is_url_safe=*/false));

  // Start two URL check queries for the unsafe URL and run the runloop until
  // the results are received. Only call StoreUnsafeResource once, rather than
  // once for each query.
  manager()->StartQuery(SafeBrowsingQueryManager::Query(url, http_method_));
  manager()->StartQuery(SafeBrowsingQueryManager::Query(url, http_method_));
  UnsafeResource resource;
  resource.url = url;
  resource.threat_type =
      safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_PHISHING;
  manager()->StoreUnsafeResource(resource);
  base::RunLoop().RunUntilIdle();
}

// Tests observer callbacks for manager destruction.
TEST_F(SafeBrowsingQueryManagerTest, ManagerDestruction) {
  web_state_ = nullptr;
  EXPECT_TRUE(observer_.manager_destroyed());
}

namespace {
// An observer that owns a WebState and destroys it when it gets a
// `SafeBrowsingQueryFinished` callback.
class WebStateDestroyingQueryManagerObserver
    : public SafeBrowsingQueryManager::Observer {
 public:
  WebStateDestroyingQueryManagerObserver()
      : browser_state_(new web::FakeBrowserState()),
        web_state_(std::make_unique<web::FakeWebState>()) {
    web_state_->SetBrowserState(browser_state_.get());
  }
  ~WebStateDestroyingQueryManagerObserver() override {}

  void SafeBrowsingQueryFinished(
      SafeBrowsingQueryManager* query_manager,
      const SafeBrowsingQueryManager::Query& query,
      const SafeBrowsingQueryManager::Result& result,
      safe_browsing::SafeBrowsingUrlCheckerImpl::PerformedCheck performed_check)
      override {
    web_state_.reset();
  }

  void SafeBrowsingQueryManagerDestroyed(
      SafeBrowsingQueryManager* manager) override {
    manager->RemoveObserver(this);
  }

  web::WebState* web_state() { return web_state_.get(); }

 private:
  std::unique_ptr<web::FakeBrowserState> browser_state_;
  std::unique_ptr<web::FakeWebState> web_state_;
};
}  // namespace

// Test fixture for testing WebState destruction during a
// SafeBrowsingQueryManager::Observer callback.
class SafeBrowsingQueryManagerWebStateDestructionTest : public PlatformTest {
 protected:
  SafeBrowsingQueryManagerWebStateDestructionTest() : http_method_("GET") {
    SafeBrowsingQueryManager::CreateForWebState(observer_.web_state(),
                                                &client_);
    manager()->AddObserver(&observer_);
  }

  SafeBrowsingQueryManager* manager() {
    return SafeBrowsingQueryManager::FromWebState(observer_.web_state());
  }

  web::WebTaskEnvironment task_environment_{
      web::WebTaskEnvironment::MainThreadType::IO};
  WebStateDestroyingQueryManagerObserver observer_;
  std::string http_method_;
  FakeSafeBrowsingClient client_;
};

// Tests that a query for a safe URL doesn't cause a crash.
TEST_F(SafeBrowsingQueryManagerWebStateDestructionTest, SafeURLQuery) {
  GURL url("http://chromium.test");
  // Start a URL check query for the safe URL and run the runloop until the
  // result is received.
  manager()->StartQuery(SafeBrowsingQueryManager::Query(url, http_method_));
  base::RunLoop().RunUntilIdle();
}

// Tests that a query for an unsafe URL doesn't cause a crash.
TEST_F(SafeBrowsingQueryManagerWebStateDestructionTest, UnsafeURLQuery) {
  GURL url("http://" + FakeSafeBrowsingService::kUnsafeHost);

  // Start a URL check query for the unsafe URL and run the runloop until the
  // result is received. An UnsafeResource is stored before the query finishes
  // to simulate the production behavior that adds a resource that will be used
  // to populate the error page.
  manager()->StartQuery(SafeBrowsingQueryManager::Query(url, http_method_));
  UnsafeResource resource;
  resource.url = url;
  resource.threat_type =
      safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_PHISHING;
  manager()->StoreUnsafeResource(resource);
  base::RunLoop().RunUntilIdle();
}