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

#include "components/signin/core/browser/signin_header_helper.h"

#include <memory>
#include <string>

#include "base/command_line.h"
#include "base/memory/raw_ref.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "components/content_settings/core/browser/cookie_settings.h"
#include "components/prefs/pref_member.h"
#include "components/privacy_sandbox/privacy_sandbox_prefs.h"
#include "components/signin/core/browser/chrome_connected_header_helper.h"
#include "components/signin/public/base/account_consistency_method.h"
#include "components/signin/public/base/signin_buildflags.h"
#include "components/signin/public/identity_manager/tribool.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "google_apis/gaia/gaia_id.h"
#include "google_apis/gaia/gaia_urls.h"
#include "net/http/http_request_headers.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
#include "components/signin/core/browser/dice_header_helper.h"
#endif

namespace signin {
namespace {
constexpr char kTestDeviceId[] = "DeviceID";
constexpr char kTestSource[] = "TestSource";

class RequestAdapterWrapper {
 public:
  RequestAdapterWrapper(const GURL& url, const net::HttpRequestHeaders& headers)
      : adapter_(url,
                 headers,
                 &modified_request_headers_,
                 &to_be_removed_request_headers_),
        original_headers_(headers) {}

  RequestAdapter* adapter() { return &adapter_; }

  net::HttpRequestHeaders GetFinalHeaders() {
    net::HttpRequestHeaders final_headers(*original_headers_);
    final_headers.MergeFrom(modified_request_headers_);
    for (const std::string& name : to_be_removed_request_headers_) {
      final_headers.RemoveHeader(name);
    }
    return final_headers;
  }

 private:
  RequestAdapter adapter_;
  const raw_ref<const net::HttpRequestHeaders> original_headers_;
  net::HttpRequestHeaders modified_request_headers_;
  std::vector<std::string> to_be_removed_request_headers_;
};
}  // namespace

class SigninHeaderHelperTest : public testing::Test {
 protected:
  void SetUp() override {
    content_settings::CookieSettings::RegisterProfilePrefs(prefs_.registry());
    HostContentSettingsMap::RegisterProfilePrefs(prefs_.registry());
    privacy_sandbox::RegisterProfilePrefs(prefs_.registry());

    settings_map_ = new HostContentSettingsMap(
        &prefs_, false /* is_off_the_record */, false /* store_last_modified */,
        false /* restore_session */, false /* should_record_metrics */);
    cookie_settings_ = new content_settings::CookieSettings(
        settings_map_.get(), &prefs_, false,
        content_settings::CookieSettings::NoFedCmSharingPermissionsCallback(),
        "");
  }

  void TearDown() override { settings_map_->ShutdownOnUIThread(); }

  void CheckMirrorCookieRequest(const GURL& url,
                                const GaiaId& gaia_id,
                                const std::string& expected_request) {
    EXPECT_EQ(expected_request,
              BuildMirrorRequestCookieIfPossible(
                  url, gaia_id, account_consistency_, cookie_settings_.get(),
                  PROFILE_MODE_DEFAULT));
  }

  net::HttpRequestHeaders CreateRequest(const GURL& url,
                                        const GaiaId& gaia_id,
                                        Tribool is_child_account) {
    net::HttpRequestHeaders original_headers;
    RequestAdapterWrapper request_adapter(url, original_headers);
    AppendOrRemoveMirrorRequestHeader(
        request_adapter.adapter(), GURL(), gaia_id, is_child_account,
        account_consistency_, cookie_settings_.get(), PROFILE_MODE_DEFAULT,
        kTestSource, force_account_consistency_);
    return request_adapter.GetFinalHeaders();
  }

  void CheckAccountConsistencyHeaderRequest(
      const net::HttpRequestHeaders& headers,
      const char* header_name,
      const std::string& expected_request) {
    bool expected_result = !expected_request.empty();
    EXPECT_THAT(headers.GetHeader(header_name),
                testing::Conditional(expected_result,
                                     testing::Optional(expected_request),
                                     std::nullopt));
  }

