// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/safe_browsing/verdict_cache_manager.h"

#include "base/base64.h"
#include "base/memory/scoped_refptr.h"
#include "base/values.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/safe_browsing/proto/csd.pb.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace safe_browsing {

class VerdictCacheManagerTest : public ::testing::Test {
 public:
  VerdictCacheManagerTest() {}

  void SetUp() override {
    HostContentSettingsMap::RegisterProfilePrefs(test_pref_service_.registry());
    content_setting_map_ = new HostContentSettingsMap(
        &test_pref_service_, false /* is_off_the_record */,
        false /* store_last_modified */,
        false /* migrate_requesting_and_top_level_origin_settings */);
    cache_manager_ = std::make_unique<VerdictCacheManager>(
        nullptr, content_setting_map_.get());
  }

  void TearDown() override {
    cache_manager_.reset();
    content_setting_map_->ShutdownOnUIThread();
  }

  void CachePhishGuardVerdict(
      const GURL& url,
      LoginReputationClientRequest::TriggerType trigger,
      PasswordType password_type,
      LoginReputationClientResponse::VerdictType verdict,
      int cache_duration_sec,
      const std::string& cache_expression,
      const base::Time& verdict_received_time) {
    ASSERT_FALSE(cache_expression.empty());
    LoginReputationClientResponse response;
    response.set_verdict_type(verdict);
    response.set_cache_expression(cache_expression);
    response.set_cache_duration_sec(cache_duration_sec);
    cache_manager_->CachePhishGuardVerdict(url, trigger, password_type,
                                           response, verdict_received_time);
  }

 protected:
  std::unique_ptr<VerdictCacheManager> cache_manager_;
  scoped_refptr<HostContentSettingsMap> content_setting_map_;

 private:
  content::TestBrowserThreadBundle thread_bundle_;
  sync_preferences::TestingPrefServiceSyncable test_pref_service_;
};

TEST_F(VerdictCacheManagerTest, TestCanRetrieveCachedVerdict) {
  GURL url("https://www.google.com/");

  LoginReputationClientResponse cached_verdict;
  cached_verdict.set_cache_expression("www.google.com/");
  EXPECT_EQ(LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED,
            cache_manager_->GetCachedPhishGuardVerdict(
                url, LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                PasswordType::PRIMARY_ACCOUNT_PASSWORD, &cached_verdict));

  CachePhishGuardVerdict(url,
                         LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                         PasswordType::PRIMARY_ACCOUNT_PASSWORD,
                         LoginReputationClientResponse::SAFE, 60,
                         "www.google.com/", base::Time::Now());

  EXPECT_EQ(LoginReputationClientResponse::SAFE,
            cache_manager_->GetCachedPhishGuardVerdict(
                url, LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                PasswordType::PRIMARY_ACCOUNT_PASSWORD, &cached_verdict));
}

TEST_F(VerdictCacheManagerTest, TestCacheSplitByTriggerType) {
  GURL url("https://www.google.com/");

  LoginReputationClientResponse cached_verdict;
  cached_verdict.set_cache_expression("www.google.com/");
  EXPECT_EQ(LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED,
            cache_manager_->GetCachedPhishGuardVerdict(
                url, LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                PasswordType::PRIMARY_ACCOUNT_PASSWORD, &cached_verdict));

  CachePhishGuardVerdict(url,
                         LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
                         PasswordType::PRIMARY_ACCOUNT_PASSWORD,
                         LoginReputationClientResponse::SAFE, 60,
                         "www.google.com/", base::Time::Now());

  EXPECT_EQ(LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED,
            cache_manager_->GetCachedPhishGuardVerdict(
                url, LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                PasswordType::PRIMARY_ACCOUNT_PASSWORD, &cached_verdict));
}

TEST_F(VerdictCacheManagerTest, TestCacheSplitByPasswordType) {
  GURL url("https://www.google.com/");

  LoginReputationClientResponse cached_verdict;
  cached_verdict.set_cache_expression("www.google.com/");
  EXPECT_EQ(LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED,
            cache_manager_->GetCachedPhishGuardVerdict(
                url, LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                PasswordType::PRIMARY_ACCOUNT_PASSWORD, &cached_verdict));

  CachePhishGuardVerdict(
      url, LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
      PasswordType::ENTERPRISE_PASSWORD, LoginReputationClientResponse::SAFE,
      60, "www.google.com/", base::Time::Now());

  EXPECT_EQ(LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED,
            cache_manager_->GetCachedPhishGuardVerdict(
                url, LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                PasswordType::PRIMARY_ACCOUNT_PASSWORD, &cached_verdict));
}

