| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef CONTENT_BROWSER_BTM_BTM_TEST_UTILS_H_ |
| #define CONTENT_BROWSER_BTM_BTM_TEST_UTILS_H_ |
| |
| #include <iosfwd> |
| #include <string> |
| #include <string_view> |
| #include <vector> |
| |
| #include "base/run_loop.h" |
| #include "base/scoped_observation.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/types/expected.h" |
| #include "components/content_settings/core/common/cookie_settings_base.h" |
| #include "components/content_settings/core/common/host_indexed_content_settings.h" |
| #include "components/ukm/test_ukm_recorder.h" |
| #include "content/browser/btm/btm_service_impl.h" |
| #include "content/browser/btm/btm_utils.h" |
| #include "content/browser/renderer_host/cookie_access_observers.h" |
| #include "content/public/browser/btm_redirect_info.h" |
| #include "content/public/browser/btm_service.h" |
| #include "content/public/browser/cookie_access_details.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "services/network/public/mojom/cookie_access_observer.mojom.h" |
| #include "url/gurl.h" |
| |
| namespace testing { |
| class MatchResultListener; |
| } |
| |
| namespace content { |
| |
| constexpr char kStorageAccessScript[] = R"( |
| function accessLocalStorage() { |
| localStorage.setItem('foo', 'bar'); |
| return localStorage.getItem('foo'); |
| } |
| |
| function accessSessionStorage() { |
| sessionStorage.setItem('foo', 'bar'); |
| return sessionStorage.getItem('foo') == 'bar'; |
| } |
| |
| async function accessFileSystem() { |
| const fs = await new Promise((resolve, reject) => { |
| window.webkitRequestFileSystem(TEMPORARY, 1024, resolve, reject); |
| }); |
| return new Promise((resolve, reject) => { |
| fs.root.getFile('foo.txt', {create: true, exclusive: true}, resolve, |
| reject); |
| }); |
| } |
| |
| function accessIndexedDB() { |
| var request = indexedDB.open('my_db', 2); |
| |
| request.onupgradeneeded = () => { |
| request.result.createObjectStore('store'); |
| } |
| return new Promise((resolve) => { |
| request.onsuccess = () => { |
| request.result.close(); |
| resolve(true); |
| } |
| request.onerror = () => {throw new Error('Failed to access!')} |
| }); |
| } |
| |
| function accessCacheStorage() { |
| return caches.open("cache") |
| .then((cache) => cache.put("/foo", new Response("bar"))) |
| .then(() => true) |
| .catch(() => {throw new Error('Failed to access!')}); |
| } |
| |
| // Placeholder for execution statement. |
| access%s(); |
| )"; |
| |
| using StateForURLCallback = base::OnceCallback<void(BtmState)>; |
| |
| // Helper function to close (and waits for closure of) a `web_contents` tab. |
| void CloseTab(WebContents* web_contents); |
| |
| // Helper function to open a link to the given URL in a new tab and return the |
| // new tab's WebContents. |
| base::expected<WebContents*, std::string> OpenInNewTab( |
| WebContents* original_tab, |
| const GURL& url); |
| |
| [[nodiscard]] testing::AssertionResult AccessStorage( |
| RenderFrameHost* frame, |
| blink::mojom::StorageTypeAccessed type); |
| |
| // Helper function for performing client side cookie access via JS. |
| void AccessCookieViaJSIn(WebContents* web_contents, RenderFrameHost* frame); |
| |
| // Redirect `frame` in `web_contents` to `target_url` via an HTML `<meta>` tag. |
| // If `expected_commit_url` is non-null, asserts a final commit URL of |
| // `expected_commit_url`; otherwise, asserts a final commit URL of `target_url`. |
| [[nodiscard]] testing::AssertionResult ClientSideRedirectViaMetaTag( |
| WebContents* web_contents, |
| RenderFrameHost* frame, |
| const GURL& target_url, |
| const std::optional<const GURL>& expected_commit_url = std::nullopt); |
| |
| // Redirect `frame` in `web_contents` to `target_url` via a JavaScript call to |
| // `window.location.replace()`. If `expected_commit_url` is non-null, asserts a |
| // final commit URL of `expected_commit_url`; otherwise, asserts a final commit |
| // URL of `target_url`. |
| [[nodiscard]] testing::AssertionResult ClientSideRedirectViaJS( |
| WebContents* web_contents, |
| RenderFrameHost* frame, |
| const GURL& target_url, |
| const std::optional<const GURL>& expected_commit_url = std::nullopt); |
| |
| enum class BtmClientRedirectMethod : int { |
| kMetaTag = 0, |
| kJsWindowLocationReplace = 1, |
| kRedirectLikeNavigation = 2, |
| }; |
| |
| const auto kAllBtmClientRedirectMethods = |
| testing::Values(BtmClientRedirectMethod::kMetaTag, |
| BtmClientRedirectMethod::kJsWindowLocationReplace, |
| BtmClientRedirectMethod::kRedirectLikeNavigation); |
| |
| std::string StringifyBtmClientRedirectMethod(BtmClientRedirectMethod method); |
| |
| // Redirect `web_contents` to `redirect_url` using the client redirect method |
| // `redirect_method`. Expects the final commit URL to be `expected_commit_url` |
| // if non-null, or else `redirect_url`. |
| [[nodiscard]] testing::AssertionResult PerformClientRedirect( |
| BtmClientRedirectMethod redirect_method, |
| WebContents* web_contents, |
| const GURL& redirect_url, |
| const std::optional<const GURL>& expected_commit_url = std::nullopt); |
| |
| // Helper function to navigate to /set-cookie on `host` and wait for |
| // OnCookiesAccessed() to be called. |
| bool NavigateToSetCookie(WebContents* web_contents, |
| const net::EmbeddedTestServer* server, |
| std::string_view host, |
| bool is_secure_cookie_set, |
| bool is_ad_tagged); |
| |
| // Helper function for creating an image with a cookie access on the provided |
| // WebContents. |
| void CreateImageAndWaitForCookieAccess(WebContents* web_contents, |
| const GURL& image_url); |
| |
| // Helper function to block until all BTM storage requests are complete. |
| inline void WaitOnStorage(BtmServiceImpl* btm_service) { |
| btm_service->storage()->FlushPostedTasksForTesting(); |
| } |
| |
| // Helper function to query the `url` state from BTM storage. |
| std::optional<StateValue> GetBtmState(BtmServiceImpl* btm_service, |
| const GURL& url); |
| |
| inline BtmServiceImpl* GetBtmService(WebContents* web_contents) { |
| return BtmServiceImpl::Get(web_contents->GetBrowserContext()); |
| } |
| |
| class URLCookieAccessObserver : public WebContentsObserver { |
| public: |
| URLCookieAccessObserver(WebContents* web_contents, |
| const GURL& url, |
| CookieOperation access_type); |
| |
| void Wait(); |
| |
| bool CookieAccessedInPrimaryPage() const; |
| |
| private: |
| // WebContentsObserver overrides |
| void OnCookiesAccessed(RenderFrameHost* render_frame_host, |
| const CookieAccessDetails& details) override; |
| void OnCookiesAccessed(NavigationHandle* navigation_handle, |
| const CookieAccessDetails& details) override; |
| |
| GURL url_; |
| CookieOperation access_type_; |
| bool cookie_accessed_in_primary_page_ = false; |
| base::RunLoop run_loop_; |
| }; |
| |
| class FrameCookieAccessObserver : public WebContentsObserver { |
| public: |
| explicit FrameCookieAccessObserver(WebContents* web_contents, |
| RenderFrameHost* render_frame_host, |
| CookieOperation access_type); |
| |
| // Wait until the frame accesses cookies. |
| void Wait(); |
| |
| // WebContentsObserver override |
| void OnCookiesAccessed(RenderFrameHost* render_frame_host, |
| const CookieAccessDetails& details) override; |
| |
| private: |
| const raw_ptr<RenderFrameHost, AcrossTasksDanglingUntriaged> |
| render_frame_host_; |
| CookieOperation access_type_; |
| base::RunLoop run_loop_; |
| }; |
| |
| class UserActivationObserver : public WebContentsObserver { |
| public: |
| explicit UserActivationObserver(WebContents* web_contents, |
| RenderFrameHost* render_frame_host); |
| |
| // Wait until the frame receives user activation. |
| void Wait(); |
| |
| private: |
| // WebContentsObserver override |
| void FrameReceivedUserActivation(RenderFrameHost* render_frame_host) override; |
| |
| raw_ptr<RenderFrameHost, AcrossTasksDanglingUntriaged> const |
| render_frame_host_; |
| base::RunLoop run_loop_; |
| }; |
| |
| // Checks that the URLs associated with the UKM entries with the given name are |
| // as expected. Sorts the URLs so order doesn't matter. |
| // |
| // Example usage: |
| // |
| // EXPECT_THAT(ukm_recorder, EntryUrlsAre(entry_name, {url1, url2, url3})); |
| class EntryUrlsAre { |
| public: |
| using is_gtest_matcher = void; |
| EntryUrlsAre(std::string entry_name, std::vector<std::string> urls); |
| EntryUrlsAre(const EntryUrlsAre&); |
| EntryUrlsAre(EntryUrlsAre&&); |
| ~EntryUrlsAre(); |
| |
| using UkmEntry = ukm::TestUkmRecorder::HumanReadableUkmEntry; |
| bool MatchAndExplain(const ukm::TestUkmRecorder& ukm_recorder, |
| testing::MatchResultListener* result_listener) const; |
| |
| void DescribeTo(std::ostream* os) const; |
| void DescribeNegationTo(std::ostream* os) const; |
| |
| private: |
| std::string entry_name_; |
| std::vector<std::string> expected_urls_; |
| }; |
| |
| // Enables or disables a base::Feature. |
| class ScopedInitFeature { |
| public: |
| explicit ScopedInitFeature(const base::Feature& feature, |
| bool enable, |
| const base::FieldTrialParams& params); |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Enables/disables the BTM Feature. |
| class ScopedInitBtmFeature { |
| public: |
| explicit ScopedInitBtmFeature(bool enable, |
| const base::FieldTrialParams& params = {}); |
| |
| private: |
| ScopedInitFeature init_feature_; |
| }; |
| |
| // Waits for a window to open. |
| class OpenedWindowObserver : public WebContentsObserver { |
| public: |
| explicit OpenedWindowObserver(WebContents* web_contents, |
| WindowOpenDisposition open_disposition); |
| |
| void Wait() { run_loop_.Run(); } |
| WebContents* window() { return window_; } |
| |
| private: |
| // WebContentsObserver overrides: |
| void DidOpenRequestedURL(WebContents* new_contents, |
| RenderFrameHost* source_render_frame_host, |
| const GURL& url, |
| const Referrer& referrer, |
| WindowOpenDisposition disposition, |
| ui::PageTransition transition, |
| bool started_from_context_menu, |
| bool renderer_initiated) override; |
| |
| const WindowOpenDisposition open_disposition_; |
| raw_ptr<WebContents> window_ = nullptr; |
| base::RunLoop run_loop_; |
| }; |
| |
| void SimulateUserActivation(WebContents* web_contents); |
| |
| // Simulate a mouse click and wait for the main frame to receive user |
| // activation. |
| void SimulateMouseClickAndWait(WebContents*); |
| |
| // A ContentBrowserClient that supports third-party cookie blocking. Note that |
| // this can only be used directly by unit tests; browser tests must use |
| // ContentBrowserTestTpcBlockingBrowserClient instead. |
| class TpcBlockingBrowserClient : public ContentBrowserClient, |
| public content_settings::CookieSettingsBase { |
| public: |
| using content_settings::CookieSettingsBase::IsFullCookieAccessAllowed; |
| |
| static constexpr uint64_t DATA_TYPE_HISTORY = |
| BrowsingDataRemover::DATA_TYPE_CONTENT_END << 1; |
| |
| TpcBlockingBrowserClient(); |
| ~TpcBlockingBrowserClient() override; |
| |
| void SetBlockThirdPartyCookiesByDefault(bool block) { block_3pcs_ = block; } |
| |
| bool IsFullCookieAccessAllowed( |
| BrowserContext* browser_context, |
| WebContents* web_contents, |
| const GURL& url, |
| const blink::StorageKey& storage_key, |
| net::CookieSettingOverrides overrides) override; |
| |
| void GrantCookieAccessDueToHeuristic(BrowserContext* browser_context, |
| const net::SchemefulSite& top_frame_site, |
| const net::SchemefulSite& accessing_site, |
| base::TimeDelta ttl, |
| bool ignore_schemes) override; |
| |
| bool AreThirdPartyCookiesGenerallyAllowed(BrowserContext* browser_context, |
| WebContents* web_contents) override; |
| |
| bool ShouldBtmDeleteInteractionRecords(uint64_t remove_mask) override; |
| |
| void AllowThirdPartyCookiesOnSite(const GURL& url); |
| void GrantCookieAccessTo3pSite(const GURL& url); |
| |
| void BlockThirdPartyCookiesOnSite(const GURL& url); |
| void BlockThirdPartyCookies(const GURL& url, const GURL& first_party_url); |
| |
| // Overrides for content_settings::CookieSettingsBase |
| |
| bool ShouldIgnoreSameSiteRestrictions( |
| const GURL& url, |
| const net::SiteForCookies& site_for_cookies) const override; |
| |
| ContentSetting GetContentSetting( |
| const GURL& primary_url, |
| const GURL& secondary_url, |
| ContentSettingsType content_type, |
| content_settings::SettingInfo* info) const override; |
| |
| bool ShouldAlwaysAllowCookies(const GURL& url, |
| const GURL& first_party_url) const override; |
| |
| bool ShouldBlockThirdPartyCookies( |
| base::optional_ref<const url::Origin> top_frame_origin, |
| net::CookieSettingOverrides overrides) const override; |
| |
| bool MitigationsEnabledFor3pcd() const override; |
| |
| bool IsThirdPartyCookiesAllowedScheme(std::string_view scheme) const override; |
| |
| private: |
| bool block_3pcs_ = false; |
| content_settings::HostIndexedContentSettings tpc_content_settings_; |
| }; |
| |
| // Class used to pause cookie access notifications. The class works by unbinding |
| // existing CookieAccessObserver receivers and storing new ones without binding |
| // them. |
| class PausedCookieAccessObservers : public CookieAccessObservers { |
| public: |
| explicit PausedCookieAccessObservers(NotifyCookiesAccessedCallback callback, |
| PendingObserversWithContext observers); |
| ~PausedCookieAccessObservers() override; |
| |
| // CookieAccessObservers |
| void Add(mojo::PendingReceiver<network::mojom::CookieAccessObserver> receiver, |
| CookieAccessDetails::Source source) override; |
| PendingObserversWithContext TakeReceiversWithContext() override; |
| |
| private: |
| // Holds existing and new receivers. |
| PendingObserversWithContext pending_receivers_; |
| }; |
| |
| // Class used to pause all cookie access notifications in a WebContents. |
| class CookieAccessInterceptor : public WebContentsObserver { |
| public: |
| explicit CookieAccessInterceptor(WebContents& web_contents); |
| ~CookieAccessInterceptor() override; |
| |
| // WebContentsObserver |
| void DidStartNavigation(NavigationHandle* navigation_handle) override; |
| }; |
| |
| } // namespace content |
| |
| #endif // CONTENT_BROWSER_BTM_BTM_TEST_UTILS_H_ |