  void CheckMirrorHeaderRequest(const GURL& url,
                                const GaiaId& gaia_id,
                                Tribool is_child_account,
                                const std::string& expected_request) {
    net::HttpRequestHeaders headers =
        CreateRequest(url, gaia_id, is_child_account);
    CheckAccountConsistencyHeaderRequest(headers, kChromeConnectedHeader,
                                         expected_request);
  }

  std::string consistency_enabled_by_default_value() const { return "false"; }

  base::test::TaskEnvironment task_environment_;

  bool sync_enabled_ = false;
  std::string device_id_ = kTestDeviceId;
  AccountConsistencyMethod account_consistency_ =
      AccountConsistencyMethod::kDisabled;
  bool force_account_consistency_ = false;

  sync_preferences::TestingPrefServiceSyncable prefs_;

  scoped_refptr<HostContentSettingsMap> settings_map_;
  scoped_refptr<content_settings::CookieSettings> cookie_settings_;
};

#if BUILDFLAG(IS_CHROMEOS)
// Tests that Mirror request is returned on Chrome OS for Public Sessions (no
// account id).
TEST_F(SigninHeaderHelperTest, TestMirrorRequestNoAccountIdChromeOS) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  CheckMirrorHeaderRequest(
      GURL("https://docs.google.com"), GaiaId(),
      /*is_child_account=*/Tribool::kUnknown,
      "source=TestSource,mode=0,enable_account_consistency=true,"
      "consistency_enabled_by_default=" +
          consistency_enabled_by_default_value());
  CheckMirrorCookieRequest(GURL("https://docs.google.com"), GaiaId(),
                           "mode=0:enable_account_consistency=true:"
                           "consistency_enabled_by_default=" +
                               consistency_enabled_by_default_value());
}
#else  // BUILDFLAG(IS_CHROMEOS)
#if BUILDFLAG(IS_ANDROID)
// Tests that eligible_for_consistency request is returned on Android
// when reaching to Gaia origin and there's no primary account.
TEST_F(SigninHeaderHelperTest, TestEligibleForConsistencyRequestGaiaOrigin) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  CheckMirrorHeaderRequest(GURL("https://accounts.google.com"), GaiaId(),
                           /*is_child_account=*/Tribool::kUnknown,
                           "source=TestSource,eligible_for_consistency=true");
  CheckMirrorCookieRequest(GURL("https://accounts.google.com"), GaiaId(),
                           "eligible_for_consistency=true");
}

// Tests that eligible_for_consistency request is NOT returned on Android
// when reaching to NON-Gaia origin and there's no primary account
TEST_F(SigninHeaderHelperTest,
       TestNoEligibleForConsistencyRequestNonGaiaOrigin) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  CheckMirrorHeaderRequest(GURL("https://docs.google.com"), GaiaId(),
                           /*is_child_account=*/Tribool::kUnknown, "");
  CheckMirrorCookieRequest(GURL("https://docs.google.com"), GaiaId(), "");
}

// Tests that the full Mirror request is returned when the
// force_account_consistency param is true.
TEST_F(SigninHeaderHelperTest, TestForceAccountConsistencyMobile) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  force_account_consistency_ = true;
  CheckMirrorHeaderRequest(
      GURL("https://docs.google.com"), GaiaId(),
      /*is_child_account=*/Tribool::kUnknown,
      "source=TestSource,mode=0,enable_account_consistency=true,"
      "consistency_enabled_by_default=false");
}
#endif  // BUILDFLAG(IS_ANDROID)

// Tests that no Mirror request is returned when the user is not signed in (no
// account id), for non Chrome OS platforms.
TEST_F(SigninHeaderHelperTest, TestNoMirrorRequestNoAccountId) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  CheckMirrorHeaderRequest(GURL("https://docs.google.com"), GaiaId(),
                           /*is_child_account=*/Tribool::kUnknown, "");
  CheckMirrorCookieRequest(GURL("https://docs.google.com"), GaiaId(), "");
}
#endif