TEST_F(VerdictCacheManagerTest, TestGetStoredPhishGuardVerdictCount) {
  GURL url("https://www.google.com/");

  LoginReputationClientResponse cached_verdict;
  cached_verdict.set_cache_expression("www.google.com/");
  EXPECT_EQ(0u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));

  CachePhishGuardVerdict(
      url, LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
      PasswordType::ENTERPRISE_PASSWORD, LoginReputationClientResponse::SAFE,
      60, "www.google.com/", base::Time::Now());

  EXPECT_EQ(1u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));

  CachePhishGuardVerdict(
      url, LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
      PasswordType::ENTERPRISE_PASSWORD, LoginReputationClientResponse::SAFE,
      60, "www.google.com/", base::Time::Now());

  EXPECT_EQ(1u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));

  CachePhishGuardVerdict(
      url, LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
      PasswordType::ENTERPRISE_PASSWORD, LoginReputationClientResponse::SAFE,
      60, "www.google.com/path", base::Time::Now());

  EXPECT_EQ(2u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
}

TEST_F(VerdictCacheManagerTest, TestParseInvalidVerdictEntry) {
  // Directly save an invalid cache entry.
  LoginReputationClientResponse verdict;
  verdict.set_verdict_type(LoginReputationClientResponse::SAFE);
  verdict.set_cache_expression("www.google.com/");
  verdict.set_cache_duration_sec(60);

  std::string verdict_serialized;
  verdict.SerializeToString(&verdict_serialized);
  base::Base64Encode(verdict_serialized, &verdict_serialized);

  auto cache_dictionary = std::make_unique<base::DictionaryValue>();
  auto* verdict_dictionary =
      cache_dictionary->SetKey("2", base::Value(base::Value::Type::DICTIONARY));
  auto* verdict_entry = verdict_dictionary->SetKey(
      "www.google.com/", base::Value(base::Value::Type::DICTIONARY));
  verdict_entry->SetStringKey("cache_creation_time", "invalid_time");
  verdict_entry->SetStringKey("verdict_proto", verdict_serialized);

  content_setting_map_->SetWebsiteSettingDefaultScope(
      GURL("http://www.google.com/"), GURL(),
      CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, std::string(),
      std::move(cache_dictionary));

  LoginReputationClientResponse cached_verdict;
  EXPECT_EQ(LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED,
            cache_manager_->GetCachedPhishGuardVerdict(
                GURL("https://www.google.com/"),
                LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                PasswordType::PRIMARY_ACCOUNT_PASSWORD, &cached_verdict));
}

