| // 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. |
| |
| #include "content/browser/dips/dips_bounce_detector.h" |
| |
| #include <string_view> |
| #include <tuple> |
| |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_forward.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/strings/strcat.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/simple_test_clock.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "base/test/task_environment.h" |
| #include "base/time/time.h" |
| #include "base/types/pass_key.h" |
| #include "components/content_settings/core/common/features.h" |
| #include "components/ukm/test_ukm_recorder.h" |
| #include "content/browser/dips/dips_service_impl.h" |
| #include "content/browser/dips/dips_test_utils.h" |
| #include "content/browser/dips/dips_utils.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/navigation_handle_timing.h" |
| #include "content/public/common/content_features.h" |
| #include "net/http/http_status_code.h" |
| #include "services/metrics/public/cpp/ukm_source_id.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using base::Bucket; |
| using base::PassKey; |
| using testing::AllOf; |
| using testing::ElementsAre; |
| using testing::Eq; |
| using testing::Gt; |
| using testing::IsEmpty; |
| using testing::Pair; |
| using testing::SizeIs; |
| |
| namespace content { |
| |
| // Encodes data about a bounce (the url, time of bounce, and |
| // whether it's stateful) for use when testing that the bounce is |
| // recorded by the BtmBounceDetector. |
| using BounceTuple = std::tuple<GURL, base::Time, bool>; |
| // Encodes data about an event recorded by DIPS event (the url, time of |
| // bounce, and type of event) for use when testing that the event is recorded |
| // by the BtmBounceDetector. |
| using EventTuple = std::tuple<GURL, base::Time, BtmRecordedEvent>; |
| |
| enum class UserGestureStatus { kNoUserGesture, kWithUserGesture }; |
| constexpr auto kNoUserGesture = UserGestureStatus::kNoUserGesture; |
| constexpr auto kWithUserGesture = UserGestureStatus::kWithUserGesture; |
| |
| // Returns a simplified URL representation for ease of comparison in tests. |
| // Just host+path. |
| std::string FormatURL(const GURL& url) { |
| return base::StrCat({url.host_piece(), url.path_piece()}); |
| } |
| |
| void AppendRedirect(std::vector<std::string>* redirects, |
| const BtmRedirectInfo& redirect, |
| const BtmRedirectChainInfo& chain) { |
| redirects->push_back(base::StringPrintf( |
| "[%zu/%zu] %s -> %s (%s) -> %s", redirect.chain_index.value() + 1, |
| chain.length, FormatURL(chain.initial_url.url).c_str(), |
| FormatURL(redirect.url.url).c_str(), |
| BtmDataAccessTypeToString(redirect.access_type).data(), |
| FormatURL(chain.final_url.url).c_str())); |
| } |
| |
| std::string URLForRedirectSourceId(ukm::TestUkmRecorder* ukm_recorder, |
| ukm::SourceId source_id) { |
| return FormatURL(ukm_recorder->GetSourceForSourceId(source_id)->url()); |
| } |
| |
| class FakeNavigation; |
| |
| class TestBounceDetectorDelegate : public BtmBounceDetectorDelegate { |
| public: |
| // BtmBounceDetectorDelegate overrides: |
| UrlAndSourceId GetLastCommittedURL() const override { |
| return {committed_url_, source_id_}; |
| } |
| |
| void HandleRedirectChain(std::vector<BtmRedirectInfoPtr> redirects, |
| BtmRedirectChainInfoPtr chain) override { |
| chain->cookie_mode = BtmCookieMode::kBlock3PC; |
| size_t redirect_index = chain->length - redirects.size(); |
| |
| for (auto& redirect : redirects) { |
| redirect->site_had_user_activation = |
| GetSiteHasUserActivation(redirect->url.url); |
| redirect->chain_id = chain->chain_id; |
| redirect->chain_index = redirect_index; |
| redirect->has_3pc_exception = false; |
| DCHECK(redirect->access_type != BtmDataAccessType::kUnknown); |
| AppendRedirect(&redirects_, *redirect, *chain); |
| |
| BtmServiceImpl::HandleRedirectForTesting( |
| *redirect, *chain, |
| base::BindRepeating(&TestBounceDetectorDelegate::RecordBounce, |
| base::Unretained(this))); |
| |
| redirect_index++; |
| } |
| } |
| |
| // The version of this method in the BtmWebContentsObserver checks |
| // BtmStorage for interactions and runs |callback| with the returned list of |
| // sites without interaction. However, for the purpose of testing here, this |
| // method just records the sites reported to it in |reported_sites_| without |
| // filtering. |
| void ReportRedirectors(const std::set<std::string> sites) override { |
| if (sites.size() == 0) { |
| return; |
| } |
| |
| reported_sites_.push_back(base::JoinString( |
| std::vector<std::string_view>(sites.begin(), sites.end()), ", ")); |
| } |
| |
| void OnSiteStorageAccessed(const GURL& first_party_url, |
| CookieOperation op, |
| bool http_cookie) override {} |
| |
| // Get the (committed) URL that the SourceId was generated for. |
| const std::string& URLForSourceId(ukm::SourceId source_id) { |
| return url_by_source_id_[source_id]; |
| } |
| |
| bool GetSiteHasUserActivation(const GURL& url) { |
| return site_has_user_activation_[GetSiteForBtm(url)]; |
| } |
| |
| void SetSiteHasUserActivation(const GURL& url) { |
| site_has_user_activation_[GetSiteForBtm(url)] = true; |
| } |
| |
| void SetCommittedURL(PassKey<FakeNavigation>, |
| const GURL& url, |
| ukm::SourceId source_id) { |
| committed_url_ = url; |
| source_id_ = source_id; |
| url_by_source_id_[source_id_] = FormatURL(url); |
| } |
| |
| const std::set<BounceTuple>& GetRecordedBounces() const { |
| return recorded_bounces_; |
| } |
| |
| const std::vector<std::string>& GetReportedSites() const { |
| return reported_sites_; |
| } |
| |
| const std::vector<std::string>& redirects() const { return redirects_; } |
| |
| int stateful_bounce_count() const { return stateful_bounce_count_; } |
| |
| private: |
| void RecordBounce( |
| const GURL& url, |
| bool has_3pc_exception, |
| const GURL& final_url, |
| base::Time time, |
| bool stateful, |
| base::RepeatingCallback<void(const GURL&)> increment_bounce_callback) { |
| recorded_bounces_.insert(std::make_tuple(url, time, stateful)); |
| if (stateful) { |
| stateful_bounce_count_++; |
| } |
| } |
| |
| GURL committed_url_; |
| ukm::SourceId source_id_; |
| std::map<ukm::SourceId, std::string> url_by_source_id_; |
| std::map<std::string, bool> site_has_user_activation_; |
| std::vector<std::string> redirects_; |
| std::set<BounceTuple> recorded_bounces_; |
| std::vector<std::string> reported_sites_; |
| int stateful_bounce_count_ = 0; |
| }; |
| |
| class FakeNavigation : public BtmNavigationHandle { |
| public: |
| FakeNavigation(BtmBounceDetector* detector, |
| TestBounceDetectorDelegate* parent, |
| const GURL& url, |
| bool has_user_gesture) |
| : detector_(detector), |
| delegate_(parent), |
| has_user_gesture_(has_user_gesture) { |
| chain_.push_back(url); |
| detector_->DidStartNavigation(this); |
| } |
| ~FakeNavigation() override { CHECK(finished_); } |
| |
| FakeNavigation& RedirectTo(std::string url) { |
| chain_.emplace_back(std::move(url)); |
| detector_->DidRedirectNavigation(this); |
| return *this; |
| } |
| |
| FakeNavigation& AccessCookie(CookieOperation op) { |
| detector_->OnServerCookiesAccessed(this, GetURL(), op); |
| return *this; |
| } |
| |
| void Finish(bool commit) { |
| CHECK(!finished_); |
| finished_ = true; |
| has_committed_ = commit; |
| if (commit) { |
| previous_url_ = delegate_->GetLastCommittedURL().url; |
| delegate_->SetCommittedURL(PassKey<FakeNavigation>(), GetURL(), |
| GetNextPageUkmSourceId()); |
| } |
| detector_->DidFinishNavigation(this); |
| } |
| |
| private: |
| // BtmNavigationHandle overrides: |
| bool HasUserGesture() const override { return has_user_gesture_; } |
| ServerBounceDetectionState* GetServerState() override { return &state_; } |
| bool HasCommitted() const override { return has_committed_; } |
| ukm::SourceId GetNextPageUkmSourceId() override { return next_source_id_; } |
| const GURL& GetPreviousPrimaryMainFrameURL() const override { |
| return previous_url_; |
| } |
| // TODO (crbug.com/1442658): Add support for simulating opening a link in a |
| // new tab. |
| const GURL GetInitiator() const override { |
| return previous_url_.is_empty() ? GURL("about:blank") : previous_url_; |
| } |
| const std::vector<GURL>& GetRedirectChain() const override { return chain_; } |
| bool WasResponseCached() override { return false; } |
| int GetHTTPResponseCode() override { return net::HTTP_FOUND; } |
| |
| raw_ptr<BtmBounceDetector> detector_; |
| raw_ptr<TestBounceDetectorDelegate> delegate_; |
| const bool has_user_gesture_; |
| bool finished_ = false; |
| const ukm::SourceId next_source_id_ = ukm::AssignNewSourceId(); |
| |
| ServerBounceDetectionState state_; |
| bool has_committed_ = false; |
| GURL previous_url_; |
| std::vector<GURL> chain_; |
| }; |
| |
| class BtmBounceDetectorTest : public ::testing::Test { |
| protected: |
| base::test::TaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| |
| FakeNavigation StartNavigation(const std::string& url, |
| UserGestureStatus status) { |
| return FakeNavigation(&detector_, &delegate_, GURL(url), |
| status == kWithUserGesture); |
| } |
| |
| void NavigateTo(const std::string& url, UserGestureStatus status) { |
| StartNavigation(url, status).Finish(true); |
| } |
| |
| void AccessClientCookie(CookieOperation op) { |
| detector_.OnClientSiteDataAccessed(delegate_.GetLastCommittedURL().url, op); |
| } |
| |
| void LateAccessClientCookie(const std::string& url, CookieOperation op) { |
| if (!detector_.AddLateCookieAccess(GURL(url), op)) { |
| detector_.OnClientSiteDataAccessed(GURL(url), op); |
| } |
| } |
| |
| void ActivatePage() { detector_.OnUserActivation(); } |
| void TriggerWebAuthnAssertionRequestSucceeded() { |
| detector_.WebAuthnAssertionRequestSucceeded(); |
| } |
| |
| const BtmRedirectContext& CommittedRedirectContext() { |
| return detector_.CommittedRedirectContext(); |
| } |
| |
| void AdvanceBtmTime(base::TimeDelta delta) { |
| task_environment_.AdvanceClock(delta); |
| task_environment_.RunUntilIdle(); |
| } |
| |
| // Advances the mocked clock by `features::kBtmClientBounceDetectionTimeout` |
| // to trigger the closure of the pending redirect chain. |
| void EndPendingRedirectChain() { |
| AdvanceBtmTime(features::kBtmClientBounceDetectionTimeout.Get()); |
| } |
| |
| const std::string& URLForNavigationSourceId(ukm::SourceId source_id) { |
| return delegate_.URLForSourceId(source_id); |
| } |
| |
| void SetSiteHasUserActivation(const std::string& url) { |
| return delegate_.SetSiteHasUserActivation(GURL(url)); |
| } |
| |
| std::set<BounceTuple> GetRecordedBounces() const { |
| return delegate_.GetRecordedBounces(); |
| } |
| |
| BounceTuple MakeBounceTuple(const std::string& url, |
| const base::Time& time, |
| bool stateful) { |
| return std::make_tuple(GURL(url), time, stateful); |
| } |
| |
| EventTuple MakeEventTuple(const std::string& url, |
| const base::Time& time, |
| BtmRecordedEvent event) { |
| return std::make_tuple(GURL(url), time, event); |
| } |
| |
| const std::vector<std::string>& GetReportedSites() const { |
| return delegate_.GetReportedSites(); |
| } |
| |
| base::Time GetCurrentTime() { |
| return task_environment_.GetMockClock()->Now(); |
| } |
| |
| const std::vector<std::string>& redirects() const { |
| return delegate_.redirects(); |
| } |
| |
| int stateful_bounce_count() const { |
| return delegate_.stateful_bounce_count(); |
| } |
| |
| private: |
| TestBounceDetectorDelegate delegate_; |
| BtmBounceDetector detector_{&delegate_, task_environment_.GetMockTickClock(), |
| task_environment_.GetMockClock()}; |
| }; |
| |
| // Ensures that for every navigation, a client redirect occurring before |
| // `dips:kClientBounceDetectionTimeout` is considered a bounce whilst leaving |
| // Server redirects unaffected. |
| TEST_F(BtmBounceDetectorTest, |
| DetectStatefulRedirects_Before_ClientBounceDetectionTimeout) { |
| NavigateTo("http://a.test", kWithUserGesture); |
| auto mocked_bounce_time_1 = GetCurrentTime(); |
| StartNavigation("http://b.test", kWithUserGesture) |
| .RedirectTo("http://c.test") |
| .RedirectTo("http://d.test") |
| .Finish(true); |
| AdvanceBtmTime(features::kBtmClientBounceDetectionTimeout.Get() - |
| base::Seconds(1)); |
| auto mocked_bounce_time_2 = GetCurrentTime(); |
| StartNavigation("http://e.test", kNoUserGesture) |
| .RedirectTo("http://f.test") |
| .RedirectTo("http://g.test") |
| .Finish(true); |
| AdvanceBtmTime(features::kBtmClientBounceDetectionTimeout.Get() - |
| base::Seconds(1)); |
| auto mocked_bounce_time_3 = GetCurrentTime(); |
| StartNavigation("http://h.test", kWithUserGesture) |
| .RedirectTo("http://i.test") |
| .RedirectTo("http://j.test") |
| .Finish(true); |
| |
| EndPendingRedirectChain(); |
| |
| EXPECT_THAT(redirects(), testing::ElementsAre( |
| ("[1/5] a.test/ -> b.test/ (None) -> g.test/"), |
| ("[2/5] a.test/ -> c.test/ (None) -> g.test/"), |
| ("[3/5] a.test/ -> d.test/ (None) -> g.test/"), |
| ("[4/5] a.test/ -> e.test/ (None) -> g.test/"), |
| ("[5/5] a.test/ -> f.test/ (None) -> g.test/"), |
| ("[1/2] g.test/ -> h.test/ (None) -> j.test/"), |
| ("[2/2] g.test/ -> i.test/ (None) -> j.test/"))); |
| |
| EXPECT_THAT(GetRecordedBounces(), |
| testing::UnorderedElementsAre( |
| MakeBounceTuple("http://b.test", mocked_bounce_time_1, |
| /*stateful=*/false), |
| MakeBounceTuple("http://c.test", mocked_bounce_time_1, |
| /*stateful=*/false), |
| MakeBounceTuple("http://d.test", mocked_bounce_time_2, |
| /*stateful=*/false), |
| MakeBounceTuple("http://e.test", mocked_bounce_time_2, |
| /*stateful=*/false), |
| MakeBounceTuple("http://f.test", mocked_bounce_time_2, |
| /*stateful=*/false), |
| MakeBounceTuple("http://h.test", mocked_bounce_time_3, |
| /*stateful=*/false), |
| MakeBounceTuple("http://i.test", mocked_bounce_time_3, |
| /*stateful=*/false))); |
| EXPECT_EQ(stateful_bounce_count(), 0); |
| } |
| |
| // Ensures that for every navigation, a client redirect occurring after |
| // `dips:kClientBounceDetectionTimeout` is not considered a bounce whilst server |
| // redirects are unaffected. |
| TEST_F(BtmBounceDetectorTest, |
| DetectStatefulRedirects_After_ClientBounceDetectionTimeout) { |
| NavigateTo("http://a.test", kWithUserGesture); |
| AdvanceBtmTime(features::kBtmClientBounceDetectionTimeout.Get()); |
| auto mocked_bounce_time_1 = GetCurrentTime(); |
| StartNavigation("http://b.test", kWithUserGesture) |
| .RedirectTo("http://c.test") |
| .RedirectTo("http://d.test") |
| .Finish(true); |
| AdvanceBtmTime(features::kBtmClientBounceDetectionTimeout.Get()); |
| auto mocked_bounce_time_2 = GetCurrentTime(); |
| StartNavigation("http://e.test", kNoUserGesture) |
| .RedirectTo("http://f.test") |
| .RedirectTo("http://g.test") |
| .Finish(true); |
| |
| EndPendingRedirectChain(); |
| |
| EXPECT_THAT(redirects(), testing::ElementsAre( |
| ("[1/2] a.test/ -> b.test/ (None) -> d.test/"), |
| ("[2/2] a.test/ -> c.test/ (None) -> d.test/"), |
| ("[1/2] d.test/ -> e.test/ (None) -> g.test/"), |
| ("[2/2] d.test/ -> f.test/ (None) -> g.test/"))); |
| |
| EXPECT_THAT(GetRecordedBounces(), |
| testing::UnorderedElementsAre( |
| MakeBounceTuple("http://b.test", mocked_bounce_time_1, |
| /*stateful=*/false), |
| MakeBounceTuple("http://c.test", mocked_bounce_time_1, |
| /*stateful=*/false), |
| MakeBounceTuple("http://e.test", mocked_bounce_time_2, |
| /*stateful=*/false), |
| MakeBounceTuple("http://f.test", mocked_bounce_time_2, |
| /*stateful=*/false))); |
| EXPECT_EQ(stateful_bounce_count(), 0); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, DetectStatefulRedirect_Server) { |
| NavigateTo("http://a.test", kWithUserGesture); |
| StartNavigation("http://b.test", kWithUserGesture) |
| .AccessCookie(CookieOperation::kRead) |
| .RedirectTo("http://c.test") |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://d.test") |
| .AccessCookie(CookieOperation::kRead) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://e.test") |
| .Finish(true); |
| |
| auto mocked_bounce_time = GetCurrentTime(); |
| |
| EndPendingRedirectChain(); |
| |
| EXPECT_THAT(redirects(), |
| testing::ElementsAre( |
| ("[1/3] a.test/ -> b.test/ (Read) -> e.test/"), |
| ("[2/3] a.test/ -> c.test/ (Write) -> e.test/"), |
| ("[3/3] a.test/ -> d.test/ (ReadWrite) -> e.test/"))); |
| |
| EXPECT_THAT(GetRecordedBounces(), |
| testing::UnorderedElementsAre( |
| MakeBounceTuple("http://b.test", mocked_bounce_time, |
| /*stateful=*/false), |
| MakeBounceTuple("http://c.test", mocked_bounce_time, |
| /*stateful=*/true), |
| MakeBounceTuple("http://d.test", mocked_bounce_time, |
| /*stateful=*/true))); |
| EXPECT_EQ(stateful_bounce_count(), 2); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, DetectStatefulRedirect_Server_OnStartUp) { |
| StartNavigation("http://b.test", kWithUserGesture) |
| .AccessCookie(CookieOperation::kRead) |
| .RedirectTo("http://c.test") |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://d.test") |
| .AccessCookie(CookieOperation::kRead) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://e.test") |
| .Finish(true); |
| |
| auto mocked_bounce_time = GetCurrentTime(); |
| |
| EndPendingRedirectChain(); |
| |
| EXPECT_THAT( |
| redirects(), |
| testing::ElementsAre(("[1/3] blank -> b.test/ (Read) -> e.test/"), |
| ("[2/3] blank -> c.test/ (Write) -> e.test/"), |
| ("[3/3] blank -> d.test/ (ReadWrite) -> e.test/"))); |
| |
| EXPECT_THAT(GetRecordedBounces(), |
| testing::UnorderedElementsAre( |
| MakeBounceTuple("http://b.test", mocked_bounce_time, |
| /*stateful=*/false), |
| MakeBounceTuple("http://c.test", mocked_bounce_time, |
| /*stateful=*/true), |
| MakeBounceTuple("http://d.test", mocked_bounce_time, |
| /*stateful=*/true))); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, DetectStatefulRedirect_Server_LateNotification) { |
| NavigateTo("http://a.test", kWithUserGesture); |
| StartNavigation("http://b.test", kWithUserGesture) |
| .AccessCookie(CookieOperation::kRead) |
| .RedirectTo("http://c.test") |
| .RedirectTo("http://d.test") |
| .RedirectTo("http://e.test") |
| .Finish(true); |
| |
| LateAccessClientCookie("http://b.test", CookieOperation::kChange); |
| LateAccessClientCookie("http://c.test", CookieOperation::kRead); |
| LateAccessClientCookie("http://d.test", CookieOperation::kChange); |
| LateAccessClientCookie("http://e.test", CookieOperation::kRead); |
| LateAccessClientCookie("http://e.test", CookieOperation::kChange); |
| |
| auto mocked_bounce_time = GetCurrentTime(); |
| |
| EndPendingRedirectChain(); |
| |
| EXPECT_THAT( |
| redirects(), |
| testing::ElementsAre(("[1/3] a.test/ -> b.test/ (ReadWrite) -> e.test/"), |
| ("[2/3] a.test/ -> c.test/ (Read) -> e.test/"), |
| ("[3/3] a.test/ -> d.test/ (Write) -> e.test/"))); |
| |
| EXPECT_THAT(GetRecordedBounces(), |
| testing::UnorderedElementsAre( |
| MakeBounceTuple("http://b.test", mocked_bounce_time, |
| /*stateful=*/true), |
| MakeBounceTuple("http://c.test", mocked_bounce_time, |
| /*stateful=*/false), |
| MakeBounceTuple("http://d.test", mocked_bounce_time, |
| /*stateful=*/true))); |
| EXPECT_EQ(stateful_bounce_count(), 2); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, DetectStatefulRedirect_Client) { |
| NavigateTo("http://a.test", kWithUserGesture); |
| NavigateTo("http://b.test", kWithUserGesture); |
| AdvanceBtmTime(features::kBtmClientBounceDetectionTimeout.Get() - |
| base::Seconds(1)); |
| NavigateTo("http://c.test", kNoUserGesture); |
| |
| auto mocked_bounce_time = GetCurrentTime(); |
| |
| EndPendingRedirectChain(); |
| |
| EXPECT_THAT(redirects(), testing::ElementsAre( |
| ("[1/1] a.test/ -> b.test/ (None) -> c.test/"))); |
| EXPECT_THAT(GetRecordedBounces(), |
| testing::UnorderedElementsAre(MakeBounceTuple( |
| "http://b.test", mocked_bounce_time, /*stateful=*/false))); |
| EXPECT_EQ(stateful_bounce_count(), 0); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, DetectStatefulRedirect_Client_OnStartUp) { |
| NavigateTo("http://a.test", kWithUserGesture); |
| AccessClientCookie(CookieOperation::kRead); |
| AccessClientCookie(CookieOperation::kChange); |
| AdvanceBtmTime(features::kBtmClientBounceDetectionTimeout.Get() - |
| base::Seconds(1)); |
| NavigateTo("http://b.test", kNoUserGesture); |
| |
| auto mocked_bounce_time = GetCurrentTime(); |
| |
| EndPendingRedirectChain(); |
| |
| EXPECT_THAT( |
| redirects(), |
| testing::ElementsAre(("[1/1] blank -> a.test/ (ReadWrite) -> b.test/"))); |
| EXPECT_THAT(GetRecordedBounces(), |
| testing::UnorderedElementsAre(MakeBounceTuple( |
| "http://a.test", mocked_bounce_time, /*stateful=*/true))); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, DetectStatefulRedirect_Client_MergeCookies) { |
| NavigateTo("http://a.test", kWithUserGesture); |
| // Server cookie read: |
| StartNavigation("http://b.test", kWithUserGesture) |
| .AccessCookie(CookieOperation::kRead) |
| .Finish(true); |
| // Client cookie write: |
| // NOTE: This navigation's client redirect will always be considered a bounce |
| // because of the (frozen) mocked clock. |
| AccessClientCookie(CookieOperation::kChange); |
| NavigateTo("http://c.test", kNoUserGesture); |
| |
| auto mocked_bounce_time = GetCurrentTime(); |
| |
| EndPendingRedirectChain(); |
| |
| // Redirect cookie access is reported as ReadWrite. |
| EXPECT_THAT(redirects(), |
| testing::ElementsAre( |
| ("[1/1] a.test/ -> b.test/ (ReadWrite) -> c.test/"))); |
| EXPECT_THAT(GetRecordedBounces(), |
| testing::UnorderedElementsAre(MakeBounceTuple( |
| "http://b.test", mocked_bounce_time, /*stateful=*/true))); |
| EXPECT_EQ(stateful_bounce_count(), 1); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, DetectStatefulRedirect_ServerClientServer) { |
| NavigateTo("http://a.test", kWithUserGesture); |
| StartNavigation("http://b.test", kWithUserGesture) |
| .RedirectTo("http://c.test") |
| .Finish(true); |
| StartNavigation("http://d.test", kNoUserGesture) |
| .RedirectTo("http://e.test") |
| .Finish(true); |
| |
| auto mocked_bounce_time = GetCurrentTime(); |
| |
| EndPendingRedirectChain(); |
| |
| EXPECT_THAT(redirects(), testing::ElementsAre( |
| ("[1/3] a.test/ -> b.test/ (None) -> e.test/"), |
| ("[2/3] a.test/ -> c.test/ (None) -> e.test/"), |
| ("[3/3] a.test/ -> d.test/ (None) -> e.test/"))); |
| EXPECT_THAT(GetRecordedBounces(), |
| testing::UnorderedElementsAre( |
| MakeBounceTuple("http://b.test", mocked_bounce_time, |
| /*stateful=*/false), |
| MakeBounceTuple("http://c.test", mocked_bounce_time, |
| /*stateful=*/false), |
| MakeBounceTuple("http://d.test", mocked_bounce_time, |
| /*stateful=*/false))); |
| EXPECT_EQ(stateful_bounce_count(), 0); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, DetectStatefulRedirect_Server_Uncommitted) { |
| NavigateTo("http://a.test", kWithUserGesture); |
| StartNavigation("http://b.test", kWithUserGesture) |
| .RedirectTo("http://c.test") |
| .RedirectTo("http://d.test") |
| .Finish(false); |
| // Because the previous navigation didn't commit, the following chain still |
| // starts from http://a.test/. |
| StartNavigation("http://e.test", kWithUserGesture) |
| .RedirectTo("http://f.test") |
| .Finish(true); |
| |
| auto mocked_bounce_time = GetCurrentTime(); |
| |
| EndPendingRedirectChain(); |
| |
| EXPECT_THAT(redirects(), testing::ElementsAre( |
| ("[1/3] a.test/ -> b.test/ (None) -> a.test/"), |
| ("[2/3] a.test/ -> c.test/ (None) -> a.test/"), |
| ("[3/3] a.test/ -> d.test/ (None) -> a.test/"), |
| ("[1/1] a.test/ -> e.test/ (None) -> f.test/"))); |
| EXPECT_THAT(GetRecordedBounces(), |
| testing::UnorderedElementsAre( |
| MakeBounceTuple("http://b.test", mocked_bounce_time, |
| /*stateful=*/false), |
| MakeBounceTuple("http://c.test", mocked_bounce_time, |
| /*stateful=*/false), |
| MakeBounceTuple("http://d.test", mocked_bounce_time, |
| /*stateful=*/false), |
| MakeBounceTuple("http://e.test", mocked_bounce_time, |
| /*stateful=*/false))); |
| EXPECT_EQ(stateful_bounce_count(), 0); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, DetectStatefulRedirect_Client_Uncommitted) { |
| NavigateTo("http://a.test", kWithUserGesture); |
| NavigateTo("http://b.test", kWithUserGesture); |
| StartNavigation("http://c.test", kNoUserGesture) |
| .RedirectTo("http://d.test") |
| .Finish(false); |
| // Because the previous navigation didn't commit, the following chain still |
| // starts from http://a.test/. |
| StartNavigation("http://e.test", kNoUserGesture) |
| .RedirectTo("http://f.test") |
| .Finish(true); |
| |
| auto mocked_bounce_time = GetCurrentTime(); |
| |
| EndPendingRedirectChain(); |
| |
| EXPECT_THAT(redirects(), testing::ElementsAre( |
| ("[1/3] a.test/ -> b.test/ (None) -> b.test/"), |
| ("[2/3] a.test/ -> c.test/ (None) -> b.test/"), |
| ("[3/3] a.test/ -> d.test/ (None) -> b.test/"), |
| ("[1/2] a.test/ -> b.test/ (None) -> f.test/"), |
| ("[2/2] a.test/ -> e.test/ (None) -> f.test/"))); |
| EXPECT_THAT(GetRecordedBounces(), |
| testing::UnorderedElementsAre( |
| MakeBounceTuple("http://b.test", mocked_bounce_time, |
| /*stateful=*/false), |
| MakeBounceTuple("http://c.test", mocked_bounce_time, |
| /*stateful=*/false), |
| MakeBounceTuple("http://d.test", mocked_bounce_time, |
| /*stateful=*/false), |
| MakeBounceTuple("http://e.test", mocked_bounce_time, |
| /*stateful=*/false))); |
| EXPECT_EQ(stateful_bounce_count(), 0); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, |
| ReportRedirectorsInChain_OnEachFinishedNavigation) { |
| // Visit initial page on a.test and access cookies via JS. |
| NavigateTo("http://a.test", kWithUserGesture); |
| AccessClientCookie(CookieOperation::kChange); |
| |
| // Navigate with a click (not a redirect) to b.test, which S-redirects to |
| // c.test. |
| StartNavigation("http://b.test", kWithUserGesture) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://c.test") |
| .AccessCookie(CookieOperation::kChange) |
| .Finish(true); |
| EXPECT_THAT(GetReportedSites(), testing::ElementsAre("b.test")); |
| |
| // Navigate without a click (i.e. by C-redirecting) to d.test. |
| NavigateTo("http://d.test", kNoUserGesture); |
| EXPECT_THAT(GetReportedSites(), testing::ElementsAre("b.test", "c.test")); |
| // Access cookies on d.test. |
| AccessClientCookie(CookieOperation::kChange); |
| |
| // Navigate without a click (i.e. by C-redirecting) to e.test, which |
| // S-redirects to f.test. |
| StartNavigation("http://e.test", kNoUserGesture) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://f.test") |
| .Finish(true); |
| EXPECT_THAT(GetReportedSites(), |
| testing::ElementsAre("b.test", "c.test", "d.test, e.test")); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, |
| ReportRedirectorsInChain_IncludingUncommittedNavigations) { |
| // Visit initial page on a.test and access cookies via JS. |
| NavigateTo("http://a.test", kWithUserGesture); |
| AccessClientCookie(CookieOperation::kChange); |
| |
| // Start a redirect chain that doesn't commit. |
| StartNavigation("http://b.test", kWithUserGesture) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://c.test") |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://d.test") |
| .AccessCookie(CookieOperation::kChange) |
| .Finish(false); |
| EXPECT_THAT(GetReportedSites(), |
| testing::ElementsAre("b.test, c.test, d.test")); |
| |
| // Because the previous navigation didn't commit, the following chain still |
| // starts from http://a.test/. |
| StartNavigation("http://e.test", kWithUserGesture) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://f.test") |
| .AccessCookie(CookieOperation::kChange) |
| .Finish(true); |
| EXPECT_THAT(GetReportedSites(), |
| testing::ElementsAre("b.test, c.test, d.test", "e.test")); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, |
| ReportRedirectorsInChain_OmitNonStatefulRedirects) { |
| // Visit initial page on a.test and access cookies via JS. |
| NavigateTo("http://a.test", kWithUserGesture); |
| AccessClientCookie(CookieOperation::kChange); |
| |
| // Navigate with a click (not a redirect) to b.test, which S-redirects to |
| // c.test (which doesn't access cookies). |
| StartNavigation("http://b.test", kWithUserGesture) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://c.test") |
| .Finish(true); |
| EXPECT_THAT(GetReportedSites(), testing::ElementsAre("b.test")); |
| |
| // Navigate without a click (i.e. by C-redirecting) to d.test (which doesn't |
| // access cookies). |
| NavigateTo("http://d.test", kNoUserGesture); |
| EXPECT_THAT(GetReportedSites(), testing::ElementsAre("b.test")); |
| |
| // Navigate without a click (i.e. by C-redirecting) to e.test, which |
| // S-redirects to f.test. |
| StartNavigation("http://e.test", kNoUserGesture) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://f.test") |
| .Finish(true); |
| EXPECT_THAT(GetReportedSites(), testing::ElementsAre("b.test", "e.test")); |
| } |
| |
| // This test verifies that sites in a redirect chain that are the same as the |
| // starting site (i.e., last site before the redirect chain started) are not |
| // reported. |
| TEST_F(BtmBounceDetectorTest, |
| ReportRedirectorsInChain_OmitSitesMatchingStartSite) { |
| // Visit initial page on a.test and access cookies via JS. |
| NavigateTo("http://a.test", kWithUserGesture); |
| AccessClientCookie(CookieOperation::kChange); |
| |
| // Navigate with a click (not a redirect) to b.test, which S-redirects to |
| // a.test, which S-redirects to c.test. |
| StartNavigation("http://b.test", kWithUserGesture) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://a.test") |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://c.test") |
| .AccessCookie(CookieOperation::kChange) |
| .Finish(true); |
| EXPECT_THAT(GetReportedSites(), testing::ElementsAre("b.test")); |
| |
| // Navigate without a click (i.e. by C-redirecting) to a.test. |
| NavigateTo("http://a.test", kNoUserGesture); |
| EXPECT_THAT(GetReportedSites(), testing::ElementsAre("b.test", "c.test")); |
| // Access cookies via JS on a.test. |
| AccessClientCookie(CookieOperation::kChange); |
| |
| // Navigate without a click (i.e. by C-redirecting) to d.test, which |
| // S-redirects to e.test, which S-redirects to f.test. |
| StartNavigation("http://d.test", kNoUserGesture) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://e.test") |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://f.test") |
| .AccessCookie(CookieOperation::kChange) |
| .Finish(true); |
| EXPECT_THAT(GetReportedSites(), |
| testing::ElementsAre("b.test", "c.test", "d.test, e.test")); |
| } |
| |
| // This test verifies that sites in a (server) redirect chain that are the same |
| // as the ending site of a navigation are not reported. |
| TEST_F(BtmBounceDetectorTest, |
| ReportRedirectorsInChain_OmitSitesMatchingEndSite) { |
| // Visit initial page on a.test and access cookies via JS. |
| NavigateTo("http://a.test", kWithUserGesture); |
| AccessClientCookie(CookieOperation::kChange); |
| |
| // Navigate with a click (not a redirect) to b.test, which S-redirects to |
| // c.test, which S-redirects to c.test. |
| StartNavigation("http://b.test", kWithUserGesture) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://c.test") |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://c.test") |
| .AccessCookie(CookieOperation::kChange) |
| .Finish(true); |
| EXPECT_THAT(GetReportedSites(), testing::ElementsAre("b.test")); |
| |
| // Navigate without a click (i.e. by C-redirecting) to d.test. |
| NavigateTo("http://d.test", kNoUserGesture); |
| EXPECT_THAT(GetReportedSites(), testing::ElementsAre("b.test", "c.test")); |
| // Access cookies via JS on d.test. |
| AccessClientCookie(CookieOperation::kChange); |
| |
| // Navigate without a click (i.e. by C-redirecting) to e.test, which |
| // S-redirects to f.test, which S-redirects to e.test. |
| StartNavigation("http://e.test", kNoUserGesture) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://f.test") |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://e.test") |
| .AccessCookie(CookieOperation::kChange) |
| .Finish(true); |
| EXPECT_THAT(GetReportedSites(), |
| testing::ElementsAre("b.test", "c.test", "d.test, f.test")); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, |
| ReportRedirectorsInChain_OmitSitesMatchingEndSite_Uncommitted) { |
| // Visit initial page on a.test and access cookies via JS. |
| NavigateTo("http://a.test", kWithUserGesture); |
| AccessClientCookie(CookieOperation::kChange); |
| |
| // Navigate with a click (not a redirect) to b.test, which S-redirects to |
| // c.test, which S-redirects to c.test. |
| StartNavigation("http://b.test", kWithUserGesture) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://c.test") |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://c.test") |
| .AccessCookie(CookieOperation::kChange) |
| .Finish(false); |
| EXPECT_THAT(GetReportedSites(), testing::ElementsAre("b.test, c.test")); |
| |
| // Navigate without a click (i.e. by C-redirecting) to d.test. |
| // NOTE: Because the previous navigation didn't commit, the chain still |
| // starts from http://a.test/. |
| NavigateTo("http://d.test", kNoUserGesture); |
| EXPECT_THAT(GetReportedSites(), |
| testing::ElementsAre("b.test, c.test", "a.test")); |
| } |
| |
| const std::vector<std::string>& GetAllRedirectMetrics() { |
| static const std::vector<std::string> kAllRedirectMetrics = { |
| // clang-format off |
| "ClientBounceDelay", |
| "CookieAccessType", |
| "HasStickyActivation", |
| "InitialAndFinalSitesSame", |
| "RedirectAndFinalSiteSame", |
| "RedirectAndInitialSiteSame", |
| "RedirectChainIndex", |
| "RedirectChainLength", |
| "RedirectType", |
| "SiteEngagementLevel", |
| "WebAuthnAssertionRequestSucceeded", |
| // clang-format on |
| }; |
| return kAllRedirectMetrics; |
| } |
| |
| TEST_F(BtmBounceDetectorTest, Histograms_UMA) { |
| base::HistogramTester histograms; |
| |
| SetSiteHasUserActivation("http://b.test"); |
| |
| NavigateTo("http://a.test", kWithUserGesture); |
| NavigateTo("http://b.test", kWithUserGesture); |
| AdvanceBtmTime(base::Seconds(3)); |
| AccessClientCookie(CookieOperation::kRead); |
| StartNavigation("http://c.test", kNoUserGesture) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://d.test") |
| .Finish(true); |
| EndPendingRedirectChain(); |
| |
| base::HistogramTester::CountsMap expected_counts; |
| expected_counts["Privacy.DIPS.BounceCategoryClient.Block3PC"] = 1; |
| expected_counts["Privacy.DIPS.BounceCategoryServer.Block3PC"] = 1; |
| EXPECT_THAT(histograms.GetTotalCountsForPrefix("Privacy.DIPS.BounceCategory"), |
| testing::ContainerEq(expected_counts)); |
| // Verify the proper values were recorded. b.test has user engagement and read |
| // cookies, while c.test has no user engagement and wrote cookies. |
| EXPECT_THAT( |
| histograms.GetAllSamples("Privacy.DIPS.BounceCategoryClient.Block3PC"), |
| testing::ElementsAre( |
| // b.test |
| Bucket((int)BtmRedirectCategory::kReadCookies_HasEngagement, 1))); |
| EXPECT_THAT( |
| histograms.GetAllSamples("Privacy.DIPS.BounceCategoryServer.Block3PC"), |
| testing::ElementsAre( |
| // c.test |
| Bucket((int)BtmRedirectCategory::kWriteCookies_NoEngagement, 1))); |
| |
| // Verify the time-to-bounce metric was recorded for the client bounce. |
| histograms.ExpectBucketCount( |
| "Privacy.DIPS.TimeFromNavigationCommitToClientBounce", |
| static_cast<base::HistogramBase::Sample32>( |
| base::Seconds(3).InMilliseconds()), |
| /*expected_count=*/1); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, Histograms_UKM) { |
| ukm::TestAutoSetUkmRecorder ukm_recorder; |
| |
| SetSiteHasUserActivation("http://c.test"); |
| |
| NavigateTo("http://a.test", kWithUserGesture); |
| NavigateTo("http://b.test", kWithUserGesture); |
| AdvanceBtmTime(base::Seconds(2)); |
| AccessClientCookie(CookieOperation::kRead); |
| TriggerWebAuthnAssertionRequestSucceeded(); |
| StartNavigation("http://c.test", kNoUserGesture) |
| .AccessCookie(CookieOperation::kChange) |
| .RedirectTo("http://d.test") |
| .Finish(true); |
| |
| EndPendingRedirectChain(); |
| |
| std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry> ukm_entries = |
| ukm_recorder.GetEntries("DIPS.Redirect", GetAllRedirectMetrics()); |
| ASSERT_EQ(2u, ukm_entries.size()); |
| |
| EXPECT_THAT(URLForNavigationSourceId(ukm_entries[0].source_id), |
| Eq("b.test/")); |
| EXPECT_THAT( |
| ukm_entries[0].metrics, |
| ElementsAre(Pair("ClientBounceDelay", 2), |
| Pair("CookieAccessType", (int)BtmDataAccessType::kRead), |
| Pair("HasStickyActivation", false), |
| Pair("InitialAndFinalSitesSame", false), |
| Pair("RedirectAndFinalSiteSame", false), |
| Pair("RedirectAndInitialSiteSame", false), |
| Pair("RedirectChainIndex", 0), Pair("RedirectChainLength", 2), |
| Pair("RedirectType", (int)BtmRedirectType::kClient), |
| Pair("SiteEngagementLevel", 0), |
| Pair("WebAuthnAssertionRequestSucceeded", true))); |
| |
| EXPECT_THAT(URLForRedirectSourceId(&ukm_recorder, ukm_entries[1].source_id), |
| Eq("c.test/")); |
| EXPECT_THAT( |
| ukm_entries[1].metrics, |
| ElementsAre(Pair("ClientBounceDelay", 0), |
| Pair("CookieAccessType", (int)BtmDataAccessType::kWrite), |
| Pair("HasStickyActivation", false), |
| Pair("InitialAndFinalSitesSame", false), |
| Pair("RedirectAndFinalSiteSame", false), |
| Pair("RedirectAndInitialSiteSame", false), |
| Pair("RedirectChainIndex", 1), Pair("RedirectChainLength", 2), |
| Pair("RedirectType", (int)BtmRedirectType::kServer), |
| Pair("SiteEngagementLevel", 1), |
| Pair("WebAuthnAssertionRequestSucceeded", false))); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, SiteHadUserActivationInteraction) { |
| NavigateTo("http://a.test", kWithUserGesture); |
| ActivatePage(); |
| AdvanceBtmTime(features::kBtmClientBounceDetectionTimeout.Get() + |
| base::Seconds(1)); |
| |
| StartNavigation("http://b.test", kNoUserGesture) |
| .RedirectTo("http://c.test") |
| .Finish(/*commit=*/true); |
| ActivatePage(); |
| NavigateTo("http://d.test", kNoUserGesture); |
| |
| // Expect one initial URL (a.test) and two redirects (b.test, c.test). |
| EXPECT_EQ(CommittedRedirectContext().GetInitialURLForTesting(), |
| GURL("http://a.test")); |
| EXPECT_EQ(CommittedRedirectContext().GetRedirectChainLength(), 2u); |
| |
| EXPECT_TRUE( |
| CommittedRedirectContext().SiteHadUserActivationOrAuthn("a.test")); |
| EXPECT_FALSE( |
| CommittedRedirectContext().SiteHadUserActivationOrAuthn("b.test")); |
| EXPECT_TRUE( |
| CommittedRedirectContext().SiteHadUserActivationOrAuthn("c.test")); |
| EXPECT_FALSE( |
| CommittedRedirectContext().SiteHadUserActivationOrAuthn("d.test")); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, SiteHadWebAuthnInteraction) { |
| NavigateTo("http://a.test", kWithUserGesture); |
| ActivatePage(); |
| AdvanceBtmTime(features::kBtmClientBounceDetectionTimeout.Get() + |
| base::Seconds(1)); |
| |
| StartNavigation("http://b.test", kNoUserGesture) |
| .RedirectTo("http://c.test") |
| .Finish(/*commit=*/true); |
| TriggerWebAuthnAssertionRequestSucceeded(); |
| NavigateTo("http://d.test", kNoUserGesture); |
| |
| // Expect one initial URL (a.test) and two redirects (b.test, c.test). |
| EXPECT_EQ(CommittedRedirectContext().GetInitialURLForTesting(), |
| GURL("http://a.test")); |
| EXPECT_EQ(CommittedRedirectContext().GetRedirectChainLength(), 2u); |
| |
| EXPECT_TRUE( |
| CommittedRedirectContext().SiteHadUserActivationOrAuthn("a.test")); |
| EXPECT_FALSE( |
| CommittedRedirectContext().SiteHadUserActivationOrAuthn("b.test")); |
| EXPECT_TRUE( |
| CommittedRedirectContext().SiteHadUserActivationOrAuthn("c.test")); |
| EXPECT_FALSE( |
| CommittedRedirectContext().SiteHadUserActivationOrAuthn("d.test")); |
| } |
| |
| TEST_F(BtmBounceDetectorTest, ClientCookieAccessDuringNavigation) { |
| NavigateTo("http://a.test", kWithUserGesture); |
| NavigateTo("http://b.test", kWithUserGesture); |
| |
| auto nav = StartNavigation("http://c.test", kNoUserGesture); |
| // b.test accesses cookies after the navigation started. |
| AccessClientCookie(CookieOperation::kChange); |
| nav.Finish(true); |
| |
| EndPendingRedirectChain(); |
| |
| // The b.test bounce is considered stateful. |
| EXPECT_THAT( |
| redirects(), |
| testing::ElementsAre(("[1/1] a.test/ -> b.test/ (Write) -> c.test/"))); |
| EXPECT_THAT(GetRecordedBounces(), |
| testing::ElementsAre(testing::FieldsAre( |
| GURL("http://b.test"), testing::_, /*stateful=*/true))); |
| EXPECT_EQ(stateful_bounce_count(), 1); |
| } |
| |
| using ChainPair = |
| std::pair<BtmRedirectChainInfoPtr, std::vector<BtmRedirectInfoPtr>>; |
| |
| void AppendChainPair(std::vector<ChainPair>& vec, |
| std::vector<BtmRedirectInfoPtr> redirects, |
| BtmRedirectChainInfoPtr chain) { |
| vec.emplace_back(std::move(chain), std::move(redirects)); |
| } |
| |
| std::vector<BtmRedirectInfoPtr> MakeServerRedirects( |
| std::vector<std::string> urls, |
| BtmDataAccessType access_type = BtmDataAccessType::kReadWrite) { |
| std::vector<BtmRedirectInfoPtr> redirects; |
| for (const auto& url : urls) { |
| redirects.push_back(BtmRedirectInfo::CreateForServer( |
| /*url=*/MakeUrlAndId(url), |
| /*access_type=*/access_type, |
| /*time=*/base::Time::Now(), |
| /*was_response_cached=*/false, |
| /*response_code=*/net::HTTP_FOUND, |
| /*server_bounce_delay=*/base::TimeDelta())); |
| } |
| return redirects; |
| } |
| |
| BtmRedirectInfoPtr MakeClientRedirect( |
| std::string url, |
| BtmDataAccessType access_type = BtmDataAccessType::kReadWrite, |
| bool has_sticky_activation = false, |
| bool has_web_authn_assertion = false) { |
| return BtmRedirectInfo::CreateForClient( |
| /*url=*/MakeUrlAndId(url), |
| /*access_type=*/access_type, |
| /*time=*/base::Time::Now(), |
| /*client_bounce_delay=*/base::Seconds(1), |
| /*has_sticky_activation=*/has_sticky_activation, |
| /*web_authn_assertion_request_succeeded*/ has_web_authn_assertion); |
| } |
| |
| MATCHER_P(HasUrl, url, "") { |
| *result_listener << "whose url is " << arg->url.url; |
| return ExplainMatchResult(Eq(url), arg->url.url, result_listener); |
| } |
| |
| MATCHER_P(HasRedirectType, redirect_type, "") { |
| *result_listener << "whose redirect_type is " |
| << BtmRedirectTypeToString(arg->redirect_type); |
| return ExplainMatchResult(Eq(redirect_type), arg->redirect_type, |
| result_listener); |
| } |
| |
| MATCHER_P(HasBtmDataAccessType, access_type, "") { |
| *result_listener << "whose access_type is " |
| << BtmDataAccessTypeToString(arg->access_type); |
| return ExplainMatchResult(Eq(access_type), arg->access_type, result_listener); |
| } |
| |
| MATCHER_P(HasInitialUrl, url, "") { |
| *result_listener << "whose initial_url is " << arg->initial_url.url; |
| return ExplainMatchResult(Eq(url), arg->initial_url.url, result_listener); |
| } |
| |
| MATCHER_P(HasFinalUrl, url, "") { |
| *result_listener << "whose final_url is " << arg->final_url.url; |
| return ExplainMatchResult(Eq(url), arg->final_url.url, result_listener); |
| } |
| |
| MATCHER_P(HasLength, length, "") { |
| *result_listener << "whose length is " << arg->length; |
| return ExplainMatchResult(Eq(length), arg->length, result_listener); |
| } |
| |
| TEST(BtmRedirectContextTest, OneAppend) { |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| ASSERT_EQ(chains.size(), 0u); |
| context.AppendCommitted( |
| MakeUrlAndId("http://a.test/"), |
| MakeServerRedirects({"http://b.test/", "http://c.test/"}), |
| MakeUrlAndId("http://d.test/"), false); |
| ASSERT_EQ(chains.size(), 0u); |
| context.EndChain(MakeUrlAndId("http://d.test/"), false); |
| |
| ASSERT_EQ(chains.size(), 1u); |
| EXPECT_THAT(chains[0].first, |
| AllOf(HasInitialUrl("http://a.test/"), |
| HasFinalUrl("http://d.test/"), HasLength(2u))); |
| EXPECT_THAT(chains[0].second, |
| ElementsAre(HasUrl("http://b.test/"), HasUrl("http://c.test/"))); |
| } |
| |
| TEST(BtmRedirectContextTest, TwoAppends_NoClientRedirect) { |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| ASSERT_EQ(chains.size(), 0u); |
| context.AppendCommitted( |
| MakeUrlAndId("http://a.test/"), |
| MakeServerRedirects({"http://b.test/", "http://c.test/"}), |
| MakeUrlAndId("http://d.test/"), false); |
| ASSERT_EQ(chains.size(), 0u); |
| context.AppendCommitted(MakeUrlAndId("http://d.test/"), |
| MakeServerRedirects({"http://e.test/"}), |
| MakeUrlAndId("http://f.test/"), false); |
| ASSERT_EQ(chains.size(), 1u); |
| context.EndChain(MakeUrlAndId("http://f.test/"), false); |
| |
| ASSERT_EQ(chains.size(), 2u); |
| EXPECT_THAT(chains[0].first, |
| AllOf(HasInitialUrl("http://a.test/"), |
| HasFinalUrl("http://d.test/"), HasLength(2u))); |
| EXPECT_THAT(chains[0].second, |
| ElementsAre(HasUrl("http://b.test/"), HasUrl("http://c.test/"))); |
| |
| EXPECT_THAT(chains[1].first, |
| AllOf(HasInitialUrl("http://d.test/"), |
| HasFinalUrl("http://f.test/"), HasLength(1u))); |
| EXPECT_THAT(chains[1].second, ElementsAre(HasUrl("http://e.test/"))); |
| } |
| |
| TEST(BtmRedirectContextTest, TwoAppends_WithClientRedirect) { |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| ASSERT_EQ(chains.size(), 0u); |
| context.AppendCommitted( |
| MakeUrlAndId("http://a.test/"), |
| MakeServerRedirects({"http://b.test/", "http://c.test/"}), |
| MakeUrlAndId("http://d.test/"), false); |
| ASSERT_EQ(chains.size(), 0u); |
| context.AppendCommitted( |
| MakeClientRedirect("http://d.test/"), |
| MakeServerRedirects({"http://e.test/", "http://f.test/"}), |
| MakeUrlAndId("http://g.test/"), false); |
| ASSERT_EQ(chains.size(), 0u); |
| context.EndChain(MakeUrlAndId("http://g.test/"), false); |
| |
| ASSERT_EQ(chains.size(), 1u); |
| EXPECT_THAT(chains[0].first, |
| AllOf(HasInitialUrl("http://a.test/"), |
| HasFinalUrl("http://g.test/"), HasLength(5u))); |
| EXPECT_THAT(chains[0].second, |
| ElementsAre(AllOf(HasUrl("http://b.test/"), |
| HasRedirectType(BtmRedirectType::kServer)), |
| AllOf(HasUrl("http://c.test/"), |
| HasRedirectType(BtmRedirectType::kServer)), |
| AllOf(HasUrl("http://d.test/"), |
| HasRedirectType(BtmRedirectType::kClient)), |
| AllOf(HasUrl("http://e.test/"), |
| HasRedirectType(BtmRedirectType::kServer)), |
| AllOf(HasUrl("http://f.test/"), |
| HasRedirectType(BtmRedirectType::kServer)))); |
| } |
| |
| TEST(BtmRedirectContextTest, OnlyClientRedirects) { |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| ASSERT_EQ(chains.size(), 0u); |
| context.AppendCommitted(MakeUrlAndId("http://a.test/"), {}, |
| MakeUrlAndId("http://b.test/"), false); |
| ASSERT_EQ(chains.size(), 0u); |
| context.AppendCommitted(MakeClientRedirect("http://b.test/"), {}, |
| MakeUrlAndId("http://c.test/"), false); |
| ASSERT_EQ(chains.size(), 0u); |
| context.AppendCommitted(MakeClientRedirect("http://c.test/"), {}, |
| MakeUrlAndId("http://d.test/"), false); |
| ASSERT_EQ(chains.size(), 0u); |
| context.EndChain(MakeUrlAndId("http://d.test"), false); |
| |
| ASSERT_EQ(chains.size(), 1u); |
| EXPECT_THAT(chains[0].first, |
| AllOf(HasInitialUrl("http://a.test/"), |
| HasFinalUrl("http://d.test/"), HasLength(2u))); |
| EXPECT_THAT(chains[0].second, |
| ElementsAre(HasUrl("http://b.test/"), HasUrl("http://c.test/"))); |
| } |
| |
| TEST(BtmRedirectContextTest, OverflowMaxChain_TrimsFromFront) { |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| context.AppendCommitted(MakeUrlAndId("http://a.test/"), {}, |
| MakeUrlAndId("http://c.test/"), false); |
| for (size_t ind = 0; ind < kBtmRedirectChainMax; ind++) { |
| std::string redirect_url = |
| base::StrCat({"http://", base::NumberToString(ind), ".test/"}); |
| context.AppendCommitted(MakeClientRedirect(redirect_url), {}, |
| MakeUrlAndId("http://c.test/"), false); |
| } |
| // Each redirect was added to the chain. |
| ASSERT_EQ(context.size(), kBtmRedirectChainMax); |
| ASSERT_EQ(chains.size(), 0u); |
| |
| // The next redirect overflows the chain and evicts the first one. |
| context.AppendCommitted(MakeClientRedirect("http://b.test/"), {}, |
| MakeUrlAndId("http://c.test/"), false); |
| ASSERT_EQ(context.size(), kBtmRedirectChainMax); |
| ASSERT_EQ(chains.size(), 1u); |
| context.EndChain(MakeUrlAndId("http://c.test/"), false); |
| |
| // Expect two chains handled: one partial chain with the dropped redirect, and |
| // one with the other redirects. |
| ASSERT_EQ(chains.size(), 2u); |
| EXPECT_THAT(chains[0].first, AllOf(HasInitialUrl("http://a.test/"), |
| HasLength(kBtmRedirectChainMax + 1))); |
| ASSERT_THAT(chains[0].second, SizeIs(1)); |
| EXPECT_THAT(chains[0].second.at(0), |
| AllOf(HasUrl("http://0.test/"), |
| HasRedirectType(BtmRedirectType::kClient))); |
| |
| // BtmRedirectChainInfo.length is computed from BtmRedirectInfo.index, so it |
| // includes the length of the partial chains. |
| EXPECT_THAT(chains[1].first, AllOf(HasInitialUrl("http://a.test/"), |
| HasFinalUrl("http://c.test/"), |
| HasLength(kBtmRedirectChainMax + 1))); |
| ASSERT_THAT(chains[1].second, SizeIs(kBtmRedirectChainMax)); |
| // Check that the first redirect in the chain is the second that was added in |
| // the setup. |
| EXPECT_THAT(chains[1].second.at(0), |
| AllOf(HasUrl("http://1.test/"), |
| HasRedirectType(BtmRedirectType::kClient))); |
| // Check the last redirect in the full chain. |
| EXPECT_THAT(chains[1].second.back(), |
| AllOf(HasUrl("http://b.test/"), |
| HasRedirectType(BtmRedirectType::kClient))); |
| } |
| |
| TEST(BtmRedirectContextTest, Uncommitted_NoClientRedirects) { |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| ASSERT_EQ(chains.size(), 0u); |
| context.AppendCommitted( |
| MakeUrlAndId("http://a.test/"), |
| MakeServerRedirects({"http://b.test/", "http://c.test/"}), |
| MakeUrlAndId("http://d.test/"), false); |
| ASSERT_EQ(chains.size(), 0u); |
| context.HandleUncommitted( |
| MakeUrlAndId("http://d.test/"), |
| MakeServerRedirects({"http://e.test/", "http://f.test/"})); |
| ASSERT_EQ(chains.size(), 1u); |
| context.AppendCommitted(MakeUrlAndId("http://h.test/"), |
| MakeServerRedirects({"http://i.test/"}), |
| MakeUrlAndId("http://j.test/"), false); |
| ASSERT_EQ(chains.size(), 2u); |
| context.EndChain(MakeUrlAndId("http://j.test/"), false); |
| |
| ASSERT_EQ(chains.size(), 3u); |
| // First, the uncommitted (middle) chain. |
| EXPECT_THAT(chains[0].first, |
| AllOf(HasInitialUrl("http://d.test/"), |
| HasFinalUrl("http://d.test/"), HasLength(2u))); |
| EXPECT_THAT(chains[0].second, |
| ElementsAre(HasUrl("http://e.test/"), HasUrl("http://f.test/"))); |
| // Then the initially-started chain. |
| EXPECT_THAT(chains[1].first, |
| AllOf(HasInitialUrl("http://a.test/"), |
| HasFinalUrl("http://h.test/"), HasLength(2u))); |
| EXPECT_THAT(chains[1].second, |
| ElementsAre(HasUrl("http://b.test/"), HasUrl("http://c.test/"))); |
| // Then the last chain. |
| EXPECT_THAT(chains[2].first, |
| AllOf(HasInitialUrl("http://h.test/"), |
| HasFinalUrl("http://j.test/"), HasLength(1u))); |
| EXPECT_THAT(chains[2].second, ElementsAre(HasUrl("http://i.test/"))); |
| } |
| |
| TEST(BtmRedirectContextTest, Uncommitted_IncludingClientRedirects) { |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| ASSERT_EQ(chains.size(), 0u); |
| context.AppendCommitted( |
| MakeUrlAndId("http://a.test/"), |
| MakeServerRedirects({"http://b.test/", "http://c.test/"}), |
| MakeUrlAndId("http://d.test/"), false); |
| ASSERT_EQ(chains.size(), 0u); |
| // Uncommitted navigation: |
| context.HandleUncommitted( |
| MakeClientRedirect("http://d.test/"), |
| MakeServerRedirects({"http://e.test/", "http://f.test/"})); |
| ASSERT_EQ(chains.size(), 1u); |
| context.AppendCommitted(MakeClientRedirect("http://h.test/"), |
| MakeServerRedirects({"http://i.test/"}), |
| MakeUrlAndId("http://j.test/"), false); |
| ASSERT_EQ(chains.size(), 1u); |
| context.EndChain(MakeUrlAndId("http://j.test/"), false); |
| |
| ASSERT_EQ(chains.size(), 2u); |
| // First, the uncommitted chain. The overall length includes the |
| // already-committed part of the chain (2 redirects, starting from a.test) |
| // plus the uncommitted part (3 redirects, starting from d.test). |
| EXPECT_THAT(chains[0].first, |
| AllOf(HasInitialUrl("http://a.test/"), |
| HasFinalUrl("http://d.test/"), HasLength(5u))); |
| // But only the 3 uncommitted redirects are included in the vector. |
| EXPECT_THAT(chains[0].second, |
| ElementsAre(HasUrl("http://d.test/"), HasUrl("http://e.test/"), |
| HasUrl("http://f.test/"))); |
| // Then the initially-started chain. |
| EXPECT_THAT(chains[1].first, |
| AllOf(HasInitialUrl("http://a.test/"), |
| HasFinalUrl("http://j.test/"), HasLength(4u))); |
| // Committed chains include all redirects in the vector. |
| EXPECT_THAT(chains[1].second, |
| ElementsAre(HasUrl("http://b.test/"), HasUrl("http://c.test/"), |
| HasUrl("http://h.test/"), HasUrl("http://i.test/"))); |
| } |
| |
| TEST(BtmRedirectContextTest, NoRedirects) { |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| ASSERT_EQ(chains.size(), 0u); |
| |
| context.AppendCommitted(MakeUrlAndId("http://a.test/"), {}, |
| MakeUrlAndId("http://b.test/"), false); |
| ASSERT_EQ(chains.size(), 0u); |
| |
| context.AppendCommitted(MakeUrlAndId("http://b.test/"), {}, |
| MakeUrlAndId("http://c.test/"), false); |
| ASSERT_EQ(chains.size(), 1u); |
| |
| context.EndChain(MakeUrlAndId("http://e.test/"), false); |
| ASSERT_EQ(chains.size(), 2u); |
| |
| EXPECT_THAT(chains[0].first, |
| AllOf(HasInitialUrl("http://a.test/"), |
| HasFinalUrl("http://b.test/"), HasLength(0u))); |
| EXPECT_THAT(chains[0].second, IsEmpty()); |
| |
| EXPECT_THAT(chains[1].first, |
| AllOf(HasInitialUrl("http://b.test/"), |
| HasFinalUrl("http://e.test/"), HasLength(0u))); |
| EXPECT_THAT(chains[1].second, IsEmpty()); |
| } |
| |
| TEST(BtmRedirectContextTest, AddLateCookieAccess) { |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| |
| context.AppendCommitted( |
| MakeUrlAndId("http://a.test/"), |
| MakeServerRedirects( |
| {"http://b.test/", "http://c.test/", "http://d.test/"}, |
| BtmDataAccessType::kNone), |
| MakeUrlAndId("http://e.test/"), false); |
| |
| EXPECT_TRUE(context.AddLateCookieAccess(GURL("http://d.test/"), |
| CookieOperation::kChange)); |
| // Update c.test even though it preceded d.test: |
| EXPECT_TRUE(context.AddLateCookieAccess(GURL("http://c.test/"), |
| CookieOperation::kRead)); |
| |
| context.AppendCommitted( |
| MakeClientRedirect("http://e.test/", BtmDataAccessType::kNone), |
| MakeServerRedirects({"http://f.test/", "http://g.test/"}, |
| BtmDataAccessType::kRead), |
| MakeUrlAndId("http://h.test/"), false); |
| |
| context.AppendCommitted( |
| MakeClientRedirect("http://h.test/", BtmDataAccessType::kNone), |
| MakeServerRedirects({"http://i.test/"}, BtmDataAccessType::kRead), |
| MakeUrlAndId("http://j.test/"), false); |
| |
| // Since kMaxLookback=5, AddLateCookieAccess() can attribute late accesses to |
| // the last 5 redirects: |
| EXPECT_TRUE(context.AddLateCookieAccess(GURL("http://i.test/"), |
| CookieOperation::kRead)); |
| EXPECT_TRUE(context.AddLateCookieAccess(GURL("http://h.test/"), |
| CookieOperation::kRead)); |
| EXPECT_TRUE(context.AddLateCookieAccess(GURL("http://g.test/"), |
| CookieOperation::kChange)); |
| EXPECT_TRUE(context.AddLateCookieAccess(GURL("http://f.test/"), |
| CookieOperation::kRead)); |
| EXPECT_TRUE(context.AddLateCookieAccess(GURL("http://e.test/"), |
| CookieOperation::kRead)); |
| // But it will fail to update d.test since it's too far back in the chain. |
| EXPECT_FALSE(context.AddLateCookieAccess(GURL("http://d.test/"), |
| CookieOperation::kRead)); |
| |
| context.EndChain(MakeUrlAndId("http://j.test/"), false); |
| |
| ASSERT_EQ(chains.size(), 1u); |
| EXPECT_THAT(chains[0].first, |
| AllOf(HasInitialUrl("http://a.test/"), |
| HasFinalUrl("http://j.test/"), HasLength(8u))); |
| EXPECT_THAT( |
| chains[0].second, |
| ElementsAre(AllOf(HasUrl("http://b.test/"), |
| HasBtmDataAccessType(BtmDataAccessType::kNone)), |
| AllOf(HasUrl("http://c.test/"), |
| HasBtmDataAccessType(BtmDataAccessType::kRead)), |
| AllOf(HasUrl("http://d.test/"), |
| HasBtmDataAccessType(BtmDataAccessType::kWrite)), |
| AllOf(HasUrl("http://e.test/"), |
| HasBtmDataAccessType(BtmDataAccessType::kRead)), |
| AllOf(HasUrl("http://f.test/"), |
| HasBtmDataAccessType(BtmDataAccessType::kRead)), |
| AllOf(HasUrl("http://g.test/"), |
| HasBtmDataAccessType(BtmDataAccessType::kReadWrite)), |
| AllOf(HasUrl("http://h.test/"), |
| HasBtmDataAccessType(BtmDataAccessType::kRead)), |
| AllOf(HasUrl("http://i.test/"), |
| HasBtmDataAccessType(BtmDataAccessType::kRead)))); |
| } |
| |
| TEST(BtmRedirectContextTest, GetRedirectHeuristicURLs_NoRequirements) { |
| base::test::ScopedFeatureList features; |
| features.InitAndEnableFeatureWithParameters( |
| content_settings::features::kTpcdHeuristicsGrants, |
| {{"TpcdRedirectHeuristicRequireABAFlow", "false"}}); |
| |
| UrlAndSourceId first_party_url = MakeUrlAndId("http://a.test/"); |
| UrlAndSourceId current_interaction_url = MakeUrlAndId("http://b.test/"); |
| GURL no_current_interaction_url("http://c.test/"); |
| |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| |
| context.AppendCommitted(first_party_url, |
| {MakeServerRedirects({"http://c.test"})}, |
| current_interaction_url, false); |
| context.AppendCommitted( |
| MakeClientRedirect("http://b.test/", BtmDataAccessType::kNone, |
| /*has_sticky_activation=*/true), |
| {}, first_party_url, false); |
| |
| ASSERT_EQ(context.size(), 2u); |
| |
| std::map<std::string, std::pair<GURL, bool>> |
| sites_to_url_and_current_interaction = context.GetRedirectHeuristicURLs( |
| first_party_url.url, std::nullopt, |
| /*require_current_interaction=*/false); |
| EXPECT_THAT( |
| sites_to_url_and_current_interaction, |
| testing::UnorderedElementsAre( |
| std::pair<std::string, std::pair<GURL, bool>>( |
| "b.test", std::make_pair(current_interaction_url.url, true)), |
| std::pair<std::string, std::pair<GURL, bool>>( |
| "c.test", std::make_pair(no_current_interaction_url, false)))); |
| } |
| |
| TEST(BtmRedirectContextTest, GetRedirectHeuristicURLs_RequireABAFlow) { |
| base::test::ScopedFeatureList features; |
| features.InitAndEnableFeatureWithParameters( |
| content_settings::features::kTpcdHeuristicsGrants, |
| {{"TpcdRedirectHeuristicRequireABAFlow", "true"}}); |
| |
| UrlAndSourceId first_party_url = MakeUrlAndId("http://a.test/"); |
| GURL aba_url("http://b.test/"); |
| GURL no_aba_url("http://c.test/"); |
| |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| |
| context.AppendCommitted( |
| first_party_url, |
| {MakeServerRedirects({"http://b.test", "http://c.test"})}, |
| first_party_url, false); |
| |
| ASSERT_EQ(context.size(), 2u); |
| |
| std::set<std::string> allowed_sites = {GetSiteForBtm(aba_url)}; |
| |
| std::map<std::string, std::pair<GURL, bool>> |
| sites_to_url_and_current_interaction = context.GetRedirectHeuristicURLs( |
| first_party_url.url, allowed_sites, |
| /*require_current_interaction=*/false); |
| EXPECT_THAT(sites_to_url_and_current_interaction, |
| testing::UnorderedElementsAre( |
| std::pair<std::string, std::pair<GURL, bool>>( |
| "b.test", std::make_pair(aba_url, false)))); |
| } |
| |
| TEST(BtmRedirectContextTest, |
| GetRedirectHeuristicURLs_RequireCurrentInteraction) { |
| base::test::ScopedFeatureList features; |
| features.InitAndEnableFeatureWithParameters( |
| content_settings::features::kTpcdHeuristicsGrants, |
| {{"TpcdRedirectHeuristicRequireABAFlow", "false"}}); |
| |
| UrlAndSourceId first_party_url = MakeUrlAndId("http://a.test/"); |
| UrlAndSourceId current_interaction_url = MakeUrlAndId("http://b.test/"); |
| GURL no_current_interaction_url("http://c.test/"); |
| |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| |
| context.AppendCommitted(first_party_url, |
| {MakeServerRedirects({"http://c.test"})}, |
| current_interaction_url, false); |
| context.AppendCommitted( |
| MakeClientRedirect("http://b.test/", BtmDataAccessType::kNone, |
| /*has_sticky_activation=*/false, true), |
| {}, first_party_url, false); |
| |
| ASSERT_EQ(context.size(), 2u); |
| |
| std::map<std::string, std::pair<GURL, bool>> |
| sites_to_url_and_current_interaction = context.GetRedirectHeuristicURLs( |
| first_party_url.url, std::nullopt, |
| /*require_current_interaction=*/true); |
| EXPECT_THAT( |
| sites_to_url_and_current_interaction, |
| testing::UnorderedElementsAre( |
| std::pair<std::string, std::pair<GURL, bool>>( |
| "b.test", std::make_pair(current_interaction_url.url, true)))); |
| } |
| |
| TEST(BtmRedirectContextTest, |
| GetServerRedirectsSinceLastPrimaryPageChangeNoRedirects) { |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| ASSERT_EQ(context.size(), 0u); |
| |
| base::span<const BtmRedirectInfoPtr> server_redirects = |
| context.GetServerRedirectsSinceLastPrimaryPageChange(); |
| |
| EXPECT_EQ(server_redirects.size(), 0u); |
| } |
| |
| TEST(BtmRedirectContextTest, |
| GetServerRedirectsSinceLastPrimaryPageChangeOnlyClientSideRedirects) { |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| context.AppendCommitted( |
| MakeClientRedirect("http://a.test/", BtmDataAccessType::kNone, false, |
| true), |
| {}, MakeUrlAndId("http://b.test/"), false); |
| ASSERT_EQ(context.size(), 1u); |
| |
| base::span<const BtmRedirectInfoPtr> server_redirects = |
| context.GetServerRedirectsSinceLastPrimaryPageChange(); |
| |
| EXPECT_EQ(server_redirects.size(), 0u); |
| } |
| |
| TEST(BtmRedirectContextTest, |
| GetServerRedirectsSinceLastPrimaryPageChangeOnlyServerSideRedirects) { |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| context.AppendCommitted( |
| MakeUrlAndId("http://a.test/"), |
| {MakeServerRedirects({"http://b.test/", "http://c.test/"})}, |
| MakeUrlAndId("http://d.test"), false); |
| ASSERT_EQ(context.size(), 2u); |
| |
| base::span<const BtmRedirectInfoPtr> server_redirects = |
| context.GetServerRedirectsSinceLastPrimaryPageChange(); |
| |
| EXPECT_EQ(server_redirects.size(), 2u); |
| EXPECT_EQ(server_redirects[0]->url.url, "http://b.test/"); |
| EXPECT_EQ(server_redirects[0]->redirect_type, BtmRedirectType::kServer); |
| EXPECT_EQ(server_redirects[1]->url.url, "http://c.test/"); |
| EXPECT_EQ(server_redirects[1]->redirect_type, BtmRedirectType::kServer); |
| } |
| |
| TEST( |
| BtmRedirectContextTest, |
| GetServerRedirectsSinceLastPrimaryPageChangeNoServerSideRedirectsSinceLastClientSideRedirect) { |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| context.AppendCommitted(MakeUrlAndId("http://a.test"), |
| {MakeServerRedirects({"http://b.test"})}, |
| MakeUrlAndId("http://c.test/"), false); |
| context.AppendCommitted( |
| MakeClientRedirect("http://b.test/", BtmDataAccessType::kNone, false, |
| true), |
| {}, MakeUrlAndId("http://d.test/"), false); |
| ASSERT_EQ(context.size(), 2u); |
| |
| base::span<const BtmRedirectInfoPtr> server_redirects = |
| context.GetServerRedirectsSinceLastPrimaryPageChange(); |
| |
| EXPECT_EQ(server_redirects.size(), 0u); |
| } |
| |
| TEST( |
| BtmRedirectContextTest, |
| GetServerRedirectsSinceLastPrimaryPageChangeServerSideRedirectsPrecededByClientSideRedirect) { |
| std::vector<ChainPair> chains; |
| BtmRedirectContext context( |
| base::BindRepeating(AppendChainPair, std::ref(chains)), base::DoNothing(), |
| UrlAndSourceId(), |
| /*redirect_prefix_count=*/0); |
| context.AppendCommitted(MakeUrlAndId("http://a.test/"), |
| {MakeServerRedirects({"http://b.test/"})}, |
| MakeUrlAndId("http://c.test/"), false); |
| context.AppendCommitted( |
| MakeClientRedirect("http://b.test/", BtmDataAccessType::kNone, false, |
| true), |
| MakeServerRedirects({"http://a.test/server-redirect/"}), |
| MakeUrlAndId("http://d.test/"), false); |
| ASSERT_EQ(context.size(), 3u); |
| ASSERT_EQ(context[0].redirect_type, BtmRedirectType::kServer); |
| ASSERT_EQ(context[1].redirect_type, BtmRedirectType::kClient); |
| ASSERT_EQ(context[2].redirect_type, BtmRedirectType::kServer); |
| |
| base::span<const BtmRedirectInfoPtr> server_redirects = |
| context.GetServerRedirectsSinceLastPrimaryPageChange(); |
| |
| EXPECT_EQ(server_redirects.size(), 1u); |
| EXPECT_EQ(server_redirects[0]->url.url, "http://a.test/server-redirect/"); |
| EXPECT_EQ(server_redirects[0]->redirect_type, BtmRedirectType::kServer); |
| } |
| |
| } // namespace content |