// Tests that no Mirror request is returned for youtubekids.com.
//
// Regression test for b/247647476
TEST_F(SigninHeaderHelperTest, TestNoMirrorHeaderForYoutubekids) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  CheckMirrorHeaderRequest(GURL("https://youtubekids.com"),
                           GaiaId("0123456789"),
                           /*is_child_account=*/Tribool::kUnknown, "");
  CheckMirrorCookieRequest(GURL("https://youtubekids.com"),
                           GaiaId("0123456789"), "");
}

// Does not apply to iOS as users cannot set cookies settings in iOS.
#if !BUILDFLAG(IS_IOS)
// Tests that no Mirror request is returned when the cookies aren't allowed to
// be set.
TEST_F(SigninHeaderHelperTest, TestNoMirrorRequestCookieSettingBlocked) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  cookie_settings_->SetDefaultCookieSetting(CONTENT_SETTING_BLOCK);
  CheckMirrorHeaderRequest(GURL("https://docs.google.com"),
                           GaiaId("0123456789"),
                           /*is_child_account=*/Tribool::kUnknown, "");
  CheckMirrorCookieRequest(GURL("https://docs.google.com"),
                           GaiaId("0123456789"), "");
}
#endif

TEST_F(SigninHeaderHelperTest,
       BuildMirrorRequestCookieIfPossibleHandlesNullptrCookieSettings) {
  std::string cookie = BuildMirrorRequestCookieIfPossible(
      GURL("https://docs.google.com"), GaiaId("0123456789"),
      AccountConsistencyMethod::kMirror,
      /*cookie_settings=*/nullptr, PROFILE_MODE_DEFAULT);
#if BUILDFLAG(IS_IOS)
  // Users cannot disable cookies via settings on iOS so we always build a
  // cookie.
  EXPECT_EQ(
      cookie,
      "mode=0:enable_account_consistency=true:consistency_enabled_by_default=" +
          consistency_enabled_by_default_value());
#else
  EXPECT_TRUE(cookie.empty());
#endif
}

// Tests that no Mirror request is returned when the target is a non-Google URL.
TEST_F(SigninHeaderHelperTest, TestNoMirrorRequestExternalURL) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  CheckMirrorHeaderRequest(GURL("https://foo.com"), GaiaId("0123456789"),
                           /*is_child_account=*/Tribool::kUnknown, "");
  CheckMirrorCookieRequest(GURL("https://foo.com"), GaiaId("0123456789"), "");
}

// Tests that the Mirror request is returned without the GAIA Id when the target
// is a google TLD domain.
TEST_F(SigninHeaderHelperTest, TestMirrorRequestGoogleTLD) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  CheckMirrorHeaderRequest(
      GURL("https://google.fr"), GaiaId("0123456789"),
      /*is_child_account=*/Tribool::kUnknown,
      "source=TestSource,mode=0,enable_account_consistency=true,"
      "consistency_enabled_by_default=" +
          consistency_enabled_by_default_value());
  CheckMirrorCookieRequest(GURL("https://google.de"), GaiaId("0123456789"),
                           "mode=0:enable_account_consistency=true:"
                           "consistency_enabled_by_default=" +
                               consistency_enabled_by_default_value());
}

// Tests that the Mirror request is returned when the target is the domain
// google.com and GAIA Id is not attached.
TEST_F(SigninHeaderHelperTest, TestMirrorRequestGoogleCom) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  CheckMirrorHeaderRequest(
      GURL("https://www.google.com"), GaiaId("0123456789"),
      /*is_child_account=*/Tribool::kUnknown,
      "source=TestSource,mode=0,enable_account_consistency=true,"
      "consistency_enabled_by_default=" +
          consistency_enabled_by_default_value());
  CheckMirrorCookieRequest(GURL("https://www.google.com"), GaiaId("0123456789"),
                           "mode=0:enable_account_consistency=true:"
                           "consistency_enabled_by_default=" +
                               consistency_enabled_by_default_value());
}