TEST_F(VerdictCacheManagerTest, TestRemoveCachedVerdictOnURLsDeleted) {
  ASSERT_EQ(0u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
  ASSERT_EQ(0u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));
  // Prepare 5 verdicts. Three are for origin "http://foo.com", and the others
  // are for "http://bar.com".
  base::Time now = base::Time::Now();
  CachePhishGuardVerdict(GURL("http://foo.com/abc/index.jsp"),
                         LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                         PasswordType::PRIMARY_ACCOUNT_PASSWORD,
                         LoginReputationClientResponse::LOW_REPUTATION, 600,
                         "foo.com/abc/", now);
  CachePhishGuardVerdict(GURL("http://foo.com/abc/index.jsp"),
                         LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                         PasswordType::ENTERPRISE_PASSWORD,
                         LoginReputationClientResponse::LOW_REPUTATION, 600,
                         "foo.com/abc/", now);
  CachePhishGuardVerdict(GURL("http://bar.com/index.jsp"),
                         LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                         PasswordType::PRIMARY_ACCOUNT_PASSWORD,
                         LoginReputationClientResponse::PHISHING, 600,
                         "bar.com", now);
  ASSERT_EQ(3u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));

  CachePhishGuardVerdict(GURL("http://foo.com/abc/index.jsp"),
                         LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
                         PasswordType::PASSWORD_TYPE_UNKNOWN,
                         LoginReputationClientResponse::LOW_REPUTATION, 600,
                         "foo.com/abc/", now);
  CachePhishGuardVerdict(GURL("http://bar.com/index.jsp"),
                         LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
                         PasswordType::PASSWORD_TYPE_UNKNOWN,
                         LoginReputationClientResponse::PHISHING, 600,
                         "bar.com", now);
  ASSERT_EQ(2u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));

  // Delete a bar.com URL. Corresponding content setting keyed on
  // origin "http://bar.com" should be removed,
  history::URLRows deleted_urls;
  deleted_urls.push_back(history::URLRow(GURL("http://bar.com")));

  // Delete an arbitrary data URL, to ensure the service is robust against
  // filtering only http/s URLs. See crbug.com/709758.
  deleted_urls.push_back(history::URLRow(GURL("data:text/html, <p>hellow")));

  cache_manager_->RemoveContentSettingsOnURLsDeleted(false /* all_history */,
                                                     deleted_urls);
  EXPECT_EQ(2u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
  EXPECT_EQ(1u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));

  LoginReputationClientResponse actual_verdict;
  EXPECT_EQ(LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED,
            cache_manager_->GetCachedPhishGuardVerdict(
                GURL("http://bar.com"),
                LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                PasswordType::PRIMARY_ACCOUNT_PASSWORD, &actual_verdict));
  EXPECT_EQ(LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED,
            cache_manager_->GetCachedPhishGuardVerdict(
                GURL("http://bar.com"),
                LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
                PasswordType::PASSWORD_TYPE_UNKNOWN, &actual_verdict));

  // If delete all history. All password protection content settings should be
  // gone.
  cache_manager_->RemoveContentSettingsOnURLsDeleted(true /* all_history */,
                                                     history::URLRows());
  EXPECT_EQ(0u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
  EXPECT_EQ(0u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));
}

TEST_F(VerdictCacheManagerTest, TestCleanUpExpiredVerdict) {
  // Prepare 4 verdicts for PASSWORD_REUSE_EVENT with SIGN_IN_PASSWORD type:
  // (1) "foo.com/abc/" valid
  // (2) "foo.com/def/" expired
  // (3) "bar.com/abc/" expired
  // (4) "bar.com/def/" expired
  base::Time now = base::Time::Now();
  CachePhishGuardVerdict(GURL("https://foo.com/abc/index.jsp"),
                         LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                         PasswordType::PRIMARY_ACCOUNT_PASSWORD,
                         LoginReputationClientResponse::LOW_REPUTATION, 600,
                         "foo.com/abc/", now);
  CachePhishGuardVerdict(GURL("https://foo.com/def/index.jsp"),
                         LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                         PasswordType::PRIMARY_ACCOUNT_PASSWORD,
                         LoginReputationClientResponse::LOW_REPUTATION, 0,
                         "foo.com/def/", now);
  CachePhishGuardVerdict(GURL("https://bar.com/abc/index.jsp"),
                         LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                         PasswordType::PRIMARY_ACCOUNT_PASSWORD,
                         LoginReputationClientResponse::PHISHING, 0,
                         "bar.com/abc/", now);
  CachePhishGuardVerdict(GURL("https://bar.com/def/index.jsp"),
                         LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                         PasswordType::PRIMARY_ACCOUNT_PASSWORD,
                         LoginReputationClientResponse::PHISHING, 0,
                         "bar.com/def/", now);
  ASSERT_EQ(4u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));

  // Prepare 2 verdicts for UNFAMILIAR_LOGIN_PAGE:
  // (1) "bar.com/def/" valid
  // (2) "bar.com/xyz/" expired
  CachePhishGuardVerdict(GURL("https://bar.com/def/index.jsp"),
                         LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
                         PasswordType::PASSWORD_TYPE_UNKNOWN,
                         LoginReputationClientResponse::SAFE, 600,
                         "bar.com/def/", now);
  CachePhishGuardVerdict(GURL("https://bar.com/xyz/index.jsp"),
                         LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
                         PasswordType::PASSWORD_TYPE_UNKNOWN,
                         LoginReputationClientResponse::PHISHING, 0,
                         "bar.com/xyz/", now);
  ASSERT_EQ(2u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));

  cache_manager_->CleanUpExpiredVerdicts();

  ASSERT_EQ(1u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::PASSWORD_REUSE_EVENT));
  ASSERT_EQ(1u, cache_manager_->GetStoredPhishGuardVerdictCount(
                    LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE));
  LoginReputationClientResponse actual_verdict;
  // Has cached PASSWORD_REUSE_EVENT verdict for foo.com/abc/.
  EXPECT_EQ(LoginReputationClientResponse::LOW_REPUTATION,
            cache_manager_->GetCachedPhishGuardVerdict(
                GURL("https://foo.com/abc/test.jsp"),
                LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                PasswordType::PRIMARY_ACCOUNT_PASSWORD, &actual_verdict));
  // No cached PASSWORD_REUSE_EVENT verdict for foo.com/def.
  EXPECT_EQ(LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED,
            cache_manager_->GetCachedPhishGuardVerdict(
                GURL("https://foo.com/def/index.jsp"),
                LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                PasswordType::PRIMARY_ACCOUNT_PASSWORD, &actual_verdict));
  // No cached PASSWORD_REUSE_EVENT verdict for bar.com/abc.
  EXPECT_EQ(LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED,
            cache_manager_->GetCachedPhishGuardVerdict(
                GURL("https://bar.com/abc/index.jsp"),
                LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                PasswordType::PRIMARY_ACCOUNT_PASSWORD, &actual_verdict));
  // No cached PASSWORD_REUSE_EVENT verdict for bar.com/def.
  EXPECT_EQ(LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED,
            cache_manager_->GetCachedPhishGuardVerdict(
                GURL("https://bar.com/def/index.jsp"),
                LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                PasswordType::PRIMARY_ACCOUNT_PASSWORD, &actual_verdict));

  // Has cached UNFAMILIAR_LOGIN_PAGE verdict for bar.com/def.
  EXPECT_EQ(LoginReputationClientResponse::SAFE,
            cache_manager_->GetCachedPhishGuardVerdict(
                GURL("https://bar.com/def/index.jsp"),
                LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
                PasswordType::PASSWORD_TYPE_UNKNOWN, &actual_verdict));

  // No cached UNFAMILIAR_LOGIN_PAGE verdict for bar.com/xyz.
  EXPECT_EQ(LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED,
            cache_manager_->GetCachedPhishGuardVerdict(
                GURL("https://bar.com/xyz/index.jsp"),
                LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE,
                PasswordType::PASSWORD_TYPE_UNKNOWN, &actual_verdict));
}

