| // 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 "base/json/json_file_value_serializer.h" |
| #include "base/json/values_util.h" |
| #include "base/path_service.h" |
| #include "base/strings/strcat.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "chrome/browser/browsing_topics/browsing_topics_service_factory.h" |
| #include "chrome/browser/content_settings/cookie_settings_factory.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/history/history_service_factory.h" |
| #include "chrome/browser/optimization_guide/browser_test_util.h" |
| #include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h" |
| #include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h" |
| #include "chrome/browser/privacy_sandbox/privacy_sandbox_attestations/privacy_sandbox_attestations_mixin.h" |
| #include "chrome/browser/privacy_sandbox/privacy_sandbox_settings_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/test/base/mixin_based_in_process_browser_test.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/browsing_topics/browsing_topics_service_impl.h" |
| #include "components/browsing_topics/epoch_topics.h" |
| #include "components/browsing_topics/test_util.h" |
| #include "components/content_settings/browser/page_specific_content_settings.h" |
| #include "components/content_settings/core/browser/cookie_settings.h" |
| #include "components/keyed_service/content/browser_context_dependency_manager.h" |
| #include "components/optimization_guide/core/optimization_guide_features.h" |
| #include "components/optimization_guide/core/test_model_info_builder.h" |
| #include "components/optimization_guide/core/test_optimization_guide_model_provider.h" |
| #include "components/optimization_guide/proto/page_topics_model_metadata.pb.h" |
| #include "components/privacy_sandbox/privacy_sandbox_attestations/privacy_sandbox_attestations.h" |
| #include "components/privacy_sandbox/privacy_sandbox_features.h" |
| #include "components/privacy_sandbox/privacy_sandbox_settings.h" |
| #include "components/ukm/test_ukm_recorder.h" |
| #include "content/public/browser/browsing_topics_site_data_manager.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/test/back_forward_cache_util.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browsing_topics_test_util.h" |
| #include "content/public/test/download_test_observer.h" |
| #include "content/public/test/fenced_frame_test_util.h" |
| #include "content/public/test/prerender_test_util.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "net/base/schemeful_site.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/request_handler_util.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/network/public/cpp/features.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom.h" |
| |
| namespace browsing_topics { |
| |
| namespace { |
| |
| constexpr browsing_topics::HmacKey kTestKey = {1}; |
| |
| constexpr base::Time kTime1 = |
| base::Time::FromDeltaSinceWindowsEpoch(base::Days(1)); |
| constexpr base::Time kTime2 = |
| base::Time::FromDeltaSinceWindowsEpoch(base::Days(2)); |
| |
| constexpr int kConfigVersion = 1; |
| constexpr int kTaxonomyVersion = 1; |
| constexpr int64_t kModelVersion = 2; |
| constexpr size_t kPaddedTopTopicsStartIndex = 5; |
| constexpr Topic kExpectedTopic1 = Topic(1); |
| constexpr Topic kExpectedTopic2 = Topic(10); |
| |
| constexpr char kExpectedApiResult[] = |
| "[{\"configVersion\":\"chrome.1\",\"modelVersion\":\"2\"," |
| "\"taxonomyVersion\":\"1\",\"topic\":1,\"version\":\"chrome.1:1:2\"};{" |
| "\"configVersion\":\"chrome.1\",\"modelVersion\":\"2\"," |
| "\"taxonomyVersion\":\"1\",\"topic\":10,\"version\":\"chrome.1:1:2\"};]"; |
| |
| constexpr char kExpectedHeaderValueForEmptyTopics[] = |
| "();p=P0000000000000000000000000000000"; |
| |
| constexpr char kExpectedHeaderValueForSiteA[] = |
| "(1 10);v=chrome.1:1:2, ();p=P00000000"; |
| |
| constexpr char kExpectedHeaderValueForSiteB[] = |
| "(1 7);v=chrome.1:1:2, ();p=P000000000"; |
| |
| static constexpr char kBrowsingTopicsApiActionTypeHistogramId[] = |
| "BrowsingTopics.ApiActionType"; |
| |
| static constexpr char kRedirectTopicsCallingSitesCountHistogramId[] = |
| "BrowsingTopics.RedirectChain.OnTopicsFirstInvokedForSite." |
| "TopicsCallingSitesCount"; |
| |
| EpochTopics CreateTestEpochTopics( |
| const std::vector<std::pair<Topic, std::set<HashedDomain>>>& topics, |
| base::Time calculation_time) { |
| DCHECK_EQ(topics.size(), 5u); |
| |
| std::vector<TopicAndDomains> top_topics_and_observing_domains; |
| for (size_t i = 0; i < 5; ++i) { |
| top_topics_and_observing_domains.emplace_back(topics[i].first, |
| topics[i].second); |
| } |
| |
| return EpochTopics(std::move(top_topics_and_observing_domains), |
| kPaddedTopTopicsStartIndex, kConfigVersion, |
| kTaxonomyVersion, kModelVersion, calculation_time, |
| /*from_manually_triggered_calculation=*/false); |
| } |
| |
| } // namespace |
| |
| // A tester class that allows waiting for the first calculation to finish. |
| class TesterBrowsingTopicsService : public BrowsingTopicsServiceImpl { |
| public: |
| TesterBrowsingTopicsService( |
| const base::FilePath& profile_path, |
| privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings, |
| history::HistoryService* history_service, |
| content::BrowsingTopicsSiteDataManager* site_data_manager, |
| std::unique_ptr<Annotator> annotator, |
| base::OnceClosure calculation_finish_callback) |
| : BrowsingTopicsServiceImpl( |
| profile_path, |
| privacy_sandbox_settings, |
| history_service, |
| site_data_manager, |
| std::move(annotator), |
| base::BindRepeating( |
| content_settings::PageSpecificContentSettings::TopicAccessed)), |
| calculation_finish_callback_(std::move(calculation_finish_callback)) {} |
| |
| ~TesterBrowsingTopicsService() override = default; |
| |
| TesterBrowsingTopicsService(const TesterBrowsingTopicsService&) = delete; |
| TesterBrowsingTopicsService& operator=(const TesterBrowsingTopicsService&) = |
| delete; |
| TesterBrowsingTopicsService(TesterBrowsingTopicsService&&) = delete; |
| TesterBrowsingTopicsService& operator=(TesterBrowsingTopicsService&&) = |
| delete; |
| |
| const BrowsingTopicsState& browsing_topics_state() override { |
| return BrowsingTopicsServiceImpl::browsing_topics_state(); |
| } |
| |
| void OnCalculateBrowsingTopicsCompleted(EpochTopics epoch_topics) override { |
| BrowsingTopicsServiceImpl::OnCalculateBrowsingTopicsCompleted( |
| std::move(epoch_topics)); |
| |
| if (calculation_finish_callback_) |
| std::move(calculation_finish_callback_).Run(); |
| } |
| |
| private: |
| base::OnceClosure calculation_finish_callback_; |
| }; |
| |
| class BrowsingTopicsBrowserTestBase : public MixinBasedInProcessBrowserTest { |
| public: |
| void SetUpOnMainThread() override { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| // Mark all Privacy Sandbox APIs as attested since the test cases are |
| // testing behaviors not related to attestations. |
| privacy_sandbox::PrivacySandboxAttestations::GetInstance() |
| ->SetAllPrivacySandboxAttestedForTesting(true); |
| https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES); |
| https_server_.AddDefaultHandlers(GetChromeTestDataDir()); |
| |
| https_server_.RegisterRequestMonitor(base::BindRepeating( |
| &BrowsingTopicsBrowserTestBase::MonitorRequestOnNetworkThread, |
| base::Unretained(this), |
| base::SequencedTaskRunner::GetCurrentDefault())); |
| |
| embedded_test_server()->RegisterRequestMonitor(base::BindRepeating( |
| &BrowsingTopicsBrowserTestBase::MonitorRequestOnNetworkThread, |
| base::Unretained(this), |
| base::SequencedTaskRunner::GetCurrentDefault())); |
| |
| content::SetupCrossSiteRedirector(&https_server_); |
| |
| for (int i = 0; i < 10; ++i) { |
| distinct_cert_hostnames_.push_back( |
| base::StrCat({"example", base::NumberToString(i), ".com"})); |
| } |
| |
| distinct_cert_hostnames_.push_back("a.test"); |
| distinct_cert_hostnames_.push_back("b.test"); |
| distinct_cert_hostnames_.push_back("c.test"); |
| distinct_cert_hostnames_.push_back("d.test"); |
| |
| https_server_.SetCertHostnames(distinct_cert_hostnames_); |
| |
| ASSERT_TRUE(https_server_.Start()); |
| |
| content::SetupCrossSiteRedirector(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| |
| ~BrowsingTopicsBrowserTestBase() override = default; |
| |
| std::string InvokeTopicsAPI(const content::ToRenderFrameHost& adapter, |
| bool skip_observation = false, |
| content::EvalJsOptions eval_options = |
| content::EXECUTE_SCRIPT_DEFAULT_OPTIONS) { |
| return EvalJs(adapter, |
| content::JsReplace(R"( |
| if (!(document.browsingTopics instanceof Function)) { |
| 'not a function'; |
| } else { |
| document.browsingTopics({skipObservation: $1}) |
| .then(topics => { |
| let result = "["; |
| for (const topic of topics) { |
| result += JSON.stringify(topic, Object.keys(topic).sort()) + ";" |
| } |
| result += "]"; |
| return result; |
| }) |
| .catch(error => error.message); |
| } |
| )", |
| skip_observation), |
| eval_options) |
| .ExtractString(); |
| } |
| |
| void MonitorRequestOnNetworkThread( |
| const scoped_refptr<base::SequencedTaskRunner>& main_thread_task_runner, |
| const net::test_server::HttpRequest& request) { |
| main_thread_task_runner->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &BrowsingTopicsBrowserTestBase::MonitorRequestOnMainThread, |
| base::Unretained(this), request)); |
| } |
| |
| void MonitorRequestOnMainThread( |
| const net::test_server::HttpRequest& request) { |
| auto topics_header = request.headers.find("Sec-Browsing-Topics"); |
| if (topics_header != request.headers.end()) { |
| request_path_topics_map_[request.GetURL().path()] = topics_header->second; |
| } |
| } |
| |
| std::optional<std::string> GetTopicsHeaderForRequestPath( |
| const std::string& request_path) { |
| auto it = request_path_topics_map_.find(request_path); |
| if (it == request_path_topics_map_.end()) { |
| return std::nullopt; |
| } |
| |
| return it->second; |
| } |
| |
| content::WebContents* web_contents() { |
| return browser()->tab_strip_model()->GetActiveWebContents(); |
| } |
| |
| protected: |
| std::vector<std::string> distinct_cert_hostnames_; |
| |
| net::EmbeddedTestServer https_server_{ |
| net::test_server::EmbeddedTestServer::TYPE_HTTPS}; |
| |
| // Mapping of request paths to the topics header they were requested with. |
| std::map<std::string, std::string> request_path_topics_map_; |
| privacy_sandbox::PrivacySandboxAttestationsMixin |
| privacy_sandbox_attestations_mixin_{&mixin_host_}; |
| }; |
| |
| class BrowsingTopicsDisabledBrowserTest : public BrowsingTopicsBrowserTestBase { |
| public: |
| BrowsingTopicsDisabledBrowserTest() { |
| scoped_feature_list_.InitWithFeatures( |
| /*enabled_features=*/{}, |
| /*disabled_features=*/{network::features::kBrowsingTopics}); |
| } |
| |
| protected: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsDisabledBrowserTest, |
| NoBrowsingTopicsService) { |
| EXPECT_FALSE( |
| BrowsingTopicsServiceFactory::GetForProfile(browser()->profile())); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsDisabledBrowserTest, NoTopicsAPI) { |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| EXPECT_EQ("not a function", InvokeTopicsAPI(web_contents())); |
| } |
| |
| // Enables the feature flags for BrowsingTopics but does not override the |
| // Annotator to a mocked instance. |
| class BrowsingTopicsAnnotationGoldenDataBrowserTest |
| : public BrowsingTopicsBrowserTestBase { |
| public: |
| BrowsingTopicsAnnotationGoldenDataBrowserTest() { |
| scoped_feature_list_.InitWithFeatures( |
| /*enabled_features=*/ |
| {network::features::kBrowsingTopics, |
| blink::features::kBrowsingTopicsBypassIPIsPubliclyRoutableCheck, |
| features::kPrivacySandboxAdsAPIsOverride}, |
| /*disabled_features=*/{ |
| optimization_guide::features::kPreventLongRunningPredictionModels}); |
| } |
| ~BrowsingTopicsAnnotationGoldenDataBrowserTest() override = default; |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Running a TFLite model in a test is expensive so it can only be done in a |
| // browser test without any page loads. |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsAnnotationGoldenDataBrowserTest, |
| GoldenData) { |
| // Boilerplate for getting the model to work for a real execution. |
| optimization_guide::proto::Any any_metadata; |
| any_metadata.set_type_url( |
| "type.googleapis.com/com.foo.PageTopicsModelMetadata"); |
| optimization_guide::proto::PageTopicsModelMetadata page_topics_model_metadata; |
| page_topics_model_metadata.set_version(123); |
| if (blink::features::kBrowsingTopicsTaxonomyVersion.Get() >= 2) { |
| page_topics_model_metadata.set_taxonomy_version( |
| blink::features::kBrowsingTopicsTaxonomyVersion.Get()); |
| } |
| page_topics_model_metadata.add_supported_output( |
| optimization_guide::proto::PAGE_TOPICS_SUPPORTED_OUTPUT_CATEGORIES); |
| auto* output_params = |
| page_topics_model_metadata.mutable_output_postprocessing_params(); |
| auto* category_params = output_params->mutable_category_params(); |
| category_params->set_max_categories(5); |
| category_params->set_min_none_weight(0.8); |
| category_params->set_min_category_weight(0.1); |
| category_params->set_min_normalized_weight_within_top_n(0.1); |
| page_topics_model_metadata.SerializeToString(any_metadata.mutable_value()); |
| base::FilePath source_root_dir; |
| ASSERT_TRUE( |
| base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &source_root_dir)); |
| base::FilePath model_file_path = source_root_dir.AppendASCII("chrome") |
| .AppendASCII("test") |
| .AppendASCII("data") |
| .AppendASCII("browsing_topics") |
| .AppendASCII("golden_data_model.tflite"); |
| |
| OptimizationGuideKeyedServiceFactory::GetForProfile(browser()->profile()) |
| ->OverrideTargetModelForTesting( |
| optimization_guide::proto::OPTIMIZATION_TARGET_PAGE_TOPICS_V2, |
| optimization_guide::TestModelInfoBuilder() |
| .SetModelFilePath(model_file_path) |
| .SetModelMetadata(any_metadata) |
| .Build()); |
| |
| BrowsingTopicsService* service = |
| BrowsingTopicsServiceFactory::GetForProfile(browser()->profile()); |
| |
| base::HistogramTester histogram_tester; |
| base::RunLoop run_loop; |
| service->GetAnnotator()->BatchAnnotate( |
| base::BindOnce( |
| [](base::RunLoop* run_loop, |
| const std::vector<Annotation>& annotations) { |
| ASSERT_EQ(annotations.size(), 1U); |
| EXPECT_EQ(annotations[0].input, "foo.bar.com"); |
| EXPECT_THAT(annotations[0].topics, |
| testing::UnorderedElementsAre(1, 289)); |
| run_loop->Quit(); |
| }, |
| &run_loop), |
| {"foo.bar.com"}); |
| |
| run_loop.Run(); |
| |
| optimization_guide::RetryForHistogramUntilCountReached( |
| &histogram_tester, |
| "OptimizationGuide.ModelExecutor.ExecutionStatus.PageTopicsV2", 1); |
| |
| histogram_tester.ExpectUniqueSample( |
| "OptimizationGuide.ModelExecutor.ExecutionStatus.PageTopicsV2", |
| /*kSuccess=*/1, 1); |
| } |
| |
| class BrowsingTopicsBrowserTest : public BrowsingTopicsBrowserTestBase { |
| public: |
| BrowsingTopicsBrowserTest() |
| : prerender_helper_( |
| base::BindRepeating(&BrowsingTopicsBrowserTest::web_contents, |
| base::Unretained(this))) { |
| // Configure a long epoch_retention_duration to prevent epochs from expiring |
| // during tests where expiration is irrelevant. |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| /*enabled_features=*/ |
| {{network::features::kBrowsingTopics, {}}, |
| {blink::features::kBrowsingTopicsParameters, |
| {{"epoch_retention_duration", "3650000d"}}}, |
| {blink::features::kBrowsingTopicsBypassIPIsPubliclyRoutableCheck, {}}, |
| {features::kPrivacySandboxAdsAPIsOverride, {}}}, |
| /*disabled_features=*/{}); |
| } |
| |
| ~BrowsingTopicsBrowserTest() override = default; |
| |
| void SetUpOnMainThread() override { |
| prerender_helper_.RegisterServerRequestMonitor(&https_server_); |
| |
| BrowsingTopicsBrowserTestBase::SetUpOnMainThread(); |
| |
| for (auto& profile_and_calculation_finish_waiter : |
| calculation_finish_waiters_) { |
| profile_and_calculation_finish_waiter.second->Run(); |
| } |
| } |
| |
| void SetUpBrowserContextKeyedServices( |
| content::BrowserContext* context) override { |
| browsing_topics::BrowsingTopicsServiceFactory::GetInstance() |
| ->SetTestingFactory( |
| context, |
| base::BindRepeating( |
| &BrowsingTopicsBrowserTest::CreateBrowsingTopicsService, |
| base::Unretained(this))); |
| } |
| |
| protected: |
| void CreateIframe(const GURL& url, bool browsing_topics_attribute = false) { |
| content::TestNavigationObserver nav_observer(web_contents()); |
| |
| ExecuteScriptAsync(web_contents(), |
| content::JsReplace(R"( |
| { |
| const iframe = document.createElement("iframe"); |
| iframe.browsingTopics = $1; |
| iframe.src = $2; |
| document.body.appendChild(iframe); |
| } |
| )", |
| browsing_topics_attribute, url)); |
| |
| nav_observer.WaitForNavigationFinished(); |
| EXPECT_TRUE(nav_observer.last_navigation_succeeded()); |
| } |
| |
| void ExpectResultTopicsEqual( |
| const std::vector<TopicAndDomains>& result, |
| std::vector<std::pair<Topic, std::set<HashedDomain>>> expected) { |
| DCHECK_EQ(expected.size(), 5u); |
| EXPECT_EQ(result.size(), 5u); |
| |
| for (int i = 0; i < 5; ++i) { |
| EXPECT_EQ(result[i].topic(), expected[i].first); |
| EXPECT_EQ(result[i].hashed_domains(), expected[i].second); |
| } |
| } |
| |
| HashedDomain GetHashedDomain(const std::string& domain) { |
| return HashContextDomainForStorage(kTestKey, domain); |
| } |
| |
| void CreateBrowsingTopicsStateFile( |
| const base::FilePath& profile_path, |
| const std::vector<EpochTopics>& epochs, |
| base::Time next_scheduled_calculation_time) { |
| base::Value::List epochs_list; |
| for (const EpochTopics& epoch : epochs) { |
| epochs_list.Append(epoch.ToDictValue()); |
| } |
| |
| base::Value::Dict dict; |
| dict.Set("epochs", std::move(epochs_list)); |
| dict.Set("next_scheduled_calculation_time", |
| base::TimeToValue(next_scheduled_calculation_time)); |
| dict.Set("hex_encoded_hmac_key", base::HexEncode(kTestKey)); |
| dict.Set("config_version", 1); |
| |
| JSONFileValueSerializer( |
| profile_path.Append(FILE_PATH_LITERAL("BrowsingTopicsState"))) |
| .Serialize(dict); |
| } |
| |
| content::BrowsingTopicsSiteDataManager* browsing_topics_site_data_manager() { |
| return browser() |
| ->profile() |
| ->GetDefaultStoragePartition() |
| ->GetBrowsingTopicsSiteDataManager(); |
| } |
| |
| history::HistoryService* history_service() { |
| return HistoryServiceFactory::GetForProfile( |
| browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS); |
| } |
| |
| TesterBrowsingTopicsService* browsing_topics_service() { |
| return static_cast<TesterBrowsingTopicsService*>( |
| BrowsingTopicsServiceFactory::GetForProfile(browser()->profile())); |
| } |
| |
| const BrowsingTopicsState& browsing_topics_state() { |
| return browsing_topics_service()->browsing_topics_state(); |
| } |
| |
| privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings() { |
| return PrivacySandboxSettingsFactory::GetForProfile(browser()->profile()); |
| } |
| |
| content::test::PrerenderTestHelper& prerender_helper() { |
| return prerender_helper_; |
| } |
| |
| void InitializePreexistingState( |
| history::HistoryService* history_service, |
| content::BrowsingTopicsSiteDataManager* site_data_manager, |
| const base::FilePath& profile_path, |
| TestAnnotator* annotator) { |
| // Configure the (mock) model. |
| |
| annotator->UseModelInfo( |
| *optimization_guide::TestModelInfoBuilder().SetVersion(1).Build()); |
| annotator->UseAnnotations({ |
| {"foo6.com", {1, 2, 3, 4, 5, 6}}, |
| {"foo5.com", {2, 3, 4, 5, 6}}, |
| {"foo4.com", {3, 4, 5, 6}}, |
| {"foo3.com", {4, 5, 6}}, |
| {"foo2.com", {5, 6}}, |
| {"foo1.com", {6}}, |
| }); |
| |
| // Add some initial history. |
| history::HistoryAddPageArgs add_page_args; |
| add_page_args.time = base::Time::Now(); |
| add_page_args.context_id = 1; |
| add_page_args.nav_entry_id = 1; |
| |
| // Note: foo6.com isn't in the initial history. |
| for (int i = 1; i <= 5; ++i) { |
| add_page_args.url = |
| GURL(base::StrCat({"https://foo", base::NumberToString(i), ".com"})); |
| history_service->AddPage(add_page_args); |
| history_service->SetBrowsingTopicsAllowed(add_page_args.context_id, |
| add_page_args.nav_entry_id, |
| add_page_args.url); |
| } |
| |
| // Add some API usage contexts data. |
| site_data_manager->OnBrowsingTopicsApiUsed( |
| HashMainFrameHostForStorage("foo1.com"), HashedDomain(1), "foo1.com", |
| base::Time::Now()); |
| |
| // Initialize the `BrowsingTopicsState`. |
| std::vector<EpochTopics> preexisting_epochs; |
| preexisting_epochs.push_back( |
| CreateTestEpochTopics({{Topic(1), {GetHashedDomain("a.test")}}, |
| {Topic(2), {GetHashedDomain("a.test")}}, |
| {Topic(3), {GetHashedDomain("a.test")}}, |
| {Topic(4), {GetHashedDomain("a.test")}}, |
| {Topic(5), {GetHashedDomain("a.test")}}}, |
| kTime1)); |
| preexisting_epochs.push_back( |
| CreateTestEpochTopics({{Topic(6), {GetHashedDomain("a.test")}}, |
| {Topic(7), {GetHashedDomain("a.test")}}, |
| {Topic(8), {GetHashedDomain("a.test")}}, |
| {Topic(9), {GetHashedDomain("a.test")}}, |
| {Topic(10), {GetHashedDomain("a.test")}}}, |
| kTime2)); |
| |
| CreateBrowsingTopicsStateFile( |
| profile_path, std::move(preexisting_epochs), |
| /*next_scheduled_calculation_time=*/base::Time::Now() - base::Days(1)); |
| } |
| |
| std::unique_ptr<KeyedService> CreateBrowsingTopicsService( |
| content::BrowserContext* context) { |
| Profile* profile = Profile::FromBrowserContext(context); |
| |
| privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings = |
| PrivacySandboxSettingsFactory::GetForProfile(profile); |
| privacy_sandbox_settings->SetAllPrivacySandboxAllowedForTesting(); |
| |
| history::HistoryService* history_service = |
| HistoryServiceFactory::GetForProfile( |
| profile, ServiceAccessType::IMPLICIT_ACCESS); |
| |
| content::BrowsingTopicsSiteDataManager* site_data_manager = |
| context->GetDefaultStoragePartition() |
| ->GetBrowsingTopicsSiteDataManager(); |
| |
| std::unique_ptr<TestAnnotator> annotator = |
| std::make_unique<TestAnnotator>(); |
| |
| InitializePreexistingState(history_service, site_data_manager, |
| profile->GetPath(), annotator.get()); |
| |
| DCHECK(!base::Contains(calculation_finish_waiters_, profile)); |
| calculation_finish_waiters_.emplace(profile, |
| std::make_unique<base::RunLoop>()); |
| |
| if (!ukm_recorder_) |
| ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>(); |
| |
| return std::make_unique<TesterBrowsingTopicsService>( |
| profile->GetPath(), privacy_sandbox_settings, history_service, |
| site_data_manager, std::move(annotator), |
| calculation_finish_waiters_.at(profile)->QuitClosure()); |
| } |
| |
| content::test::FencedFrameTestHelper fenced_frame_test_helper_; |
| |
| content::test::PrerenderTestHelper prerender_helper_; |
| |
| base::test::ScopedFeatureList scoped_feature_list_; |
| |
| std::map<Profile*, std::unique_ptr<base::RunLoop>> |
| calculation_finish_waiters_; |
| |
| optimization_guide::TestOptimizationGuideModelProvider model_provider_; |
| |
| std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_; |
| }; |
| |
| class BrowsingTopicsSubresourceRequestTest |
| : public BrowsingTopicsBrowserTest, |
| public ::testing::WithParamInterface<bool> { |
| public: |
| // If true, test the fetch option. If false, test the img attribute. |
| bool TestFetch() { return GetParam(); } |
| |
| std::string GetRelativePath() { |
| if (TestFetch()) { |
| return "/browsing_topics/page_with_custom_topics_header.html"; |
| } |
| |
| return "/browsing_topics/topics-writable-pixel.png"; |
| } |
| |
| std::string GetRedirectRelativePath() { |
| if (TestFetch()) { |
| return "/browsing_topics/page_with_custom_topics_header2.html"; |
| } |
| |
| return "/browsing_topics/topics-writable-pixel2.png"; |
| } |
| |
| bool ExecJsWithBrowsingTopicsTrue(GURL url) { |
| if (TestFetch()) { |
| return ExecJs( |
| web_contents()->GetPrimaryMainFrame(), |
| content::JsReplace("fetch($1, {browsingTopics: true})", url)); |
| } |
| |
| return ExecJs(web_contents()->GetPrimaryMainFrame(), |
| content::JsReplace(R"( |
| let img = document.createElement('img'); |
| img.src = $1; |
| img.browsingTopics = true; |
| img.decode() |
| .then(() => { |
| document.body.appendChild(img); |
| }) |
| )", |
| url.spec())); |
| } |
| |
| bool ExecJSWithBrowsingTopicsFalse(GURL url) { |
| if (TestFetch()) { |
| return ExecJs( |
| web_contents()->GetPrimaryMainFrame(), |
| content::JsReplace("fetch($1, {browsingTopics: false})", url)); |
| } |
| |
| return ExecJs(web_contents()->GetPrimaryMainFrame(), |
| content::JsReplace(R"( |
| let img = document.createElement('img'); |
| img.src = $1; |
| img.browsingTopics = false; |
| img.decode() |
| .then(() => { |
| document.body.appendChild(img); |
| }) |
| )", |
| url.spec())); |
| } |
| |
| bool ExecJsWithMissingBrowsingTopicsAttribute(GURL url) { |
| if (TestFetch()) { |
| return ExecJs(web_contents()->GetPrimaryMainFrame(), |
| content::JsReplace("fetch($1)", url)); |
| } |
| |
| return ExecJs(web_contents()->GetPrimaryMainFrame(), |
| content::JsReplace(R"( |
| let img = document.createElement('img'); |
| img.src = $1; |
| img.decode() |
| .then(() => { |
| document.body.appendChild(img); |
| }) |
| )", |
| url.spec())); |
| } |
| |
| int GetBrowsingTopicsApiActionType(bool observe) { |
| if (TestFetch()) { |
| if (observe) { |
| return 3; // kObserveViaFetchLikeApi |
| } |
| return 2; // kGetViaFetchLikeApi; |
| } |
| if (observe) { |
| return 7; // kObserveViaImgAttributeApi |
| } |
| return 6; // kGetViaImgAttributeApi |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| BrowsingTopicsSubresourceRequestTest, |
| ::testing::Bool()); |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, HasBrowsingTopicsService) { |
| EXPECT_TRUE(browsing_topics_service()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, NoServiceInIncognitoMode) { |
| CreateIncognitoBrowser(browser()->profile()); |
| |
| EXPECT_TRUE(browser()->profile()->HasPrimaryOTRProfile()); |
| |
| Profile* incognito_profile = |
| browser()->profile()->GetPrimaryOTRProfile(/*create_if_needed=*/false); |
| EXPECT_TRUE(incognito_profile); |
| |
| BrowsingTopicsService* incognito_browsing_topics_service = |
| BrowsingTopicsServiceFactory::GetForProfile(incognito_profile); |
| EXPECT_FALSE(incognito_browsing_topics_service); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, BrowsingTopicsStateOnStart) { |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| base::Time now = base::Time::Now(); |
| |
| EXPECT_EQ(browsing_topics_state().epochs().size(), 3u); |
| EXPECT_EQ(browsing_topics_state().epochs()[0].calculation_time(), kTime1); |
| EXPECT_EQ(browsing_topics_state().epochs()[1].calculation_time(), kTime2); |
| EXPECT_GT(browsing_topics_state().epochs()[2].calculation_time(), |
| now - base::Minutes(1)); |
| EXPECT_LT(browsing_topics_state().epochs()[2].calculation_time(), now); |
| |
| ExpectResultTopicsEqual( |
| browsing_topics_state().epochs()[0].top_topics_and_observing_domains(), |
| {{Topic(1), {GetHashedDomain("a.test")}}, |
| {Topic(2), {GetHashedDomain("a.test")}}, |
| {Topic(3), {GetHashedDomain("a.test")}}, |
| {Topic(4), {GetHashedDomain("a.test")}}, |
| {Topic(5), {GetHashedDomain("a.test")}}}); |
| |
| ExpectResultTopicsEqual( |
| browsing_topics_state().epochs()[1].top_topics_and_observing_domains(), |
| {{Topic(6), {GetHashedDomain("a.test")}}, |
| {Topic(7), {GetHashedDomain("a.test")}}, |
| {Topic(8), {GetHashedDomain("a.test")}}, |
| {Topic(9), {GetHashedDomain("a.test")}}, |
| {Topic(10), {GetHashedDomain("a.test")}}}); |
| |
| ExpectResultTopicsEqual( |
| browsing_topics_state().epochs()[2].top_topics_and_observing_domains(), |
| {{Topic(6), {HashedDomain(1)}}, |
| {Topic(5), {}}, |
| {Topic(4), {}}, |
| {Topic(3), {}}, |
| {Topic(2), {}}}); |
| |
| EXPECT_GT(browsing_topics_state().next_scheduled_calculation_time(), |
| now + base::Days(7) - base::Minutes(1)); |
| EXPECT_LT(browsing_topics_state().next_scheduled_calculation_time(), |
| now + base::Days(7)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, ApiResultUkm) { |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| InvokeTopicsAPI(web_contents()); |
| |
| auto entries = ukm_recorder_->GetEntriesByName( |
| ukm::builders::BrowsingTopics_DocumentBrowsingTopicsApiResult2:: |
| kEntryName); |
| EXPECT_EQ(1u, entries.size()); |
| |
| ukm_recorder_->ExpectEntrySourceHasUrl(entries.back(), main_frame_url); |
| |
| std::vector<ApiResultUkmMetrics> metrics_entries = |
| ReadApiResultUkmMetrics(*ukm_recorder_); |
| |
| EXPECT_EQ(1u, metrics_entries.size()); |
| |
| EXPECT_FALSE(metrics_entries[0].failure_reason); |
| |
| EXPECT_TRUE(metrics_entries[0].topic0.IsValid()); |
| EXPECT_TRUE(metrics_entries[0].topic0.is_true_topic()); |
| EXPECT_FALSE(metrics_entries[0].topic0.should_be_filtered()); |
| EXPECT_EQ(metrics_entries[0].topic0.taxonomy_version(), 1); |
| EXPECT_EQ(metrics_entries[0].topic0.model_version(), 2); |
| |
| EXPECT_TRUE(metrics_entries[0].topic1.IsValid()); |
| EXPECT_TRUE(metrics_entries[0].topic1.is_true_topic()); |
| EXPECT_FALSE(metrics_entries[0].topic1.should_be_filtered()); |
| EXPECT_EQ(metrics_entries[0].topic1.taxonomy_version(), 1); |
| EXPECT_EQ(metrics_entries[0].topic1.model_version(), 2); |
| |
| EXPECT_FALSE(metrics_entries[0].topic2.IsValid()); |
| |
| EXPECT_EQ(metrics_entries[0].topic0.topic(), kExpectedTopic1); |
| EXPECT_EQ(metrics_entries[0].topic1.topic(), kExpectedTopic2); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, PageLoadUkm) { |
| // The test assumes pages gets deleted after navigation, triggering metrics |
| // recording. Disable back/forward cache to ensure that pages don't get |
| // preserved in the cache. |
| content::DisableBackForwardCacheForTesting( |
| web_contents(), content::BackForwardCache::TEST_REQUIRES_NO_CACHING); |
| |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| InvokeTopicsAPI(web_contents()); |
| |
| ASSERT_TRUE( |
| ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL))); |
| |
| auto entries = ukm_recorder_->GetEntriesByName( |
| ukm::builders::BrowsingTopics_PageLoad::kEntryName); |
| EXPECT_EQ(1u, entries.size()); |
| |
| ukm_recorder_->ExpectEntrySourceHasUrl(entries.back(), main_frame_url); |
| |
| ukm_recorder_->ExpectEntryMetric(entries.back(), |
| ukm::builders::BrowsingTopics_PageLoad:: |
| kTopicsRequestingContextDomainsCountName, |
| 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, GetTopTopicsForDisplay) { |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| std::vector<privacy_sandbox::CanonicalTopic> result = |
| browsing_topics_service()->GetTopTopicsForDisplay(); |
| |
| EXPECT_EQ(result.size(), 15u); |
| EXPECT_EQ(result[0].topic_id(), Topic(1)); |
| EXPECT_EQ(result[1].topic_id(), Topic(2)); |
| EXPECT_EQ(result[2].topic_id(), Topic(3)); |
| EXPECT_EQ(result[3].topic_id(), Topic(4)); |
| EXPECT_EQ(result[4].topic_id(), Topic(5)); |
| EXPECT_EQ(result[5].topic_id(), Topic(6)); |
| EXPECT_EQ(result[6].topic_id(), Topic(7)); |
| EXPECT_EQ(result[7].topic_id(), Topic(8)); |
| EXPECT_EQ(result[8].topic_id(), Topic(9)); |
| EXPECT_EQ(result[9].topic_id(), Topic(10)); |
| EXPECT_EQ(result[10].topic_id(), Topic(6)); |
| EXPECT_EQ(result[11].topic_id(), Topic(5)); |
| EXPECT_EQ(result[12].topic_id(), Topic(4)); |
| EXPECT_EQ(result[13].topic_id(), Topic(3)); |
| EXPECT_EQ(result[14].topic_id(), Topic(2)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| TopicsAPI_ContextDomainNotFiltered_FromMainFrame) { |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| std::string result = InvokeTopicsAPI(web_contents()); |
| |
| EXPECT_EQ(result, kExpectedApiResult); |
| |
| // Ensure access has been reported to the Page Specific Content Settings. |
| auto* pscs = content_settings::PageSpecificContentSettings::GetForPage( |
| web_contents()->GetPrimaryPage()); |
| EXPECT_TRUE(pscs->HasAccessedTopics()); |
| auto topics = pscs->GetAccessedTopics(); |
| ASSERT_EQ(2u, topics.size()); |
| |
| // PSCS::GetAccessedTopics() will return sorted values. |
| EXPECT_EQ(topics[0].topic_id(), kExpectedTopic1); |
| EXPECT_EQ(topics[1].topic_id(), kExpectedTopic2); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| TopicsAPI_ContextDomainNotFiltered_FromSubframe) { |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| GURL subframe_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(content::NavigateIframeToURL(web_contents(), |
| /*iframe_id=*/"frame", |
| subframe_url)); |
| |
| std::string result = InvokeTopicsAPI( |
| content::ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0)); |
| |
| EXPECT_EQ(result, kExpectedApiResult); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| TopicsAPI_ContextDomainFiltered) { |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| GURL subframe_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(content::NavigateIframeToURL(web_contents(), |
| /*iframe_id=*/"frame", |
| subframe_url)); |
| |
| // b.test has yet to call the API so it shouldn't receive a topic. |
| EXPECT_EQ("[]", InvokeTopicsAPI(content::ChildFrameAt( |
| web_contents()->GetPrimaryMainFrame(), 0))); |
| auto* pscs = content_settings::PageSpecificContentSettings::GetForPage( |
| web_contents()->GetPrimaryPage()); |
| EXPECT_FALSE(pscs->HasAccessedTopics()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, TopicsAPI_ObserveBehavior) { |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| GURL subframe_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(content::NavigateIframeToURL(web_contents(), |
| /*iframe_id=*/"frame", |
| subframe_url)); |
| |
| { |
| base::HistogramTester histogram_tester; |
| |
| // Invoked the API with {skipObservation: true}. |
| EXPECT_EQ("[]", |
| InvokeTopicsAPI(content::ChildFrameAt( |
| web_contents()->GetPrimaryMainFrame(), 0), |
| /*skip_observation=*/true)); |
| |
| // Since {skipObservation: true} was specified, the page is not eligible for |
| // topics calculation. |
| EXPECT_FALSE( |
| BrowsingTopicsEligibleForURLVisit(history_service(), main_frame_url)); |
| |
| // Since {skipObservation: true} was specified, the usage is not tracked. |
| // The returned entry was from the pre-existing storage. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 1u); |
| |
| histogram_tester.ExpectUniqueSample(kBrowsingTopicsApiActionTypeHistogramId, |
| 0 /*kGetViaDocumentApi*/, |
| /*expected_bucket_count=*/1); |
| } |
| |
| { |
| base::HistogramTester histogram_tester; |
| |
| // Invoked the API with {skipObservation: false}. |
| EXPECT_EQ("[]", InvokeTopicsAPI(content::ChildFrameAt( |
| web_contents()->GetPrimaryMainFrame(), 0))); |
| |
| // Since {skipObservation: false} was specified, the page is eligible for |
| // topics calculation. |
| EXPECT_TRUE( |
| BrowsingTopicsEligibleForURLVisit(history_service(), main_frame_url)); |
| |
| // Since {skipObservation: false} was specified, the usage is tracked. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| |
| EXPECT_EQ(api_usage_contexts.size(), 2u); |
| EXPECT_EQ(api_usage_contexts[0].hashed_main_frame_host, |
| HashMainFrameHostForStorage("foo1.com")); |
| EXPECT_EQ(api_usage_contexts[0].hashed_context_domain, HashedDomain(1)); |
| |
| EXPECT_EQ(api_usage_contexts[1].hashed_main_frame_host, |
| HashMainFrameHostForStorage( |
| https_server_.GetURL("a.test", "/").host())); |
| EXPECT_EQ(api_usage_contexts[1].hashed_context_domain, |
| GetHashedDomain("b.test")); |
| |
| histogram_tester.ExpectUniqueSample(kBrowsingTopicsApiActionTypeHistogramId, |
| 1 /*kGetAndObserveViaDocumentApi*/, |
| /*expected_bucket_count=*/1); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| BrowsingTopicsBrowserTest, |
| EmptyPage_PermissionsPolicyBrowsingTopicsNone_TopicsAPI) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url = https_server_.GetURL( |
| "a.test", "/browsing_topics/empty_page_browsing_topics_none.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| base::RunLoop ukm_loop; |
| ukm_recorder_->SetOnAddEntryCallback( |
| ukm::builders::BrowsingTopics_DocumentBrowsingTopicsApiResult2:: |
| kEntryName, |
| ukm_loop.QuitClosure()); |
| |
| EXPECT_EQ( |
| "The \"browsing-topics\" Permissions Policy denied the use of " |
| "document.browsingTopics().", |
| InvokeTopicsAPI(web_contents())); |
| |
| ukm_loop.Run(); |
| |
| std::vector<ApiResultUkmMetrics> metrics_entries = |
| ReadApiResultUkmMetrics(*ukm_recorder_); |
| |
| EXPECT_EQ(1u, metrics_entries.size()); |
| |
| EXPECT_EQ(metrics_entries[0].failure_reason, |
| ApiAccessResult::kInvalidRequestingContext); |
| EXPECT_FALSE(metrics_entries[0].topic0.IsValid()); |
| EXPECT_FALSE(metrics_entries[0].topic1.IsValid()); |
| EXPECT_FALSE(metrics_entries[0].topic2.IsValid()); |
| |
| // No BrowsingTopicsApiActionType metrics are recorded, as |
| // `BrowsingTopicsServiceImpl` did not get a chance to handle the request due |
| // to earlier permissions policy reject. |
| histogram_tester.ExpectTotalCount(kBrowsingTopicsApiActionTypeHistogramId, |
| /*expected_count=*/0); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| BrowsingTopicsBrowserTest, |
| EmptyPage_PermissionsPolicyInterestCohortNone_TopicsAPI) { |
| GURL main_frame_url = https_server_.GetURL( |
| "a.test", "/browsing_topics/empty_page_interest_cohort_none.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| EXPECT_EQ( |
| "The \"interest-cohort\" Permissions Policy denied the use of " |
| "document.browsingTopics().", |
| InvokeTopicsAPI(web_contents())); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| BrowsingTopicsBrowserTest, |
| OneIframePage_SubframePermissionsPolicyBrowsingTopicsNone_TopicsAPI) { |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| GURL subframe_url = https_server_.GetURL( |
| "a.test", "/browsing_topics/empty_page_browsing_topics_none.html"); |
| |
| ASSERT_TRUE(content::NavigateIframeToURL(web_contents(), |
| /*iframe_id=*/"frame", |
| subframe_url)); |
| |
| std::string result = InvokeTopicsAPI(web_contents()); |
| EXPECT_EQ(result, kExpectedApiResult); |
| |
| EXPECT_EQ( |
| "The \"browsing-topics\" Permissions Policy denied the use of " |
| "document.browsingTopics().", |
| InvokeTopicsAPI( |
| content::ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| PermissionsPolicyAllowCertainOrigin_TopicsAPI) { |
| base::StringPairs allowed_origin_replacement; |
| allowed_origin_replacement.emplace_back( |
| "{{ALLOWED_ORIGIN}}", https_server_.GetOrigin("c.test").Serialize()); |
| |
| GURL main_frame_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements( |
| "/browsing_topics/" |
| "one_iframe_page_browsing_topics_allow_certain_origin.html", |
| allowed_origin_replacement)); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| std::string result = InvokeTopicsAPI(web_contents()); |
| EXPECT_EQ(result, kExpectedApiResult); |
| |
| GURL subframe_url = |
| https_server_.GetURL("c.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(content::NavigateIframeToURL(web_contents(), |
| /*iframe_id=*/"frame", |
| subframe_url)); |
| EXPECT_EQ("[]", InvokeTopicsAPI(content::ChildFrameAt( |
| web_contents()->GetPrimaryMainFrame(), 0))); |
| |
| subframe_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(content::NavigateIframeToURL(web_contents(), |
| /*iframe_id=*/"frame", |
| subframe_url)); |
| |
| EXPECT_EQ( |
| "The \"browsing-topics\" Permissions Policy denied the use of " |
| "document.browsingTopics().", |
| InvokeTopicsAPI( |
| content::ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| TopicsAPINotAllowedInInsecureContext) { |
| GURL main_frame_url = embedded_test_server()->GetURL( |
| "a.test", "/browsing_topics/one_iframe_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| // Navigate the iframe to a https site. |
| GURL subframe_url = https_server_.GetURL("b.test", "/empty_page.html"); |
| content::NavigateIframeToURL(web_contents(), |
| /*iframe_id=*/"frame", subframe_url); |
| |
| // Both the main frame and the subframe are insecure context because the main |
| // frame is loaded over HTTP. Expect that the API isn't available in either |
| // frame. |
| EXPECT_EQ("not a function", InvokeTopicsAPI(web_contents())); |
| EXPECT_EQ("not a function", InvokeTopicsAPI(content::ChildFrameAt( |
| web_contents()->GetPrimaryMainFrame(), 0))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| TopicsAPINotAllowedInDetachedDocument) { |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| EXPECT_EQ( |
| "Failed to execute 'browsingTopics' on 'Document': A browsing " |
| "context is required when calling document.browsingTopics().", |
| EvalJs(web_contents(), R"( |
| const iframe = document.getElementById('frame'); |
| const childDocument = iframe.contentWindow.document; |
| iframe.remove(); |
| |
| childDocument.browsingTopics() |
| .then(topics => "success") |
| .catch(error => error.message); |
| )")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| TopicsAPINotAllowedInOpaqueOriginDocument) { |
| GURL main_frame_url = https_server_.GetURL( |
| "a.test", "/browsing_topics/one_sandboxed_iframe_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| EXPECT_EQ( |
| "document.browsingTopics() is not allowed in an opaque origin context.", |
| InvokeTopicsAPI( |
| content::ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| TopicsAPINotAllowedInFencedFrame) { |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| GURL fenced_frame_url = |
| https_server_.GetURL("b.test", "/fenced_frames/title1.html"); |
| |
| content::RenderFrameHostWrapper fenced_frame_rfh_wrapper( |
| fenced_frame_test_helper_.CreateFencedFrame( |
| web_contents()->GetPrimaryMainFrame(), fenced_frame_url)); |
| |
| EXPECT_EQ("document.browsingTopics() is not allowed in a fenced frame.", |
| InvokeTopicsAPI(fenced_frame_rfh_wrapper.get())); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| TopicsAPINotAllowedInPrerenderedPage) { |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| GURL prerender_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| content::FrameTreeNodeId host_id = |
| prerender_helper().AddPrerender(prerender_url); |
| |
| content::RenderFrameHost* prerender_host = |
| prerender_helper().GetPrerenderedMainFrameHost(host_id); |
| EXPECT_EQ( |
| "document.browsingTopics() is not allowed when the page is being " |
| "prerendered.", |
| InvokeTopicsAPI(prerender_host)); |
| |
| // Activate the prerendered page. The API call should succeed. |
| content::test::PrerenderHostObserver prerender_observer(*web_contents(), |
| prerender_url); |
| prerender_helper().NavigatePrimaryPage(prerender_url); |
| prerender_observer.WaitForActivation(); |
| |
| std::string result = InvokeTopicsAPI(web_contents()); |
| |
| EXPECT_EQ(result, kExpectedApiResult); |
| } |
| |
| // Regression test for crbug/1339735. |
| IN_PROC_BROWSER_TEST_F( |
| BrowsingTopicsBrowserTest, |
| TopicsAPIInvokedInMainFrameUnloadHandler_NoRendererCrash) { |
| GURL main_frame_url = https_server_.GetURL( |
| "a.test", "/browsing_topics/get_topics_during_unload.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| // A renderer crash won't always be captured if the renderer is also shutting |
| // down naturally around the same time. Thus, we create a new page in the same |
| // renderer process to keep the renderer process alive when the page navigates |
| // away later. |
| content::TestNavigationObserver popup_observer(main_frame_url); |
| popup_observer.StartWatchingNewWebContents(); |
| EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(), |
| content::JsReplace("window.open($1)", main_frame_url))); |
| popup_observer.Wait(); |
| |
| GURL new_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), new_url)); |
| } |
| |
| IN_PROC_BROWSER_TEST_P( |
| BrowsingTopicsSubresourceRequestTest, |
| SameOrigin_TopicsEligible_SendTopics_HasNoObserveResponse) { |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| GURL resource_url = https_server_.GetURL("a.test", GetRelativePath()); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| EXPECT_TRUE(topics_header_value); |
| EXPECT_EQ(*topics_header_value, kExpectedHeaderValueForSiteA); |
| |
| // No observation should have been recorded in addition to the pre-existing |
| // one. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 1u); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(BrowsingTopicsSubresourceRequestTest, |
| WithoutTopicsFlagSet) { |
| GURL main_frame_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| GURL resource_url = https_server_.GetURL("b.test", GetRelativePath()); |
| |
| { |
| // Invoke fetch() or img without the `browsingTopics` flag. This request |
| // isn't eligible for topics. |
| |
| EXPECT_TRUE(ExecJsWithMissingBrowsingTopicsAttribute(resource_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| // Expect no topics header as the request did not specify |
| // {browsingTopics: true}. |
| EXPECT_FALSE(topics_header_value); |
| } |
| |
| { |
| // Invoke fetch() or img with the `browsingTopics` flag set to false. This |
| // request isn't eligible for topics. |
| EXPECT_TRUE(ExecJSWithBrowsingTopicsFalse(resource_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| // Expect no topics header as the request did not specify |
| // {browsingTopics: true}. |
| EXPECT_FALSE(topics_header_value); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P( |
| BrowsingTopicsSubresourceRequestTest, |
| SameOrigin_TopicsEligible_SendNoTopic_HasNoObserveResponse) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| GURL resource_url = https_server_.GetURL("b.test", GetRelativePath()); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| // Expect an empty header value as "b.test" did not observe the candidate |
| // topics. |
| EXPECT_TRUE(topics_header_value); |
| EXPECT_EQ(topics_header_value, kExpectedHeaderValueForEmptyTopics); |
| |
| // No observation should have been recorded in addition to the pre-existing |
| // one, as the response did not have the `Observe-Browsing-Topics: ?1` header. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 1u); |
| |
| histogram_tester.ExpectUniqueSample( |
| kBrowsingTopicsApiActionTypeHistogramId, |
| GetBrowsingTopicsApiActionType(/*observe=*/false), |
| /*expected_bucket_count=*/1); |
| } |
| |
| IN_PROC_BROWSER_TEST_P( |
| BrowsingTopicsSubresourceRequestTest, |
| SameOrigin_TopicsEligible_SendNoTopic_HasObserveResponse) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| base::StringPairs replacement; |
| replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK")); |
| replacement.emplace_back(std::make_pair("{{OBSERVE_BROWSING_TOPICS_HEADER}}", |
| "Observe-Browsing-Topics: ?1")); |
| replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", "")); |
| |
| GURL resource_url = https_server_.GetURL( |
| "b.test", net::test_server::GetFilePathWithReplacements(GetRelativePath(), |
| replacement)); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| // A new observation should have been recorded in addition to the pre-existing |
| // one, as the response had the `Observe-Browsing-Topics: ?1` header and the |
| // request was eligible for topics. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 2u); |
| EXPECT_EQ(api_usage_contexts[0].hashed_main_frame_host, |
| HashMainFrameHostForStorage("foo1.com")); |
| EXPECT_EQ(api_usage_contexts[0].hashed_context_domain, HashedDomain(1)); |
| EXPECT_EQ( |
| api_usage_contexts[1].hashed_main_frame_host, |
| HashMainFrameHostForStorage(https_server_.GetURL("b.test", "/").host())); |
| EXPECT_EQ(api_usage_contexts[1].hashed_context_domain, |
| GetHashedDomain("b.test")); |
| |
| // Expect a "get" event and an "observe" event respectively. |
| histogram_tester.ExpectTotalCount(kBrowsingTopicsApiActionTypeHistogramId, |
| /*expected_count=*/2); |
| histogram_tester.ExpectBucketCount( |
| kBrowsingTopicsApiActionTypeHistogramId, |
| GetBrowsingTopicsApiActionType(/*observe=*/false), |
| /*expected_count=*/1); |
| histogram_tester.ExpectBucketCount( |
| kBrowsingTopicsApiActionTypeHistogramId, |
| GetBrowsingTopicsApiActionType(/*observe=*/true), |
| /*expected_count=*/1); |
| } |
| |
| IN_PROC_BROWSER_TEST_P( |
| BrowsingTopicsSubresourceRequestTest, |
| SameOrigin_TopicsNotEligibleDueToUserSettings_HasObserveResponse) { |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| base::StringPairs replacement; |
| replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK")); |
| replacement.emplace_back(std::make_pair("{{OBSERVE_BROWSING_TOPICS_HEADER}}", |
| "Observe-Browsing-Topics: ?1")); |
| replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", "")); |
| |
| GURL resource_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements(GetRelativePath(), |
| replacement)); |
| |
| CookieSettingsFactory::GetForProfile(browser()->profile()) |
| ->SetCookieSetting(resource_url, CONTENT_SETTING_BLOCK); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| // When the request is ineligible for topics due to user settings, an empty |
| // list of topics will be sent in the header. |
| EXPECT_EQ(topics_header_value, kExpectedHeaderValueForEmptyTopics); |
| |
| // No observation should have been recorded in addition to the pre-existing |
| // one even though the response had the `Observe-Browsing-Topics: ?1` header, |
| // as the request was not eligible for topics. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 1u); |
| } |
| |
| IN_PROC_BROWSER_TEST_P( |
| BrowsingTopicsSubresourceRequestTest, |
| CrossOrigin_TopicsEligible_SendTopics_HasObserveResponse) { |
| GURL main_frame_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| base::StringPairs replacement; |
| replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK")); |
| replacement.emplace_back(std::make_pair("{{OBSERVE_BROWSING_TOPICS_HEADER}}", |
| "Observe-Browsing-Topics: ?1")); |
| replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", "")); |
| |
| GURL resource_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements(GetRelativePath(), |
| replacement)); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| EXPECT_TRUE(topics_header_value); |
| EXPECT_EQ(*topics_header_value, kExpectedHeaderValueForSiteB); |
| |
| // A new observation should have been recorded in addition to the pre-existing |
| // one, as the response had the `Observe-Browsing-Topics: ?1` header and the |
| // request was eligible for topics. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 2u); |
| EXPECT_EQ( |
| api_usage_contexts[0].hashed_main_frame_host, |
| HashMainFrameHostForStorage(https_server_.GetURL("b.test", "/").host())); |
| EXPECT_EQ(api_usage_contexts[0].hashed_context_domain, |
| GetHashedDomain("a.test")); |
| EXPECT_EQ(api_usage_contexts[1].hashed_main_frame_host, |
| HashMainFrameHostForStorage("foo1.com")); |
| EXPECT_EQ(api_usage_contexts[1].hashed_context_domain, HashedDomain(1)); |
| } |
| |
| // On an insecure site (i.e. URL with http scheme), test a fetch or image |
| // request with the `browsingTopics` set to true. Expect it to throw an |
| // exception. |
| IN_PROC_BROWSER_TEST_P( |
| BrowsingTopicsSubresourceRequestTest, |
| CrossOrigin_TopicsNotEligibleDueToInsecureInitiatorContext) { |
| GURL main_frame_url = embedded_test_server()->GetURL( |
| "b.test", "/browsing_topics/empty_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| GURL resource_url = https_server_.GetURL("a.test", GetRelativePath()); |
| |
| if (TestFetch()) { |
| content::EvalJsResult result = EvalJs( |
| web_contents()->GetPrimaryMainFrame(), |
| content::JsReplace("fetch($1, {browsingTopics: true})", resource_url)); |
| EXPECT_THAT(result.error, |
| testing::HasSubstr("browsingTopics: Topics operations are only " |
| "available in secure contexts.")); |
| } else { |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| } |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| // Expect no topics header as the request was not eligible for topics due to |
| // insecure initiator context. |
| EXPECT_FALSE(topics_header_value); |
| } |
| |
| // Only allow topics from origin c.test, and test fetch requests to b.test and |
| // c.test to verify that only c.test gets them. |
| IN_PROC_BROWSER_TEST_P( |
| BrowsingTopicsSubresourceRequestTest, |
| CrossOrigin_TopicsNotEligibleDueToPermissionsPolicyAgainstRequestOrigin) { |
| base::StringPairs allowed_origin_replacement; |
| allowed_origin_replacement.emplace_back( |
| "{{ALLOWED_ORIGIN}}", https_server_.GetOrigin("c.test").Serialize()); |
| |
| GURL main_frame_url = https_server_.GetURL( |
| "b.test", net::test_server::GetFilePathWithReplacements( |
| "/browsing_topics/" |
| "one_iframe_page_browsing_topics_allow_certain_origin.html", |
| allowed_origin_replacement)); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| { |
| base::HistogramTester histogram_tester; |
| |
| GURL resource_url = https_server_.GetURL("a.test", GetRelativePath()); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| // No topics header was sent, as the permissions policy denied it. |
| EXPECT_FALSE(topics_header_value); |
| |
| // No BrowsingTopicsApiActionType metrics are recorded, as |
| // `BrowsingTopicsServiceImpl` did not get a chance to handle the request |
| // due to earlier permissions policy reject. |
| histogram_tester.ExpectTotalCount(kBrowsingTopicsApiActionTypeHistogramId, |
| /*expected_count=*/0); |
| } |
| |
| { |
| base::HistogramTester histogram_tester; |
| |
| GURL resource_url = https_server_.GetURL("c.test", GetRelativePath()); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| EXPECT_TRUE(topics_header_value); |
| |
| histogram_tester.ExpectUniqueSample( |
| kBrowsingTopicsApiActionTypeHistogramId, |
| GetBrowsingTopicsApiActionType(/*observe=*/false), |
| /*expected_bucket_count=*/1); |
| } |
| } |
| |
| // On site b.test, test a fetch or image request to a.test that gets redirected |
| // to c.test. The topics header should be calculated for them individually (i.e. |
| // given that only a.test has observed the candidate topics for site b.test, the |
| // request to a.test should have a non-empty topics header, while the redirected |
| // request to c.test should have an empty topics header.) |
| IN_PROC_BROWSER_TEST_P(BrowsingTopicsSubresourceRequestTest, |
| CrossOriginWithRedirect) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| base::StringPairs redirect_replacement; |
| redirect_replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK")); |
| redirect_replacement.emplace_back(std::make_pair( |
| "{{OBSERVE_BROWSING_TOPICS_HEADER}}", "Observe-Browsing-Topics: ?1")); |
| redirect_replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", "")); |
| |
| GURL redirect_url = https_server_.GetURL( |
| "c.test", net::test_server::GetFilePathWithReplacements( |
| GetRedirectRelativePath(), redirect_replacement)); |
| |
| base::StringPairs replacement; |
| replacement.emplace_back( |
| std::make_pair("{{STATUS}}", "301 Moved Permanently")); |
| replacement.emplace_back(std::make_pair("{{OBSERVE_BROWSING_TOPICS_HEADER}}", |
| "Observe-Browsing-Topics: ?1")); |
| replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", |
| "Location: " + redirect_url.spec())); |
| |
| GURL resource_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements(GetRelativePath(), |
| replacement)); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| { |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| EXPECT_TRUE(topics_header_value); |
| EXPECT_EQ(*topics_header_value, kExpectedHeaderValueForSiteB); |
| } |
| { |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRedirectRelativePath()); |
| EXPECT_TRUE(topics_header_value); |
| |
| // An empty topics header value was sent, because "c.test" did not observe |
| // the candidate topics. |
| EXPECT_EQ(topics_header_value, kExpectedHeaderValueForEmptyTopics); |
| } |
| |
| // Two new observations should have been recorded in addition to the |
| // pre-existing one. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 3u); |
| EXPECT_EQ( |
| api_usage_contexts[0].hashed_main_frame_host, |
| HashMainFrameHostForStorage(https_server_.GetURL("b.test", "/").host())); |
| EXPECT_EQ(api_usage_contexts[0].hashed_context_domain, |
| GetHashedDomain("c.test")); |
| EXPECT_EQ( |
| api_usage_contexts[1].hashed_main_frame_host, |
| HashMainFrameHostForStorage(https_server_.GetURL("b.test", "/").host())); |
| EXPECT_EQ(api_usage_contexts[1].hashed_context_domain, |
| GetHashedDomain("a.test")); |
| EXPECT_EQ(api_usage_contexts[2].hashed_main_frame_host, |
| HashMainFrameHostForStorage("foo1.com")); |
| EXPECT_EQ(api_usage_contexts[2].hashed_context_domain, HashedDomain(1)); |
| |
| // Expect two "get" events and two "observe" events for the initial request |
| // and the redirect respectively. |
| histogram_tester.ExpectTotalCount(kBrowsingTopicsApiActionTypeHistogramId, |
| /*expected_count=*/4); |
| histogram_tester.ExpectBucketCount( |
| kBrowsingTopicsApiActionTypeHistogramId, |
| GetBrowsingTopicsApiActionType(/*observe=*/false), |
| /*expected_count=*/2); |
| histogram_tester.ExpectBucketCount( |
| kBrowsingTopicsApiActionTypeHistogramId, |
| GetBrowsingTopicsApiActionType(/*observe=*/true), |
| /*expected_count=*/2); |
| } |
| |
| // On site b.test, test a fetch or image request to a.test that gets redirected |
| // to c.test. The topics header eligibility should be checked for them |
| // individually (i.e. given that the declared policy on the page only allows |
| // origin c.test, the request to a.test should not have the topics header, while |
| // the redirected request to c.test should have the topics header.) |
| IN_PROC_BROWSER_TEST_P( |
| BrowsingTopicsSubresourceRequestTest, |
| CrossOriginWithRedirect_InitialRequestTopicsNotEligibleDueToPermissionsPolicy) { |
| base::StringPairs allowed_origin_replacement; |
| allowed_origin_replacement.emplace_back( |
| "{{ALLOWED_ORIGIN}}", https_server_.GetOrigin("c.test").Serialize()); |
| |
| GURL main_frame_url = https_server_.GetURL( |
| "b.test", net::test_server::GetFilePathWithReplacements( |
| "/browsing_topics/" |
| "one_iframe_page_browsing_topics_allow_certain_origin.html", |
| allowed_origin_replacement)); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| base::StringPairs redirect_replacement; |
| redirect_replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK")); |
| redirect_replacement.emplace_back(std::make_pair( |
| "{{OBSERVE_BROWSING_TOPICS_HEADER}}", "Observe-Browsing-Topics: ?1")); |
| redirect_replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", "")); |
| |
| GURL redirect_url = https_server_.GetURL( |
| "c.test", net::test_server::GetFilePathWithReplacements( |
| GetRedirectRelativePath(), redirect_replacement)); |
| |
| base::StringPairs replacement; |
| replacement.emplace_back( |
| std::make_pair("{{STATUS}}", "301 Moved Permanently")); |
| replacement.emplace_back(std::make_pair("{{OBSERVE_BROWSING_TOPICS_HEADER}}", |
| "Observe-Browsing-Topics: ?1")); |
| replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", |
| "Location: " + redirect_url.spec())); |
| |
| GURL resource_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements(GetRelativePath(), |
| replacement)); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| { |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| // No topics header was sent, as the permissions policy denied it. |
| EXPECT_FALSE(topics_header_value); |
| } |
| { |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRedirectRelativePath()); |
| EXPECT_TRUE(topics_header_value); |
| |
| // An empty topics header value was sent, as "c.test" did not observe the |
| // candidate topics. |
| EXPECT_EQ(topics_header_value, kExpectedHeaderValueForEmptyTopics); |
| } |
| |
| // A new observation should have been recorded in addition to the pre-existing |
| // one. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 2u); |
| EXPECT_EQ( |
| api_usage_contexts[0].hashed_main_frame_host, |
| HashMainFrameHostForStorage(https_server_.GetURL("b.test", "/").host())); |
| EXPECT_EQ(api_usage_contexts[0].hashed_context_domain, |
| GetHashedDomain("c.test")); |
| EXPECT_EQ(api_usage_contexts[1].hashed_main_frame_host, |
| HashMainFrameHostForStorage("foo1.com")); |
| EXPECT_EQ(api_usage_contexts[1].hashed_context_domain, HashedDomain(1)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, UseCounter_DocumentApi) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| InvokeTopicsAPI(web_contents()); |
| |
| // Navigate away to flush use counters. |
| ASSERT_TRUE( |
| ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL))); |
| |
| histogram_tester.ExpectBucketCount( |
| "Blink.UseCounter.Features", |
| blink::mojom::WebFeature::kTopicsAPI_BrowsingTopics_Method, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, UseCounter_Fetch) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| GURL fetch_url = main_frame_url; |
| |
| { |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| // Send a fetch() request with `browsingTopics` set to false. Expect no |
| // `kTopicsAPIFetch` use counter. |
| EXPECT_TRUE(ExecJs( |
| web_contents()->GetPrimaryMainFrame(), |
| content::JsReplace("fetch($1, {browsingTopics: false})", fetch_url))); |
| |
| // Navigate away to flush use counters. |
| ASSERT_TRUE( |
| ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL))); |
| |
| histogram_tester.ExpectBucketCount( |
| "Blink.UseCounter.Features", blink::mojom::WebFeature::kTopicsAPIFetch, |
| 0); |
| } |
| |
| { |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| // Send a fetch() request with `browsingTopics` set to true. Expect one |
| // `kTopicsAPIFetch` use counter. |
| EXPECT_TRUE(ExecJs( |
| web_contents()->GetPrimaryMainFrame(), |
| content::JsReplace("fetch($1, {browsingTopics: true})", fetch_url))); |
| |
| // Navigate away to flush use counters. |
| ASSERT_TRUE( |
| ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL))); |
| |
| histogram_tester.ExpectBucketCount( |
| "Blink.UseCounter.Features", blink::mojom::WebFeature::kTopicsAPIFetch, |
| 1); |
| histogram_tester.ExpectBucketCount("Blink.UseCounter.Features", |
| blink::mojom::WebFeature::kTopicsAPIAll, |
| 1); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, UseCounter_Img_Unused) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| GURL img_url = https_server_.GetURL( |
| "a.test", "/browsing_topics/topics-writable-pixel.png"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| // Request an image with `browsingTopics` set to false. Expect no |
| // `kTopicsAPIImg` use counter. |
| EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(), |
| content::JsReplace(R"( |
| let img = document.createElement('img'); |
| img.src = $1; |
| img.browsingTopics = false; |
| img.decode() |
| .then(() => { |
| document.body.appendChild(img); |
| }) |
| )", |
| img_url.spec()))); |
| |
| // Navigate away to flush use counters. |
| ASSERT_TRUE( |
| ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL))); |
| |
| histogram_tester.ExpectBucketCount( |
| "Blink.UseCounter.Features", blink::mojom::WebFeature::kTopicsAPIImg, 0); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, UseCounter_Img_Used) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| GURL img_url = https_server_.GetURL( |
| "a.test", "/browsing_topics/topics-writable-pixel.png"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| // Request an img with `browsingTopics` set to true. Expect one |
| // `kTopicsAPIImg` use counter. |
| EXPECT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(), |
| content::JsReplace(R"( |
| let img = document.createElement('img'); |
| img.src = $1; |
| img.browsingTopics = true; |
| img.decode() |
| .then(() => { |
| document.body.appendChild(img); |
| }) |
| )", |
| img_url.spec()))); |
| |
| // Navigate away to flush use counters. |
| ASSERT_TRUE( |
| ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL))); |
| |
| histogram_tester.ExpectBucketCount( |
| "Blink.UseCounter.Features", blink::mojom::WebFeature::kTopicsAPIImg, 1); |
| histogram_tester.ExpectBucketCount( |
| "Blink.UseCounter.Features", blink::mojom::WebFeature::kTopicsAPIAll, 1); |
| } |
| |
| // For a page that contains a static <iframe> with a "browsingtopics" |
| // attribute, the iframe navigation request should be eligible for topics. |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| CrossOriginStaticIframeWithTopicsAttribute) { |
| base::StringPairs replacement; |
| replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK")); |
| replacement.emplace_back(std::make_pair("{{OBSERVE_BROWSING_TOPICS_HEADER}}", |
| "Observe-Browsing-Topics: ?1")); |
| replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", "")); |
| |
| GURL subframe_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements( |
| "/browsing_topics/" |
| "page_with_custom_topics_header.html", |
| replacement)); |
| |
| base::StringPairs topics_attribute_replacement; |
| topics_attribute_replacement.emplace_back( |
| "{{MAYBE_BROWSING_TOPICS_ATTRIBUTE}}", "browsingtopics"); |
| |
| topics_attribute_replacement.emplace_back("{{SRC_URL}}", subframe_url.spec()); |
| |
| GURL main_frame_url = https_server_.GetURL( |
| "b.test", net::test_server::GetFilePathWithReplacements( |
| "/browsing_topics/page_with_custom_attribute_iframe.html", |
| topics_attribute_replacement)); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath( |
| "/browsing_topics/page_with_custom_topics_header.html"); |
| EXPECT_TRUE(topics_header_value); |
| EXPECT_EQ(*topics_header_value, kExpectedHeaderValueForSiteB); |
| |
| // A new observation should have been recorded in addition to the pre-existing |
| // one. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 2u); |
| EXPECT_EQ( |
| api_usage_contexts[0].hashed_main_frame_host, |
| HashMainFrameHostForStorage(https_server_.GetURL("b.test", "/").host())); |
| EXPECT_EQ(api_usage_contexts[0].hashed_context_domain, |
| GetHashedDomain("a.test")); |
| EXPECT_EQ(api_usage_contexts[1].hashed_main_frame_host, |
| HashMainFrameHostForStorage("foo1.com")); |
| EXPECT_EQ(api_usage_contexts[1].hashed_context_domain, HashedDomain(1)); |
| } |
| |
| // For a page that contains a static <iframe> without a "browsingtopics" |
| // attribute, the iframe navigation request should not be eligible for topics. |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| CrossOriginStaticIframeWithoutTopicsAttribute) { |
| base::StringPairs replacement; |
| replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK")); |
| replacement.emplace_back(std::make_pair("{{OBSERVE_BROWSING_TOPICS_HEADER}}", |
| "Observe-Browsing-Topics: ?1")); |
| replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", "")); |
| |
| GURL subframe_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements( |
| "/browsing_topics/" |
| "page_with_custom_topics_header.html", |
| replacement)); |
| |
| base::StringPairs topics_attribute_replacement; |
| topics_attribute_replacement.emplace_back( |
| "{{MAYBE_BROWSING_TOPICS_ATTRIBUTE}}", ""); |
| |
| topics_attribute_replacement.emplace_back("{{SRC_URL}}", subframe_url.spec()); |
| |
| GURL main_frame_url = https_server_.GetURL( |
| "b.test", net::test_server::GetFilePathWithReplacements( |
| "/browsing_topics/page_with_custom_attribute_iframe.html", |
| topics_attribute_replacement)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath( |
| "/browsing_topics/page_with_custom_topics_header.html"); |
| EXPECT_FALSE(topics_header_value); |
| |
| // Since the request wasn't eligible for topics, no observation should have |
| // been recorded in addition to the pre-existing one, even though the response |
| // contains a `Observe-Browsing-Topics: ?1` header. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 1u); |
| } |
| |
| // For a page with a dynamically appended iframe with iframe.browsingTopics set |
| // to true, the iframe navigation request should be eligible for topics. |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| CrossOriginDynamicIframeWithTopicsAttribute) { |
| GURL main_frame_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| base::StringPairs replacement; |
| replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK")); |
| replacement.emplace_back(std::make_pair("{{OBSERVE_BROWSING_TOPICS_HEADER}}", |
| "Observe-Browsing-Topics: ?1")); |
| replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", "")); |
| |
| GURL subframe_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements( |
| "/browsing_topics/" |
| "page_with_custom_topics_header.html", |
| replacement)); |
| |
| CreateIframe(subframe_url, /*browsing_topics_attribute=*/true); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath( |
| "/browsing_topics/page_with_custom_topics_header.html"); |
| EXPECT_TRUE(topics_header_value); |
| EXPECT_EQ(*topics_header_value, kExpectedHeaderValueForSiteB); |
| |
| // A new observation should have been recorded in addition to the pre-existing |
| // one. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 2u); |
| EXPECT_EQ( |
| api_usage_contexts[0].hashed_main_frame_host, |
| HashMainFrameHostForStorage(https_server_.GetURL("b.test", "/").host())); |
| EXPECT_EQ(api_usage_contexts[0].hashed_context_domain, |
| GetHashedDomain("a.test")); |
| EXPECT_EQ(api_usage_contexts[1].hashed_main_frame_host, |
| HashMainFrameHostForStorage("foo1.com")); |
| EXPECT_EQ(api_usage_contexts[1].hashed_context_domain, HashedDomain(1)); |
| } |
| |
| // For a page with a dynamically appended iframe with iframe.browsingTopics set |
| // to true, the iframe navigation request should not be eligible for topics. |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| CrossOriginDynamicIframeWithoutTopicsAttribute) { |
| GURL main_frame_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| base::StringPairs replacement; |
| replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK")); |
| replacement.emplace_back(std::make_pair("{{OBSERVE_BROWSING_TOPICS_HEADER}}", |
| "Observe-Browsing-Topics: ?1")); |
| replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", "")); |
| |
| GURL subframe_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements( |
| "/browsing_topics/" |
| "page_with_custom_topics_header.html", |
| replacement)); |
| |
| CreateIframe(subframe_url); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath( |
| "/browsing_topics/page_with_custom_topics_header.html"); |
| EXPECT_FALSE(topics_header_value); |
| |
| // Since the request wasn't eligible for topics, no observation should have |
| // been recorded in addition to the pre-existing one, even though the response |
| // contains a `Observe-Browsing-Topics: ?1` header. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 1u); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| BrowsingTopicsBrowserTest, |
| CrossOriginDynamicIframe_TopicsNotEligibleDueToUserSettings_HasObserveResponse) { |
| GURL main_frame_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| base::StringPairs replacement; |
| replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK")); |
| replacement.emplace_back(std::make_pair("{{OBSERVE_BROWSING_TOPICS_HEADER}}", |
| "Observe-Browsing-Topics: ?1")); |
| replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", "")); |
| |
| GURL subframe_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements( |
| "/browsing_topics/" |
| "page_with_custom_topics_header.html", |
| replacement)); |
| |
| CookieSettingsFactory::GetForProfile(browser()->profile()) |
| ->SetCookieSetting(subframe_url, CONTENT_SETTING_BLOCK); |
| |
| CreateIframe(subframe_url, /*browsing_topics_attribute=*/true); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath( |
| "/browsing_topics/page_with_custom_topics_header.html"); |
| |
| // When the request is ineligible for topics due to user settings, an empty |
| // list of topics will be sent in the header. |
| EXPECT_EQ(topics_header_value, kExpectedHeaderValueForEmptyTopics); |
| |
| // No observation should have been recorded in addition to the pre-existing |
| // one even though the response had the `Observe-Browsing-Topics: ?1` header, |
| // as the request was not eligible for topics. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 1u); |
| } |
| |
| // Only allow topics from origin c.test, and test <iframe browsingtopics> |
| // requests to b.test and c.test to verify that only c.test gets the header. |
| IN_PROC_BROWSER_TEST_F( |
| BrowsingTopicsBrowserTest, |
| CrossOriginIframe_TopicsNotEligibleDueToPermissionsPolicyAgainstRequestOrigin) { |
| base::StringPairs allowed_origin_replacement; |
| allowed_origin_replacement.emplace_back( |
| "{{ALLOWED_ORIGIN}}", https_server_.GetOrigin("c.test").Serialize()); |
| |
| GURL main_frame_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements( |
| "/browsing_topics/" |
| "one_iframe_page_browsing_topics_allow_certain_origin.html", |
| allowed_origin_replacement)); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| { |
| GURL subframe_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| CreateIframe(subframe_url, /*browsing_topics_attribute=*/true); |
| |
| // No topics header was sent, as the permissions policy denied it. |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath("/browsing_topics/empty_page.html"); |
| EXPECT_FALSE(topics_header_value); |
| } |
| |
| { |
| GURL subframe_url = |
| https_server_.GetURL("c.test", "/browsing_topics/empty_page.html"); |
| |
| CreateIframe(subframe_url, /*browsing_topics_attribute=*/true); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath("/browsing_topics/empty_page.html"); |
| EXPECT_TRUE(topics_header_value); |
| } |
| } |
| |
| // On site b.test, test <iframe browsingtopics> request to a.test that gets |
| // redirected to c.test. The topics header should be calculated for them |
| // individually (i.e. given that only a.test has observed the candidate topics |
| // for site b.test, the request to a.test should have a non-empty topics header, |
| // while the redirected request to c.test should have an empty topics header.) |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| CrossOriginIframeWithRedirect) { |
| GURL main_frame_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| base::StringPairs redirect_replacement; |
| redirect_replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK")); |
| redirect_replacement.emplace_back(std::make_pair( |
| "{{OBSERVE_BROWSING_TOPICS_HEADER}}", "Observe-Browsing-Topics: ?1")); |
| redirect_replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", "")); |
| |
| GURL redirect_url = https_server_.GetURL( |
| "c.test", net::test_server::GetFilePathWithReplacements( |
| "/browsing_topics/" |
| "page_with_custom_topics_header2.html", |
| redirect_replacement)); |
| |
| base::StringPairs replacement; |
| replacement.emplace_back( |
| std::make_pair("{{STATUS}}", "301 Moved Permanently")); |
| replacement.emplace_back(std::make_pair("{{OBSERVE_BROWSING_TOPICS_HEADER}}", |
| "Observe-Browsing-Topics: ?1")); |
| replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", |
| "Location: " + redirect_url.spec())); |
| |
| GURL subframe_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements( |
| "/browsing_topics/" |
| "page_with_custom_topics_header.html", |
| replacement)); |
| |
| CreateIframe(subframe_url, /*browsing_topics_attribute=*/true); |
| |
| { |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath( |
| "/browsing_topics/page_with_custom_topics_header.html"); |
| EXPECT_TRUE(topics_header_value); |
| EXPECT_EQ(*topics_header_value, kExpectedHeaderValueForSiteB); |
| } |
| { |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath( |
| "/browsing_topics/page_with_custom_topics_header2.html"); |
| EXPECT_TRUE(topics_header_value); |
| |
| // An empty topics header value was sent, because "c.test" did not observe |
| // the candidate topics. |
| EXPECT_EQ(topics_header_value, kExpectedHeaderValueForEmptyTopics); |
| } |
| |
| // Two new observations should have been recorded in addition to the |
| // pre-existing one. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 3u); |
| EXPECT_EQ( |
| api_usage_contexts[0].hashed_main_frame_host, |
| HashMainFrameHostForStorage(https_server_.GetURL("b.test", "/").host())); |
| EXPECT_EQ(api_usage_contexts[0].hashed_context_domain, |
| GetHashedDomain("c.test")); |
| EXPECT_EQ( |
| api_usage_contexts[1].hashed_main_frame_host, |
| HashMainFrameHostForStorage(https_server_.GetURL("b.test", "/").host())); |
| EXPECT_EQ(api_usage_contexts[1].hashed_context_domain, |
| GetHashedDomain("a.test")); |
| EXPECT_EQ(api_usage_contexts[2].hashed_main_frame_host, |
| HashMainFrameHostForStorage("foo1.com")); |
| EXPECT_EQ(api_usage_contexts[2].hashed_context_domain, HashedDomain(1)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, RedirectMetrics_OnePage) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| // Expect no UMA, as Topics API has not been invoked in the page. |
| histogram_tester.ExpectTotalCount(kRedirectTopicsCallingSitesCountHistogramId, |
| /*expected_count=*/0); |
| |
| InvokeTopicsAPI(web_contents()); |
| |
| histogram_tester.ExpectUniqueSample( |
| kRedirectTopicsCallingSitesCountHistogramId, |
| /*sample=*/1, |
| /*expected_bucket_count=*/1); |
| |
| // Calling Topics API the second time won't record UMA again. |
| InvokeTopicsAPI(web_contents()); |
| |
| // Expect a sample emitted from the current page. |
| histogram_tester.ExpectUniqueSample( |
| kRedirectTopicsCallingSitesCountHistogramId, |
| /*sample=*/1, |
| /*expected_bucket_count=*/1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| BrowsingTopicsBrowserTest, |
| RedirectMetrics_TwoPages_DifferentSites_FirstPageDoesNotInvokeTopics) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url1 = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url1)); |
| |
| GURL main_frame_url2 = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents(), main_frame_url2)); |
| |
| // Expect no UMA, as Topics API has not been invoked in the redirect chain. |
| histogram_tester.ExpectTotalCount(kRedirectTopicsCallingSitesCountHistogramId, |
| /*expected_count=*/0); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| // Expect a sample emitted from the second page. |
| histogram_tester.ExpectUniqueSample( |
| kRedirectTopicsCallingSitesCountHistogramId, |
| /*sample=*/1, |
| /*expected_bucket_count=*/1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| BrowsingTopicsBrowserTest, |
| RedirectMetrics_TwoPages_DifferentSites_FirstPageInvokesTopics) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url1 = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url1)); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| GURL main_frame_url2 = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents(), main_frame_url2)); |
| |
| // Expect no sample except for the one emitted from the first page. |
| histogram_tester.ExpectUniqueSample( |
| kRedirectTopicsCallingSitesCountHistogramId, |
| /*sample=*/1, |
| /*expected_bucket_count=*/1); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| // Expect a sample emitted from the second page. |
| histogram_tester.ExpectBucketCount( |
| kRedirectTopicsCallingSitesCountHistogramId, |
| /*sample=*/2, |
| /*expected_count=*/1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| BrowsingTopicsBrowserTest, |
| RedirectMetrics_TwoPages_SameSites_FirstPageDoesNotInvokeTopics) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url1 = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url1)); |
| |
| GURL main_frame_url2 = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents(), main_frame_url2)); |
| |
| // Expect no UMA, as Topics API has not been invoked in the redirect chain. |
| histogram_tester.ExpectTotalCount(kRedirectTopicsCallingSitesCountHistogramId, |
| /*expected_count=*/0); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| // Expect a sample emitted from the second page. |
| histogram_tester.ExpectUniqueSample( |
| kRedirectTopicsCallingSitesCountHistogramId, |
| /*sample=*/1, |
| /*expected_bucket_count=*/1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| BrowsingTopicsBrowserTest, |
| RedirectMetrics_TwoPages_SameSites_FirstPageInvokesTopics) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url1 = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url1)); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| GURL main_frame_url2 = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents(), main_frame_url2)); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| // Expect no sample except for the one emitted from the first page, because |
| // the second page's site is not a new site in the redirect chain. |
| histogram_tester.ExpectUniqueSample( |
| kRedirectTopicsCallingSitesCountHistogramId, |
| /*sample=*/1, |
| /*expected_bucket_count=*/1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| RedirectMetrics_HasGesture_RedirectTrackingReset) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url1 = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url1)); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| GURL main_frame_url2 = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| // Trigger a renderer navigation with user activation. The redirect tracking |
| // will be reset. |
| ASSERT_TRUE( |
| content::NavigateToURLFromRenderer(web_contents(), main_frame_url2)); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| histogram_tester.ExpectUniqueSample( |
| kRedirectTopicsCallingSitesCountHistogramId, |
| /*sample=*/1, |
| /*expected_bucket_count=*/2); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| BrowsingTopicsBrowserTest, |
| RedirectMetrics_BrowserInitiatedNavigation_RedirectTrackingReset) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url1 = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url1)); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| GURL main_frame_url2 = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| // Trigger a browser-initiated navigation. The redirect tracking will be |
| // reset. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url2)); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| histogram_tester.ExpectUniqueSample( |
| kRedirectTopicsCallingSitesCountHistogramId, |
| /*sample=*/1, |
| /*expected_bucket_count=*/2); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| RedirectMetrics_PopUp_RedirectTrackingReset) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url1 = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url1)); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| // Enable automated pop-ups. |
| HostContentSettingsMapFactory::GetForProfile(browser()->profile()) |
| ->SetContentSettingDefaultScope(web_contents()->GetURL(), GURL(), |
| ContentSettingsType::POPUPS, |
| CONTENT_SETTING_ALLOW); |
| |
| GURL main_frame_url2 = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| // Trigger an automated pop-up. The redirect tracking will be reset. |
| content::WebContentsAddedObserver observer; |
| EXPECT_TRUE(content::ExecJs( |
| web_contents(), content::JsReplace("window.open($1)", main_frame_url2), |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| |
| content::WebContents* new_web_contents = observer.GetWebContents(); |
| content::TestNavigationObserver popup_navigation_observer(new_web_contents); |
| popup_navigation_observer.Wait(); |
| |
| InvokeTopicsAPI(new_web_contents, /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| histogram_tester.ExpectUniqueSample( |
| kRedirectTopicsCallingSitesCountHistogramId, |
| /*sample=*/1, |
| /*expected_bucket_count=*/2); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| RedirectMetrics_PopUpAndOpenerNavigation) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url1 = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url1)); |
| |
| content::WebContents* initial_web_contents = web_contents(); |
| InvokeTopicsAPI(initial_web_contents, /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| GURL main_frame_url2 = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| |
| // Trigger a pop-up (with user gesture). |
| content::WebContentsAddedObserver observer; |
| EXPECT_TRUE( |
| content::ExecJs(initial_web_contents, |
| content::JsReplace("window.open($1)", main_frame_url2))); |
| |
| content::WebContents* new_web_contents = observer.GetWebContents(); |
| content::TestNavigationObserver popup_navigation_observer(new_web_contents); |
| popup_navigation_observer.Wait(); |
| |
| GURL main_frame_url3 = |
| https_server_.GetURL("c.test", "/browsing_topics/empty_page.html"); |
| |
| // Trigger an opener navigation from the pop-up page. |
| content::TestNavigationObserver opener_navigation_observer( |
| initial_web_contents); |
| EXPECT_TRUE(content::ExecJs( |
| new_web_contents, |
| content::JsReplace("window.opener.location.href = $1", main_frame_url3), |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| opener_navigation_observer.Wait(); |
| |
| InvokeTopicsAPI(initial_web_contents, /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| // Expect a new sample. This implies that the page resulted from the opener |
| // navigation was initialized with the redirect status derived from the |
| // initial page. |
| histogram_tester.ExpectBucketCount( |
| kRedirectTopicsCallingSitesCountHistogramId, |
| /*sample=*/2, |
| /*expected_count=*/1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| BrowsingTopicsBrowserTest, |
| RedirectMetrics_SameDocNavigation_RedirectStateUnaffected) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url1 = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url1)); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| GURL main_frame_url2 = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html#123"); |
| |
| // Trigger a same-doc navigation with user activation. The page and its |
| // redirect state won't be affected. |
| ASSERT_TRUE( |
| content::NavigateToURLFromRenderer(web_contents(), main_frame_url2)); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| histogram_tester.ExpectUniqueSample( |
| kRedirectTopicsCallingSitesCountHistogramId, |
| /*sample=*/1, |
| /*expected_bucket_count=*/1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, RedirectMetrics_CapReached) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| ASSERT_EQ(distinct_cert_hostnames_.size(), 14u); |
| |
| for (const std::string& host : distinct_cert_hostnames_) { |
| GURL new_main_frame_url = |
| https_server_.GetURL(host, "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents(), new_main_frame_url)); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| } |
| |
| // For each bucket from 1 to 5, expect a single sample. |
| for (int i = 1; i < 5; ++i) { |
| histogram_tester.ExpectBucketCount( |
| kRedirectTopicsCallingSitesCountHistogramId, |
| /*sample=*/i, |
| /*expected_count=*/1); |
| } |
| |
| // Expect no more samples in other buckets, as we cap the number at 5. |
| histogram_tester.ExpectTotalCount(kRedirectTopicsCallingSitesCountHistogramId, |
| /*expected_count=*/5); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, |
| Download_RedirectStateUnaffected) { |
| base::HistogramTester histogram_tester; |
| |
| GURL main_frame_url1 = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url1)); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| GURL download_url = |
| https_server_.GetURL("b.test", "/downloads/a_zip_file.zip"); |
| |
| // Trigger a renderer-initiated navigation that turns into a download. The |
| // page and its redirect state won't be affected. |
| std::unique_ptr<content::DownloadTestObserver> observer( |
| new content::DownloadTestObserverTerminal( |
| browser()->profile()->GetDownloadManager(), /*wait_count=*/1, |
| content::DownloadTestObserver::ON_DANGEROUS_DOWNLOAD_FAIL)); |
| ASSERT_FALSE(content::NavigateToURLFromRendererWithoutUserGesture( |
| web_contents(), download_url)); |
| observer->WaitForFinished(); |
| |
| EXPECT_EQ( |
| 1u, observer->NumDownloadsSeenInState(download::DownloadItem::COMPLETE)); |
| |
| InvokeTopicsAPI(web_contents(), /*skip_observation=*/false, |
| content::EXECUTE_SCRIPT_NO_USER_GESTURE); |
| |
| histogram_tester.ExpectUniqueSample( |
| kRedirectTopicsCallingSitesCountHistogramId, |
| /*sample=*/1, |
| /*expected_bucket_count=*/1); |
| } |
| |
| // Tests that the Topics API abides by the Privacy Sandbox Enrollment framework. |
| class AttestationBrowsingTopicsBrowserTest : public BrowsingTopicsBrowserTest { |
| public: |
| void SetUpOnMainThread() override { |
| // This test suite tests Privacy Sandbox Attestations related behaviors, |
| // turn off the setting that makes all APIs considered attested. |
| BrowsingTopicsBrowserTest::SetUpOnMainThread(); |
| privacy_sandbox::PrivacySandboxAttestations::GetInstance() |
| ->SetAllPrivacySandboxAttestedForTesting(false); |
| } |
| |
| ~AttestationBrowsingTopicsBrowserTest() override = default; |
| }; |
| |
| class AttestationSubresourceRequestTest |
| : public BrowsingTopicsSubresourceRequestTest { |
| public: |
| void SetUpOnMainThread() override { |
| // This test suite tests Privacy Sandbox Attestations related behaviors, |
| // turn off the setting that makes all APIs considered attested. |
| BrowsingTopicsBrowserTest::SetUpOnMainThread(); |
| privacy_sandbox::PrivacySandboxAttestations::GetInstance() |
| ->SetAllPrivacySandboxAttestedForTesting(false); |
| } |
| |
| ~AttestationSubresourceRequestTest() override = default; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| AttestationSubresourceRequestTest, |
| ::testing::Bool()); |
| |
| // Site a.test is attested for Topics, so it should receive a valid response. |
| IN_PROC_BROWSER_TEST_F(AttestationBrowsingTopicsBrowserTest, |
| AttestedSiteCanGetBrowsingTopicsViaDocumentAPI) { |
| privacy_sandbox::PrivacySandboxAttestationsMap map; |
| map.insert_or_assign( |
| net::SchemefulSite(GURL("https://a.test")), |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPISet{ |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPI::kTopics}); |
| privacy_sandbox::PrivacySandboxAttestations::GetInstance() |
| ->SetAttestationsForTesting(map); |
| |
| content::WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("Attestation check for Topics on * failed."); |
| |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| std::string result = InvokeTopicsAPI(web_contents()); |
| EXPECT_EQ(result, kExpectedApiResult); |
| |
| EXPECT_TRUE(console_observer.messages().empty()); |
| } |
| |
| // Site a.test is not attested for Topics, so it should receive no topics. Note: |
| // Attestation failure works differently from other failure modes like operating |
| // in an insecure context. In this case, the API is still exposed, but handling |
| // will exit before any topics are filled. |
| IN_PROC_BROWSER_TEST_F(AttestationBrowsingTopicsBrowserTest, |
| UnattestedSiteCannotGetBrowsingTopicsViaDocumentAPI) { |
| privacy_sandbox::PrivacySandboxAttestationsMap map; |
| map.insert_or_assign( |
| net::SchemefulSite(GURL("https://b.test")), |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPISet{ |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPI::kTopics}); |
| privacy_sandbox::PrivacySandboxAttestations::GetInstance() |
| ->SetAttestationsForTesting(map); |
| |
| content::WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("Attestation check for Topics on * failed."); |
| |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| EXPECT_EQ("[]", InvokeTopicsAPI(web_contents())); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| EXPECT_FALSE(console_observer.messages().empty()); |
| } |
| |
| // Site a.test is attested, but not for Topics, so no topics should be returned. |
| IN_PROC_BROWSER_TEST_F( |
| AttestationBrowsingTopicsBrowserTest, |
| AttestedSiteCannotGetBrowsingTopicsViaDocumentAPIWithMismatchedMap) { |
| privacy_sandbox::PrivacySandboxAttestationsMap map; |
| map.insert_or_assign(net::SchemefulSite(GURL("https://a.test")), |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPISet{ |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPI:: |
| kProtectedAudience}); |
| privacy_sandbox::PrivacySandboxAttestations::GetInstance() |
| ->SetAttestationsForTesting(map); |
| |
| content::WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("Attestation check for Topics on * failed."); |
| |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| EXPECT_EQ("[]", InvokeTopicsAPI(web_contents())); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| EXPECT_FALSE(console_observer.messages().empty()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(AttestationSubresourceRequestTest, |
| SameOrigin_TopicsEligible_SendTopics_SiteAttested) { |
| privacy_sandbox::PrivacySandboxAttestationsMap map; |
| map.insert_or_assign( |
| net::SchemefulSite(GURL("https://a.test")), |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPISet{ |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPI::kTopics}); |
| privacy_sandbox::PrivacySandboxAttestations::GetInstance() |
| ->SetAttestationsForTesting(map); |
| |
| content::WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("Attestation check for Topics on * failed."); |
| |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| GURL resource_url = https_server_.GetURL("a.test", GetRelativePath()); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| EXPECT_TRUE(topics_header_value); |
| EXPECT_EQ(*topics_header_value, kExpectedHeaderValueForSiteA); |
| |
| EXPECT_TRUE(console_observer.messages().empty()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(AttestationSubresourceRequestTest, |
| SameOrigin_TopicsEligible_SiteNotAttested) { |
| privacy_sandbox::PrivacySandboxAttestationsMap map; |
| map.insert_or_assign( |
| net::SchemefulSite(GURL("https://b.test")), |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPISet{ |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPI::kTopics}); |
| privacy_sandbox::PrivacySandboxAttestations::GetInstance() |
| ->SetAttestationsForTesting(map); |
| |
| content::WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("Attestation check for Topics on * failed."); |
| |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| GURL resource_url = https_server_.GetURL("a.test", GetRelativePath()); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| EXPECT_EQ(topics_header_value, kExpectedHeaderValueForEmptyTopics); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| EXPECT_FALSE(console_observer.messages().empty()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(AttestationSubresourceRequestTest, |
| SameOrigin_TopicsEligible_SiteAttested_MismatchedMap) { |
| privacy_sandbox::PrivacySandboxAttestationsMap map; |
| map.insert_or_assign(net::SchemefulSite(GURL("https://a.test")), |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPISet{ |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPI:: |
| kProtectedAudience}); |
| privacy_sandbox::PrivacySandboxAttestations::GetInstance() |
| ->SetAttestationsForTesting(map); |
| |
| content::WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("Attestation check for Topics on * failed."); |
| |
| GURL main_frame_url = |
| https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| GURL resource_url = https_server_.GetURL("a.test", GetRelativePath()); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| EXPECT_EQ(topics_header_value, kExpectedHeaderValueForEmptyTopics); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| EXPECT_FALSE(console_observer.messages().empty()); |
| } |
| |
| // Site a.test is attested, so when an x-origin request is made to it from |
| // site b.test, a.test should still include a topics header. |
| IN_PROC_BROWSER_TEST_P( |
| AttestationSubresourceRequestTest, |
| CrossOrigin_TopicsEligible_SendTopics_HasObserveResponse_SiteAttested) { |
| privacy_sandbox::PrivacySandboxAttestationsMap map; |
| map.insert_or_assign( |
| net::SchemefulSite(GURL("https://a.test")), |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPISet{ |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPI::kTopics}); |
| privacy_sandbox::PrivacySandboxAttestations::GetInstance() |
| ->SetAttestationsForTesting(map); |
| |
| content::WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("Attestation check for Topics on * failed."); |
| |
| GURL main_frame_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| base::StringPairs replacement; |
| replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK")); |
| replacement.emplace_back(std::make_pair("{{OBSERVE_BROWSING_TOPICS_HEADER}}", |
| "Observe-Browsing-Topics: ?1")); |
| replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", "")); |
| |
| GURL resource_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements(GetRelativePath(), |
| replacement)); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| EXPECT_TRUE(topics_header_value); |
| EXPECT_EQ(*topics_header_value, kExpectedHeaderValueForSiteB); |
| |
| // A new observation should have been recorded in addition to the pre-existing |
| // one, as the response had the `Observe-Browsing-Topics: ?1` header and the |
| // request was eligible for topics. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 2u); |
| EXPECT_EQ( |
| api_usage_contexts[0].hashed_main_frame_host, |
| HashMainFrameHostForStorage(https_server_.GetURL("b.test", "/").host())); |
| EXPECT_EQ(api_usage_contexts[0].hashed_context_domain, |
| GetHashedDomain("a.test")); |
| EXPECT_EQ(api_usage_contexts[1].hashed_main_frame_host, |
| HashMainFrameHostForStorage("foo1.com")); |
| EXPECT_EQ(api_usage_contexts[1].hashed_context_domain, HashedDomain(1)); |
| |
| EXPECT_TRUE(console_observer.messages().empty()); |
| } |
| |
| // Site a.test is not attested, so this should not generate a Topics header in a |
| // x-origin fetch to site a.test. |
| IN_PROC_BROWSER_TEST_P(AttestationSubresourceRequestTest, |
| CrossOrigin_TopicsEligible_SiteNotAttested) { |
| privacy_sandbox::PrivacySandboxAttestationsMap map; |
| map.insert_or_assign( |
| net::SchemefulSite(GURL("https://b.test")), |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPISet{ |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPI::kTopics}); |
| privacy_sandbox::PrivacySandboxAttestations::GetInstance() |
| ->SetAttestationsForTesting(map); |
| |
| content::WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("Attestation check for Topics on * failed."); |
| |
| GURL main_frame_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| base::StringPairs replacement; |
| replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK")); |
| replacement.emplace_back(std::make_pair("{{OBSERVE_BROWSING_TOPICS_HEADER}}", |
| "Observe-Browsing-Topics: ?1")); |
| replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", "")); |
| |
| GURL resource_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements(GetRelativePath(), |
| replacement)); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| EXPECT_EQ(topics_header_value, kExpectedHeaderValueForEmptyTopics); |
| |
| // Because a.test is not attested for Topics, we should not have any new |
| // observations of API usage. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 1u); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| EXPECT_FALSE(console_observer.messages().empty()); |
| } |
| |
| // Site a.test is attested, but not for Topics, so the fetch/img request to |
| // a.test should not get a header. |
| IN_PROC_BROWSER_TEST_P( |
| AttestationSubresourceRequestTest, |
| CrossOrigin_TopicsEligible_SiteNotAttested_MismatchedMap) { |
| privacy_sandbox::PrivacySandboxAttestationsMap map; |
| map.insert_or_assign(net::SchemefulSite(GURL("https://a.test")), |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPISet{ |
| privacy_sandbox::PrivacySandboxAttestationsGatedAPI:: |
| kProtectedAudience}); |
| privacy_sandbox::PrivacySandboxAttestations::GetInstance() |
| ->SetAttestationsForTesting(map); |
| |
| content::WebContentsConsoleObserver console_observer(web_contents()); |
| console_observer.SetPattern("Attestation check for Topics on * failed."); |
| |
| GURL main_frame_url = |
| https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); |
| |
| base::StringPairs replacement; |
| replacement.emplace_back(std::make_pair("{{STATUS}}", "200 OK")); |
| replacement.emplace_back(std::make_pair("{{OBSERVE_BROWSING_TOPICS_HEADER}}", |
| "Observe-Browsing-Topics: ?1")); |
| replacement.emplace_back(std::make_pair("{{REDIRECT_HEADER}}", "")); |
| |
| GURL resource_url = https_server_.GetURL( |
| "a.test", net::test_server::GetFilePathWithReplacements(GetRelativePath(), |
| replacement)); |
| |
| EXPECT_TRUE(ExecJsWithBrowsingTopicsTrue(resource_url)); |
| |
| std::optional<std::string> topics_header_value = |
| GetTopicsHeaderForRequestPath(GetRelativePath()); |
| |
| EXPECT_EQ(topics_header_value, kExpectedHeaderValueForEmptyTopics); |
| |
| // Because a.test is not attested for Topics, we should not have any new |
| // observations of API usage. |
| std::vector<ApiUsageContext> api_usage_contexts = |
| content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); |
| EXPECT_EQ(api_usage_contexts.size(), 1u); |
| |
| ASSERT_TRUE(console_observer.Wait()); |
| EXPECT_FALSE(console_observer.messages().empty()); |
| } |
| |
| } // namespace browsing_topics |