// Tests that no header sent when mirror account consistency is nor requested.
TEST_F(SigninHeaderHelperTest, TestMirrorRequestGoogleComNoProfileConsistency) {
  net::HttpRequestHeaders original_headers;
  RequestAdapterWrapper request_adapter(GURL("https://www.google.com"),
                                        original_headers);
  AppendOrRemoveMirrorRequestHeader(
      request_adapter.adapter(), GURL(), GaiaId("0123456789"),
      /*is_child_account=*/Tribool::kUnknown, account_consistency_,
      cookie_settings_.get(), PROFILE_MODE_DEFAULT, kTestSource,
      false /* force_account_consistency */);
  CheckAccountConsistencyHeaderRequest(request_adapter.GetFinalHeaders(),
                                       kChromeConnectedHeader, "");
}

// Tests that header sent when mirror account consistency is requested.
TEST_F(SigninHeaderHelperTest, TestMirrorRequestGoogleComProfileConsistency) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  net::HttpRequestHeaders original_headers;
  RequestAdapterWrapper request_adapter(GURL("https://www.google.com"),
                                        original_headers);
  AppendOrRemoveMirrorRequestHeader(
      request_adapter.adapter(), GURL(), GaiaId("0123456789"),
      /*is_child_account=*/Tribool::kUnknown, account_consistency_,
      cookie_settings_.get(), PROFILE_MODE_DEFAULT, kTestSource,
      false /* force_account_consistency */);
  CheckAccountConsistencyHeaderRequest(
      request_adapter.GetFinalHeaders(), kChromeConnectedHeader,
      "source=TestSource,mode=0,enable_account_consistency=true,"
      "consistency_enabled_by_default=" +
          consistency_enabled_by_default_value());
}

TEST_F(SigninHeaderHelperTest, TestMirrorRequestGoogleComSupervised) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  CheckMirrorHeaderRequest(
      GURL("https://www.google.com"), GaiaId("0123456789"),
      /*is_child_account=*/Tribool::kUnknown,
      "source=TestSource,mode=0,enable_account_consistency=true,"
      "consistency_enabled_by_default=" +
          consistency_enabled_by_default_value());
  CheckMirrorHeaderRequest(
      GURL("https://www.google.com"), GaiaId("0123456789"),
      /*is_child_account=*/Tribool::kTrue,
      "source=TestSource,mode=0,enable_account_consistency=true,"
      "supervised=true,consistency_enabled_by_default=" +
          consistency_enabled_by_default_value());
  CheckMirrorHeaderRequest(
      GURL("https://www.google.com"), GaiaId("0123456789"),
      /*is_child_account=*/Tribool::kFalse,
      "source=TestSource,mode=0,enable_account_consistency=true,"
      "supervised=false,consistency_enabled_by_default=" +
          consistency_enabled_by_default_value());
}

// Mirror is always enabled on Android and iOS, so these tests are only relevant
// on Desktop.
#if BUILDFLAG(ENABLE_DICE_SUPPORT)