TEST_F(VerdictCacheManagerTest, TestCleanUpExpiredVerdictWithInvalidEntry) {
  // Directly save an invalid cache entry.
  LoginReputationClientResponse verdict;
  verdict.set_verdict_type(LoginReputationClientResponse::SAFE);
  verdict.set_cache_expression("www.google.com/");
  verdict.set_cache_duration_sec(60);

  std::string verdict_serialized;
  verdict.SerializeToString(&verdict_serialized);
  base::Base64Encode(verdict_serialized, &verdict_serialized);

  auto cache_dictionary = std::make_unique<base::DictionaryValue>();
  auto* verdict_dictionary =
      cache_dictionary->SetKey("1", base::Value(base::Value::Type::DICTIONARY));
  auto* verdict_entry = verdict_dictionary->SetKey(
      "www.google.com/path", base::Value(base::Value::Type::DICTIONARY));
  verdict_entry->SetStringKey("cache_creation_time", "invalid_time");
  verdict_entry->SetStringKey("verdict_proto", verdict_serialized);

  content_setting_map_->SetWebsiteSettingDefaultScope(
      GURL("http://www.google.com/"), GURL(),
      CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, std::string(),
      std::move(cache_dictionary));

  // Save one valid entry
  CachePhishGuardVerdict(GURL("https://www.google.com"),
                         LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                         PasswordType::PRIMARY_ACCOUNT_PASSWORD,
                         LoginReputationClientResponse::SAFE, 60,
                         "www.google.com/", base::Time::Now());

  // Verify we saved two entries under PasswordType PRIMARY_ACCOUNT_PASSWORD
  EXPECT_EQ(2U,
            content_setting_map_
                ->GetWebsiteSetting(GURL("http://www.google.com/"), GURL(),
                                    CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION,
                                    std::string(), nullptr)
                ->FindDictKey("1")
                ->DictSize());

  cache_manager_->CleanUpExpiredVerdicts();

  // One should have been cleaned up
  EXPECT_EQ(1U,
            content_setting_map_
                ->GetWebsiteSetting(GURL("http://www.google.com/"), GURL(),
                                    CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION,
                                    std::string(), nullptr)
                ->FindDictKey("1")
                ->DictSize());
}

}  // namespace safe_browsing