TEST_F(SigninHeaderHelperTest, TestMirrorRequestGaiaURL) {
  // No request when account consistency is disabled.
  account_consistency_ = AccountConsistencyMethod::kDisabled;
  CheckMirrorHeaderRequest(GURL("https://accounts.google.com"),
                           GaiaId("0123456789"),
                           /*is_child_account=*/Tribool::kUnknown,
                           /*expected_request=*/"");
  CheckMirrorCookieRequest(GURL("https://accounts.google.com"),
                           GaiaId("0123456789"),
                           /*expected_request=*/"");

  account_consistency_ = AccountConsistencyMethod::kMirror;
  // No request when Mirror account consistency enabled, but user not signed in
  // to Chrome.
  CheckMirrorHeaderRequest(GURL("https://accounts.google.com"), GaiaId(),
                           /*is_child_account=*/Tribool::kUnknown,
                           /*expected_request=*/"");
  CheckMirrorCookieRequest(GURL("https://accounts.google.com"), GaiaId(),
                           /*expected_request=*/"");

  // Request with Gaia ID when Mirror account consistency enabled, and user is
  // signed in to Chrome.
  CheckMirrorHeaderRequest(
      GURL("https://accounts.google.com"), GaiaId("0123456789"),
      /*is_child_account=*/Tribool::kUnknown,
      "source=TestSource,mode=0,enable_account_consistency=true,"
      "consistency_enabled_by_default=false");
  CheckMirrorCookieRequest(GURL("https://accounts.google.com"),
                           GaiaId("0123456789"),
                           "mode=0:enable_account_consistency=true:"
                           "consistency_enabled_by_default=false");
}

// Tests that the no Mirror request is returned for on Drive/Docs origin when
// the user is not opted in to sync (also tests DICE acount consistency as there
// is special logic for the Mirror header for Drive URLs when DICE is enabled).
TEST_F(SigninHeaderHelperTest, TestMirrorHeaderRequestDriveSignedOut) {
  for (const GURL& url :
       {GURL("https://drive.google.com/"), GURL("https://docs.google.com/")}) {
    for (auto account_consistency :
         {AccountConsistencyMethod::kDisabled,
          AccountConsistencyMethod::kMirror, AccountConsistencyMethod::kDice}) {
      account_consistency_ = account_consistency;
      CheckMirrorHeaderRequest(url, GaiaId(),
                               /*is_child_account=*/Tribool::kUnknown,
                               /*expected_request=*/"");
      CheckMirrorCookieRequest(url, GaiaId(), /*expected_request=*/"");
    }
  }
}

// Tests that the Mirror request is returned with the GAIA Id on Drive/Docs
// origin, both when Mirror or DICE account consistency is enabled.
TEST_F(SigninHeaderHelperTest, TestMirrorHeaderRequestDriveSignedIn) {
  for (const GURL& url :
       {GURL("https://drive.google.com/"), GURL("https://docs.google.com/")}) {
    account_consistency_ = AccountConsistencyMethod::kMirror;
    // Request with Gaia ID when Mirror account consistency is enabled and user
    // is signed in to Chrome.
    CheckMirrorHeaderRequest(
        url, GaiaId("0123456789"), /*is_child_account=*/Tribool::kUnknown,
        "source=TestSource,id=0123456789,mode=0,"
        "enable_account_consistency=true,consistency_enabled_by_default=false");
    // Cookie does not include the Gaia ID even when Mirror account consistency
    // is enabled and user is signed in to Chrome.
    CheckMirrorCookieRequest(url, GaiaId("0123456789"),
                             "mode=0:enable_account_consistency=true:"
                             "consistency_enabled_by_default=false");

    account_consistency_ = AccountConsistencyMethod::kDice;
    // Request with Gaia ID when DICE account consistency is enabled and user is
    // opted in to sync.
    CheckMirrorHeaderRequest(url, GaiaId("0123456789"),
                             /*is_child_account=*/Tribool::kUnknown,
                             "source=TestSource,id=0123456789,mode=0,"
                             "enable_account_consistency=false,"
                             "consistency_enabled_by_default=false");
  }
}
#endif  // BUILDFLAG(ENABLE_DICE_SUPPORT)

// Tests that the Mirror header request is returned normally when the redirect
// URL is eligible.
TEST_F(SigninHeaderHelperTest, TestMirrorHeaderEligibleRedirectURL) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  const GURL url("https://docs.google.com/document");
  const GURL redirect_url("https://www.google.com");
  const GaiaId gaia_id("0123456789");
  net::HttpRequestHeaders original_headers;
  RequestAdapterWrapper request_adapter(url, original_headers);
  AppendOrRemoveMirrorRequestHeader(
      request_adapter.adapter(), redirect_url, gaia_id,
      /*is_child_account=*/Tribool::kUnknown, account_consistency_,
      cookie_settings_.get(), PROFILE_MODE_DEFAULT, kTestSource,
      false /* force_account_consistency */);
  EXPECT_TRUE(
      request_adapter.GetFinalHeaders().HasHeader(kChromeConnectedHeader));
}

// Tests that the Mirror header request is stripped when the redirect URL is not
// eligible.
TEST_F(SigninHeaderHelperTest, TestMirrorHeaderNonEligibleRedirectURL) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  const GURL url("https://docs.google.com/document");
  const GURL redirect_url("http://www.foo.com");
  const GaiaId gaia_id("0123456789");
  net::HttpRequestHeaders original_headers;
  original_headers.SetHeader(kChromeConnectedHeader, "foo,bar");
  RequestAdapterWrapper request_adapter(url, original_headers);
  AppendOrRemoveMirrorRequestHeader(
      request_adapter.adapter(), redirect_url, gaia_id,
      /*is_child_account=*/Tribool::kUnknown, account_consistency_,
      cookie_settings_.get(), PROFILE_MODE_DEFAULT, kTestSource,
      false /* force_account_consistency */);
  EXPECT_FALSE(
      request_adapter.GetFinalHeaders().HasHeader(kChromeConnectedHeader));
}

// Tests that the Mirror header, whatever its value is, is untouched when both
// the current and the redirect URL are non-eligible.
TEST_F(SigninHeaderHelperTest, TestIgnoreMirrorHeaderNonEligibleURLs) {
  account_consistency_ = AccountConsistencyMethod::kMirror;
  const GURL url("https://www.bar.com");
  const GURL redirect_url("http://www.foo.com");
  const GaiaId gaia_id("0123456789");
  const std::string fake_header = "foo,bar";
  net::HttpRequestHeaders original_headers;
  original_headers.SetHeader(kChromeConnectedHeader, fake_header);
  RequestAdapterWrapper request_adapter(url, original_headers);
  AppendOrRemoveMirrorRequestHeader(
      request_adapter.adapter(), redirect_url, gaia_id,
      /*is_child_account=*/Tribool::kUnknown, account_consistency_,
      cookie_settings_.get(), PROFILE_MODE_DEFAULT, kTestSource,
      false /* force_account_consistency */);
  EXPECT_THAT(
      request_adapter.GetFinalHeaders().GetHeader(kChromeConnectedHeader),
      testing::Optional(fake_header));
}

TEST_F(SigninHeaderHelperTest, TestInvalidManageAccountsParams) {
  ManageAccountsParams params = BuildManageAccountsParams("blah");
  EXPECT_EQ(GAIA_SERVICE_TYPE_NONE, params.service_type);
}

// Tests that managed accounts' header is parsed correctly from string input.
TEST_F(SigninHeaderHelperTest, TestBuildManageAccountsParams) {
  const char kContinueURL[] = "https://www.example.com/continue";
  const char kEmail[] = "foo@example.com";

  std::string header = base::StringPrintf(
      "action=ADDSESSION,email=%s,is_saml=true,"
      "is_same_tab=true,continue_url=%s",
      kEmail, kContinueURL);
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
  header += ",show_consistency_promo=true";
#endif

  ManageAccountsParams params = BuildManageAccountsParams(header);
  EXPECT_EQ(GAIA_SERVICE_TYPE_ADDSESSION, params.service_type);
  EXPECT_EQ(kEmail, params.email);
  EXPECT_EQ(true, params.is_saml);
  EXPECT_EQ(true, params.is_same_tab);
  EXPECT_EQ(GURL(kContinueURL), params.continue_url);
#if BUILDFLAG(IS_ANDROID)
  EXPECT_EQ(true, params.show_consistency_promo);
#endif
}

}  // namespace signin
