blob: a97492af4ba5a044c0b0bd181e03d13ca397fbe0 [file] [log] [blame]
// 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 "components/browsing_topics/browsing_topics_service_impl.h"
#include "base/files/scoped_temp_dir.h"
#include "base/json/json_file_value_serializer.h"
#include "base/json/values_util.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "components/browsing_topics/test_util.h"
#include "components/browsing_topics/util.h"
#include "components/content_settings/browser/page_specific_content_settings.h"
#include "components/content_settings/browser/test_page_specific_content_settings_delegate.h"
#include "components/content_settings/core/browser/cookie_settings.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/history/core/browser/history_database_params.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/test/test_history_database.h"
#include "components/optimization_guide/content/browser/page_content_annotations_service.h"
#include "components/optimization_guide/content/browser/test_page_content_annotations_service.h"
#include "components/optimization_guide/content/browser/test_page_content_annotator.h"
#include "components/optimization_guide/core/test_model_info_builder.h"
#include "components/privacy_sandbox/privacy_sandbox_prefs.h"
#include "components/privacy_sandbox/privacy_sandbox_settings_impl.h"
#include "components/privacy_sandbox/privacy_sandbox_test_util.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/browser/browsing_topics_site_data_manager.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/test/browsing_topics_test_util.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/web_contents_tester.h"
#include "content/test/test_render_view_host.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h"
namespace browsing_topics {
namespace {
// Tests can be slow if `TaskEnvironment::FastForwardBy()` is called with a long
// period of time. Thus, use `base::Seconds(1)` as the duration of a day in
// tests.
constexpr base::TimeDelta kOneTestDay = base::Seconds(1);
constexpr base::TimeDelta kEpoch = 7 * kOneTestDay;
constexpr base::TimeDelta kMaxEpochIntroductionDelay = 2 * kOneTestDay;
constexpr base::TimeDelta kCalculatorDelay = base::Milliseconds(1);
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 size_t kTaxonomySize = 349;
constexpr int kTaxonomyVersion = 1;
constexpr int64_t kModelVersion = 5000000000LL;
EpochTopics CreateTestEpochTopics(
const std::vector<std::pair<Topic, std::set<HashedDomain>>>& topics,
base::Time calculation_time,
size_t padded_top_topics_start_index = 5) {
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),
padded_top_topics_start_index, kTaxonomySize,
kTaxonomyVersion, kModelVersion, calculation_time);
}
} // namespace
// A tester class that allows mocking the topics calculators (i.e. the result
// and the finish delay).
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,
optimization_guide::PageContentAnnotationsService* annotations_service,
base::queue<EpochTopics> mock_calculator_results,
base::TimeDelta calculator_finish_delay)
: BrowsingTopicsServiceImpl(profile_path,
privacy_sandbox_settings,
history_service,
site_data_manager,
annotations_service),
mock_calculator_results_(std::move(mock_calculator_results)),
calculator_finish_delay_(calculator_finish_delay) {}
~TesterBrowsingTopicsService() override = default;
TesterBrowsingTopicsService(const TesterBrowsingTopicsService&) = delete;
TesterBrowsingTopicsService& operator=(const TesterBrowsingTopicsService&) =
delete;
TesterBrowsingTopicsService(TesterBrowsingTopicsService&&) = delete;
TesterBrowsingTopicsService& operator=(TesterBrowsingTopicsService&&) =
delete;
std::unique_ptr<BrowsingTopicsCalculator> CreateCalculator(
privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings,
history::HistoryService* history_service,
content::BrowsingTopicsSiteDataManager* site_data_manager,
optimization_guide::PageContentAnnotationsService* annotations_service,
const base::circular_deque<EpochTopics>& epochs,
BrowsingTopicsCalculator::CalculateCompletedCallback callback) override {
DCHECK(!mock_calculator_results_.empty());
++started_calculations_count_;
EpochTopics next_epoch = std::move(mock_calculator_results_.front());
mock_calculator_results_.pop();
return std::make_unique<TesterBrowsingTopicsCalculator>(
privacy_sandbox_settings, history_service, site_data_manager,
annotations_service, std::move(callback), std::move(next_epoch),
calculator_finish_delay_);
}
const BrowsingTopicsState& browsing_topics_state() override {
return BrowsingTopicsServiceImpl::browsing_topics_state();
}
void OnTopicsDataAccessibleSinceUpdated() override {
BrowsingTopicsServiceImpl::OnTopicsDataAccessibleSinceUpdated();
}
void OnURLsDeleted(history::HistoryService* history_service,
const history::DeletionInfo& deletion_info) override {
BrowsingTopicsServiceImpl::OnURLsDeleted(history_service, deletion_info);
}
// The number of calculations that have started, including those that have
// finished, those that are ongoing, and those that have been canceled.
size_t started_calculations_count() const {
return started_calculations_count_;
}
private:
base::queue<EpochTopics> mock_calculator_results_;
base::TimeDelta calculator_finish_delay_;
size_t started_calculations_count_ = 0u;
};
class BrowsingTopicsServiceImplTest
: public content::RenderViewHostTestHarness {
public:
BrowsingTopicsServiceImplTest()
: content::RenderViewHostTestHarness(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
scoped_feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{blink::features::kBrowsingTopics,
{{"time_period_per_epoch",
base::StrCat({base::NumberToString(kEpoch.InSeconds()), "s"})},
{"browsing_topics_max_epoch_introduction_delay",
base::StrCat(
{base::NumberToString(kMaxEpochIntroductionDelay.InSeconds()),
"s"})}}}},
/*disabled_features=*/{});
OverrideHmacKeyForTesting(kTestKey);
EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
content_settings::CookieSettings::RegisterProfilePrefs(prefs_.registry());
HostContentSettingsMap::RegisterProfilePrefs(prefs_.registry());
privacy_sandbox::RegisterProfilePrefs(prefs_.registry());
host_content_settings_map_ = base::MakeRefCounted<HostContentSettingsMap>(
&prefs_, /*is_off_the_record=*/false, /*store_last_modified=*/false,
/*restore_session=*/false, /*should_record_metrics=*/false);
cookie_settings_ = base::MakeRefCounted<content_settings::CookieSettings>(
host_content_settings_map_.get(), &prefs_, false, "chrome-extension");
auto privacy_sandbox_delegate = std::make_unique<
privacy_sandbox_test_util::MockPrivacySandboxSettingsDelegate>();
privacy_sandbox_delegate->SetUpIsPrivacySandboxRestrictedResponse(
/*restricted=*/false);
privacy_sandbox_delegate->SetUpIsIncognitoProfileResponse(
/*incognito=*/false);
privacy_sandbox_settings_ =
std::make_unique<privacy_sandbox::PrivacySandboxSettingsImpl>(
std::move(privacy_sandbox_delegate),
host_content_settings_map_.get(), cookie_settings_, &prefs_);
privacy_sandbox_settings_->SetAllPrivacySandboxAllowedForTesting();
history_service_ = std::make_unique<history::HistoryService>();
history_service_->Init(
history::TestHistoryDatabaseParamsForPath(temp_dir_.GetPath()));
page_content_annotations_service_ =
optimization_guide::TestPageContentAnnotationsService::Create(
/*optimization_guide_model_provider=*/nullptr,
history_service_.get());
page_content_annotations_service_->OverridePageContentAnnotatorForTesting(
&test_page_content_annotator_);
task_environment()->RunUntilIdle();
}
~BrowsingTopicsServiceImplTest() override = default;
void SetUp() override {
content::RenderViewHostTestHarness::SetUp();
content_settings::PageSpecificContentSettings::CreateForWebContents(
web_contents(),
std::make_unique<
content_settings::TestPageSpecificContentSettingsDelegate>(
&prefs_, host_content_settings_map_.get()));
}
void TearDown() override {
DCHECK(history_service_);
browsing_topics_service_.reset();
base::RunLoop run_loop;
history_service_->SetOnBackendDestroyTask(run_loop.QuitClosure());
history_service_->Shutdown();
run_loop.Run();
page_content_annotations_service_.reset();
task_environment()->RunUntilIdle();
host_content_settings_map_->ShutdownOnUIThread();
content::RenderViewHostTestHarness::TearDown();
}
void NavigateToPage(const GURL& url) {
auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
url, web_contents());
simulator->SetTransition(ui::PageTransition::PAGE_TRANSITION_TYPED);
simulator->Commit();
}
content::BrowsingTopicsSiteDataManager* topics_site_data_manager() {
return web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetStoragePartition()
->GetBrowsingTopicsSiteDataManager();
}
base::FilePath BrowsingTopicsStateFilePath() {
return temp_dir_.GetPath().Append(FILE_PATH_LITERAL("BrowsingTopicsState"));
}
void CreateBrowsingTopicsStateFile(
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(BrowsingTopicsStateFilePath()).Serialize(dict);
}
void InitializeBrowsingTopicsService(
base::queue<EpochTopics> mock_calculator_results) {
browsing_topics_service_ = std::make_unique<TesterBrowsingTopicsService>(
temp_dir_.GetPath(), privacy_sandbox_settings_.get(),
history_service_.get(), topics_site_data_manager(),
page_content_annotations_service_.get(),
std::move(mock_calculator_results), kCalculatorDelay);
}
const BrowsingTopicsState& browsing_topics_state() {
DCHECK(browsing_topics_service_);
return browsing_topics_service_->browsing_topics_state();
}
HashedDomain GetHashedDomain(const std::string& domain) {
return HashContextDomainForStorage(kTestKey, domain);
}
protected:
base::test::ScopedFeatureList scoped_feature_list_;
base::ScopedTempDir temp_dir_;
sync_preferences::TestingPrefServiceSyncable prefs_;
scoped_refptr<HostContentSettingsMap> host_content_settings_map_;
scoped_refptr<content_settings::CookieSettings> cookie_settings_;
std::unique_ptr<privacy_sandbox::PrivacySandboxSettings>
privacy_sandbox_settings_;
std::unique_ptr<history::HistoryService> history_service_;
std::unique_ptr<optimization_guide::PageContentAnnotationsService>
page_content_annotations_service_;
optimization_guide::TestPageContentAnnotator test_page_content_annotator_;
std::unique_ptr<TesterBrowsingTopicsService> browsing_topics_service_;
};
TEST_F(BrowsingTopicsServiceImplTest, EmptyInitialState_CalculationScheduling) {
base::Time start_time = base::Time::Now();
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
kTime1));
mock_calculator_results.push(CreateTestEpochTopics({{Topic(6), {}},
{Topic(7), {}},
{Topic(8), {}},
{Topic(9), {}},
{Topic(10), {}}},
kTime2));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 0u);
// Finish file loading.
task_environment()->RunUntilIdle();
EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 1u);
EXPECT_TRUE(browsing_topics_state().epochs().empty());
// Finish the calculation.
task_environment()->FastForwardBy(kCalculatorDelay);
EXPECT_EQ(browsing_topics_state().epochs().size(), 1u);
EXPECT_EQ(browsing_topics_state().epochs()[0].calculation_time(), kTime1);
EXPECT_EQ(browsing_topics_state().next_scheduled_calculation_time(),
start_time + kCalculatorDelay + kEpoch);
// Advance the time to right before the next scheduled calculation. The next
// calculation should not happen.
task_environment()->FastForwardBy(kEpoch - base::Microseconds(1));
EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 1u);
// Advance the time to the scheduled calculation time. A calculation should
// happen.
task_environment()->FastForwardBy(base::Microseconds(1));
EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 2u);
// Finish the calculation.
task_environment()->FastForwardBy(kCalculatorDelay);
EXPECT_EQ(browsing_topics_state().epochs().size(), 2u);
EXPECT_EQ(browsing_topics_state().epochs()[1].calculation_time(), kTime2);
EXPECT_EQ(browsing_topics_state().next_scheduled_calculation_time(),
start_time + 2 * kCalculatorDelay + 2 * kEpoch);
}
TEST_F(BrowsingTopicsServiceImplTest,
StartFromPreexistingState_CalculateAtScheduledTime) {
base::Time start_time = base::Time::Now();
std::vector<EpochTopics> preexisting_epochs;
preexisting_epochs.push_back(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
kTime1));
CreateBrowsingTopicsStateFile(
std::move(preexisting_epochs),
/*next_scheduled_calculation_time=*/start_time + kOneTestDay);
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(6), {}},
{Topic(7), {}},
{Topic(8), {}},
{Topic(9), {}},
{Topic(10), {}}},
kTime2));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish file loading.
task_environment()->RunUntilIdle();
EXPECT_EQ(browsing_topics_state().epochs().size(), 1u);
EXPECT_EQ(browsing_topics_state().epochs()[0].calculation_time(), kTime1);
EXPECT_EQ(browsing_topics_state().next_scheduled_calculation_time(),
start_time + kOneTestDay);
EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 0u);
// Advance the time to the scheduled calculation time. A calculation should
// happen.
task_environment()->FastForwardBy(kOneTestDay);
EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 1u);
// Finish the calculation.
task_environment()->FastForwardBy(kCalculatorDelay);
EXPECT_EQ(browsing_topics_state().epochs().size(), 2u);
EXPECT_EQ(browsing_topics_state().epochs()[1].calculation_time(), kTime2);
EXPECT_EQ(browsing_topics_state().next_scheduled_calculation_time(),
start_time + kOneTestDay + kCalculatorDelay + kEpoch);
}
TEST_F(
BrowsingTopicsServiceImplTest,
StartFromPreexistingState_ScheduledTimeReachedBeforeStartup_CalculateImmediately) {
base::Time start_time = base::Time::Now();
std::vector<EpochTopics> preexisting_epochs;
preexisting_epochs.push_back(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
kTime1));
CreateBrowsingTopicsStateFile(
std::move(preexisting_epochs),
/*next_scheduled_calculation_time=*/start_time - base::Microseconds(1));
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(6), {}},
{Topic(7), {}},
{Topic(8), {}},
{Topic(9), {}},
{Topic(10), {}}},
kTime2));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish file loading.
task_environment()->RunUntilIdle();
EXPECT_EQ(browsing_topics_state().epochs().size(), 1u);
EXPECT_EQ(browsing_topics_state().epochs()[0].calculation_time(), kTime1);
EXPECT_EQ(browsing_topics_state().next_scheduled_calculation_time(),
start_time - base::Microseconds(1));
EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 1u);
}
TEST_F(
BrowsingTopicsServiceImplTest,
StartFromPreexistingState_TopicsAccessibleSinceUpdated_ResetStateAndStorage_CalculateAtScheduledTime) {
base::Time start_time = base::Time::Now();
std::vector<EpochTopics> preexisting_epochs;
preexisting_epochs.push_back(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
start_time - kOneTestDay));
// Add some arbitrary data to site data storage. The intent is just to test
// data deletion.
topics_site_data_manager()->OnBrowsingTopicsApiUsed(
HashMainFrameHostForStorage("a.com"), {HashedDomain(1)},
base::Time::Now());
task_environment()->FastForwardBy(base::Microseconds(1));
privacy_sandbox_settings_->OnCookiesCleared();
EXPECT_EQ(
content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(),
1u);
CreateBrowsingTopicsStateFile(
std::move(preexisting_epochs),
/*next_scheduled_calculation_time=*/start_time + kOneTestDay);
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(6), {}},
{Topic(7), {}},
{Topic(8), {}},
{Topic(9), {}},
{Topic(10), {}}},
start_time - kOneTestDay));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish file loading.
task_environment()->RunUntilIdle();
EXPECT_EQ(browsing_topics_state().epochs().size(), 0u);
EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 0u);
EXPECT_EQ(
content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(),
0u);
}
TEST_F(
BrowsingTopicsServiceImplTest,
StartFromPreexistingState_UnexpectedNextCalculationDelay_ResetState_CalculateImmediately) {
base::Time start_time = base::Time::Now();
std::vector<EpochTopics> preexisting_epochs;
preexisting_epochs.push_back(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
start_time - kOneTestDay));
privacy_sandbox_settings_->OnCookiesCleared();
CreateBrowsingTopicsStateFile(
std::move(preexisting_epochs),
/*next_scheduled_calculation_time=*/start_time + 15 * kOneTestDay);
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(6), {}},
{Topic(7), {}},
{Topic(8), {}},
{Topic(9), {}},
{Topic(10), {}}},
kTime2));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish file loading.
task_environment()->RunUntilIdle();
EXPECT_EQ(browsing_topics_state().epochs().size(), 0u);
EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 1u);
}
TEST_F(BrowsingTopicsServiceImplTest,
StartFromPreexistingState_DefaultHandlingBeforeLoadFinish) {
ukm::TestAutoSetUkmRecorder ukm_recorder;
base::Time start_time = base::Time::Now();
std::vector<EpochTopics> preexisting_epochs;
preexisting_epochs.push_back(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1));
CreateBrowsingTopicsStateFile(
std::move(preexisting_epochs),
/*next_scheduled_calculation_time=*/start_time + kOneTestDay);
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(6), {}},
{Topic(7), {}},
{Topic(8), {}},
{Topic(9), {}},
{Topic(10), {}}},
kTime2));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
NavigateToPage(GURL("https://www.foo.com"));
{
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_FALSE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
EXPECT_TRUE(result.empty());
}
std::vector<ApiResultUkmMetrics> metrics_entries =
ReadApiResultUkmMetrics(ukm_recorder);
EXPECT_EQ(1u, metrics_entries.size());
EXPECT_EQ(metrics_entries[0].failure_reason,
ApiAccessFailureReason::kStateNotReady);
EXPECT_FALSE(metrics_entries[0].topic0.IsValid());
EXPECT_FALSE(metrics_entries[0].topic1.IsValid());
EXPECT_FALSE(metrics_entries[0].topic2.IsValid());
EXPECT_TRUE(browsing_topics_service_->GetTopTopicsForDisplay().empty());
base::test::TestFuture<mojom::WebUIGetBrowsingTopicsStateResultPtr> future1;
browsing_topics_service_->GetBrowsingTopicsStateForWebUi(
/*calculate_now=*/false, future1.GetCallback());
EXPECT_TRUE(future1.IsReady());
EXPECT_EQ(future1.Take()->get_override_status_message(),
"State loading hasn't finished. Please retry shortly.");
// Finish file loading.
task_environment()->RunUntilIdle();
{
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
EXPECT_FALSE(result.empty());
}
EXPECT_FALSE(browsing_topics_service_->GetTopTopicsForDisplay().empty());
base::test::TestFuture<mojom::WebUIGetBrowsingTopicsStateResultPtr> future2;
browsing_topics_service_->GetBrowsingTopicsStateForWebUi(
/*calculate_now=*/false, future2.GetCallback());
EXPECT_TRUE(future2.IsReady());
EXPECT_FALSE(future2.Take()->is_override_status_message());
}
TEST_F(
BrowsingTopicsServiceImplTest,
OnTopicsDataAccessibleSinceUpdated_ResetState_ClearTopicsSiteDataStorage) {
base::Time start_time = base::Time::Now();
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
start_time));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {}},
{Topic(7), {}},
{Topic(8), {}},
{Topic(9), {}},
{Topic(10), {}}},
start_time + kCalculatorDelay + kEpoch));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish file loading and two calculations.
task_environment()->FastForwardBy(2 * kCalculatorDelay + kEpoch);
// Add some arbitrary data to site data storage. The intent is just to test
// data deletion.
topics_site_data_manager()->OnBrowsingTopicsApiUsed(
HashMainFrameHostForStorage("a.com"), {HashedDomain(1)},
base::Time::Now());
EXPECT_EQ(browsing_topics_state().epochs().size(), 2u);
EXPECT_EQ(
content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(),
1u);
task_environment()->FastForwardBy(base::Microseconds(1));
privacy_sandbox_settings_->OnCookiesCleared();
EXPECT_EQ(browsing_topics_state().epochs().size(), 0u);
EXPECT_EQ(
content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(),
0u);
}
TEST_F(BrowsingTopicsServiceImplTest,
OnURLsDeleted_TimeRangeOverlapWithOneEpoch) {
base::Time start_time = base::Time::Now();
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
start_time));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {}},
{Topic(7), {}},
{Topic(8), {}},
{Topic(9), {}},
{Topic(10), {}}},
start_time + kCalculatorDelay + kEpoch));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish file loading and two calculations.
task_environment()->FastForwardBy(2 * kCalculatorDelay + kEpoch);
EXPECT_EQ(browsing_topics_state().epochs().size(), 2u);
EXPECT_FALSE(browsing_topics_state().epochs()[0].empty());
EXPECT_FALSE(browsing_topics_state().epochs()[1].empty());
history::DeletionInfo deletion_info(
history::DeletionTimeRange(start_time + 5 * kOneTestDay,
start_time + 6 * kOneTestDay),
/*is_from_expiration=*/false, /*deleted_rows=*/{}, /*favicon_urls=*/{},
/*restrict_urls=*/absl::nullopt);
browsing_topics_service_->OnURLsDeleted(history_service_.get(),
deletion_info);
EXPECT_EQ(browsing_topics_state().epochs().size(), 2u);
EXPECT_FALSE(browsing_topics_state().epochs()[0].empty());
EXPECT_TRUE(browsing_topics_state().epochs()[1].empty());
}
TEST_F(BrowsingTopicsServiceImplTest,
OnURLsDeleted_TimeRangeOverlapWithAllEpochs) {
base::Time start_time = base::Time::Now();
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
start_time));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {}},
{Topic(7), {}},
{Topic(8), {}},
{Topic(9), {}},
{Topic(10), {}}},
start_time + kCalculatorDelay + kEpoch));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish file loading and two calculations.
task_environment()->FastForwardBy(2 * kCalculatorDelay + kEpoch);
EXPECT_EQ(browsing_topics_state().epochs().size(), 2u);
EXPECT_FALSE(browsing_topics_state().epochs()[0].empty());
EXPECT_FALSE(browsing_topics_state().epochs()[1].empty());
history::DeletionInfo deletion_info(
history::DeletionTimeRange(start_time, start_time + 2 * kOneTestDay),
/*is_from_expiration=*/false, /*deleted_rows=*/{}, /*favicon_urls=*/{},
/*restrict_urls=*/absl::nullopt);
browsing_topics_service_->OnURLsDeleted(history_service_.get(),
deletion_info);
EXPECT_EQ(browsing_topics_state().epochs().size(), 2u);
EXPECT_TRUE(browsing_topics_state().epochs()[0].empty());
EXPECT_TRUE(browsing_topics_state().epochs()[1].empty());
}
TEST_F(BrowsingTopicsServiceImplTest, Recalculate) {
base::Time start_time = base::Time::Now();
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
kTime1));
mock_calculator_results.push(CreateTestEpochTopics({{Topic(6), {}},
{Topic(7), {}},
{Topic(8), {}},
{Topic(9), {}},
{Topic(10), {}}},
kTime2));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
task_environment()->FastForwardBy(kCalculatorDelay - base::Microseconds(1));
EXPECT_EQ(browsing_topics_state().epochs().size(), 0u);
EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 1u);
// History deletion during a calculation should trigger the re-calculation.
history::DeletionInfo deletion_info(
history::DeletionTimeRange(start_time, start_time + 2 * kOneTestDay),
/*is_from_expiration=*/false, /*deleted_rows=*/{}, /*favicon_urls=*/{},
/*restrict_urls=*/absl::nullopt);
browsing_topics_service_->OnURLsDeleted(history_service_.get(),
deletion_info);
// The calculation shouldn't finish at the originally expected time, as it was
// dropped and a new calculation has started.
task_environment()->FastForwardBy(base::Microseconds(1));
EXPECT_EQ(browsing_topics_state().epochs().size(), 0u);
EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 2u);
// Finish the re-started calculation.
task_environment()->FastForwardBy(kCalculatorDelay - base::Microseconds(1));
EXPECT_EQ(browsing_topics_state().epochs().size(), 1u);
// Expect that the result comes from the re-started calculator.
EXPECT_EQ(browsing_topics_state().epochs()[0].calculation_time(), kTime2);
EXPECT_EQ(browsing_topics_state().next_scheduled_calculation_time(),
base::Time::Now() + kEpoch);
}
TEST_F(BrowsingTopicsServiceImplTest,
HandleTopicsWebApi_PrivacySandboxSettingsDisabled) {
ukm::TestAutoSetUkmRecorder ukm_recorder;
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
task_environment()->RunUntilIdle();
privacy_sandbox_settings_->SetTopicsBlockedForTesting();
NavigateToPage(GURL("https://www.foo.com"));
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_FALSE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
EXPECT_TRUE(result.empty());
std::vector<ApiResultUkmMetrics> metrics_entries =
ReadApiResultUkmMetrics(ukm_recorder);
EXPECT_EQ(1u, metrics_entries.size());
EXPECT_EQ(metrics_entries[0].failure_reason,
ApiAccessFailureReason::kAccessDisallowedBySettings);
EXPECT_FALSE(metrics_entries[0].topic0.IsValid());
EXPECT_FALSE(metrics_entries[0].topic1.IsValid());
EXPECT_FALSE(metrics_entries[0].topic2.IsValid());
}
TEST_F(BrowsingTopicsServiceImplTest, HandleTopicsWebApi_OneEpoch) {
ukm::TestAutoSetUkmRecorder ukm_recorder;
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
task_environment()->FastForwardBy(kCalculatorDelay);
NavigateToPage(GURL("https://www.foo.com"));
{
// Current time is before the epoch switch time.
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
EXPECT_TRUE(result.empty());
}
std::vector<ApiResultUkmMetrics> metrics_entries =
ReadApiResultUkmMetrics(ukm_recorder);
EXPECT_EQ(1u, metrics_entries.size());
// This is an empty event with no metrics.
EXPECT_FALSE(metrics_entries[0].failure_reason);
EXPECT_FALSE(metrics_entries[0].topic0.IsValid());
EXPECT_FALSE(metrics_entries[0].topic1.IsValid());
EXPECT_FALSE(metrics_entries[0].topic2.IsValid());
// Advance to the time after the epoch switch time.
task_environment()->AdvanceClock(kMaxEpochIntroductionDelay);
{
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
EXPECT_EQ(result.size(), 1u);
EXPECT_EQ(result[0]->topic, 2);
EXPECT_EQ(result[0]->config_version, "chrome.1");
EXPECT_EQ(result[0]->taxonomy_version, "1");
EXPECT_EQ(result[0]->model_version, "5000000000");
EXPECT_EQ(result[0]->version, "chrome.1:1:5000000000");
}
}
TEST_F(BrowsingTopicsServiceImplTest, HandleTopicsWebApi_OneEpoch_Filtered) {
ukm::TestAutoSetUkmRecorder ukm_recorder;
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
kTime1));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
task_environment()->FastForwardBy(kCalculatorDelay);
NavigateToPage(GURL("https://www.foo.com"));
// Advance to the time after the epoch switch time.
task_environment()->AdvanceClock(kMaxEpochIntroductionDelay);
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
EXPECT_TRUE(result.empty());
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_EQ(metrics_entries[0].topic0.topic(), Topic(2));
EXPECT_TRUE(metrics_entries[0].topic0.is_true_topic());
EXPECT_TRUE(metrics_entries[0].topic0.should_be_filtered());
EXPECT_EQ(metrics_entries[0].topic0.taxonomy_version(), 1);
EXPECT_EQ(metrics_entries[0].topic0.model_version(), 5000000000LL);
EXPECT_FALSE(metrics_entries[0].topic1.IsValid());
EXPECT_FALSE(metrics_entries[0].topic2.IsValid());
}
TEST_F(BrowsingTopicsServiceImplTest,
HandleTopicsWebApi_TopicNotAllowedByPrivacySandboxSettings) {
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
task_environment()->FastForwardBy(kCalculatorDelay);
// This domain would let a random topic (Topic(130)) be returned.
NavigateToPage(GURL("https://www.foo81.com"));
// Current time is before the epoch switch time.
{
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
EXPECT_TRUE(result.empty());
}
// Advance to the time after the epoch switch time.
task_environment()->AdvanceClock(kMaxEpochIntroductionDelay);
{
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
// Some topics are returned.
EXPECT_FALSE(result.empty());
}
privacy_sandbox_settings_->SetTopicAllowed(
privacy_sandbox::CanonicalTopic(Topic(130), /*taxonomy_version=*/1),
false);
{
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
// The topic was blocked by the settings.
EXPECT_TRUE(result.empty());
}
}
TEST_F(BrowsingTopicsServiceImplTest, HandleTopicsWebApi_FourEpochs) {
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}},
{Topic(7), {GetHashedDomain("bar.com")}},
{Topic(8), {GetHashedDomain("bar.com")}},
{Topic(9), {GetHashedDomain("bar.com")}},
{Topic(10), {GetHashedDomain("bar.com")}}},
kTime1));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(11), {GetHashedDomain("bar.com")}},
{Topic(12), {GetHashedDomain("bar.com")}},
{Topic(13), {GetHashedDomain("bar.com")}},
{Topic(14), {GetHashedDomain("bar.com")}},
{Topic(15), {GetHashedDomain("bar.com")}}},
kTime1));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(16), {GetHashedDomain("bar.com")}},
{Topic(17), {GetHashedDomain("bar.com")}},
{Topic(18), {GetHashedDomain("bar.com")}},
{Topic(19), {GetHashedDomain("bar.com")}},
{Topic(20), {GetHashedDomain("bar.com")}}},
kTime1));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish all calculations.
task_environment()->FastForwardBy(4 * kCalculatorDelay + 3 * kEpoch);
EXPECT_EQ(browsing_topics_state().epochs().size(), 4u);
NavigateToPage(GURL("https://www.foo.com"));
// Current time is before the epoch switch time.
{
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
EXPECT_EQ(result.size(), 3u);
std::set<int> result_set;
result_set.insert(result[0]->topic);
result_set.insert(result[1]->topic);
result_set.insert(result[2]->topic);
EXPECT_EQ(result_set, std::set<int>({2, 7, 12}));
}
// Advance to the time after the epoch switch time.
task_environment()->AdvanceClock(kMaxEpochIntroductionDelay);
{
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
EXPECT_EQ(result.size(), 3u);
std::set<int> result_set;
result_set.insert(result[0]->topic);
result_set.insert(result[1]->topic);
result_set.insert(result[2]->topic);
EXPECT_EQ(result_set, std::set<int>({7, 12, 17}));
}
}
TEST_F(BrowsingTopicsServiceImplTest,
HandleTopicsWebApi_DuplicateTopicsRemoved) {
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}},
{Topic(7), {GetHashedDomain("bar.com")}},
{Topic(8), {GetHashedDomain("bar.com")}},
{Topic(9), {GetHashedDomain("bar.com")}},
{Topic(10), {GetHashedDomain("bar.com")}}},
kTime1));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}},
{Topic(7), {GetHashedDomain("bar.com")}},
{Topic(8), {GetHashedDomain("bar.com")}},
{Topic(9), {GetHashedDomain("bar.com")}},
{Topic(10), {GetHashedDomain("bar.com")}}},
kTime1));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish all calculations.
task_environment()->FastForwardBy(4 * kCalculatorDelay + 3 * kEpoch);
EXPECT_EQ(browsing_topics_state().epochs().size(), 4u);
NavigateToPage(GURL("https://www.foo.com"));
// Current time is before the epoch switch time.
{
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
EXPECT_EQ(result.size(), 2u);
std::set<int> result_set;
result_set.insert(result[0]->topic);
result_set.insert(result[1]->topic);
EXPECT_EQ(result_set, std::set<int>({2, 7}));
}
// Advance to the time after the epoch switch time.
task_environment()->AdvanceClock(kMaxEpochIntroductionDelay);
{
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
EXPECT_EQ(result.size(), 2u);
std::set<int> result_set;
result_set.insert(result[0]->topic);
result_set.insert(result[1]->topic);
EXPECT_EQ(result_set, std::set<int>({2, 7}));
}
// 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();
EXPECT_EQ(2u, topics.size());
// PSCS::GetAccessedTopics() will return sorted values.
EXPECT_EQ(topics[0].topic_id(), Topic(2));
EXPECT_EQ(topics[1].topic_id(), Topic(7));
}
TEST_F(BrowsingTopicsServiceImplTest,
HandleTopicsWebApi_TopicsReturnedInSortedOrder) {
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}},
{Topic(7), {GetHashedDomain("bar.com")}},
{Topic(8), {GetHashedDomain("bar.com")}},
{Topic(9), {GetHashedDomain("bar.com")}},
{Topic(10), {GetHashedDomain("bar.com")}}},
kTime1));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}},
{Topic(7), {GetHashedDomain("bar.com")}},
{Topic(8), {GetHashedDomain("bar.com")}},
{Topic(9), {GetHashedDomain("bar.com")}},
{Topic(10), {GetHashedDomain("bar.com")}}},
kTime1));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish all calculations.
task_environment()->FastForwardBy(4 * kCalculatorDelay + 3 * kEpoch);
EXPECT_EQ(browsing_topics_state().epochs().size(), 4u);
NavigateToPage(GURL("https://www.foo.com"));
// Current time is before the epoch switch time.
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
EXPECT_EQ(result.size(), 2u);
EXPECT_EQ(result[0]->topic, 2);
EXPECT_EQ(result[1]->topic, 7);
}
TEST_F(BrowsingTopicsServiceImplTest, HandleTopicsWebApi_TrackedUsageContext) {
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Advance to the time after the epoch switch time.
task_environment()->FastForwardBy(kCalculatorDelay +
kMaxEpochIntroductionDelay);
EXPECT_EQ(
content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(),
0u);
NavigateToPage(GURL("https://www.foo.com"));
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
EXPECT_EQ(result.size(), 1u);
std::vector<ApiUsageContext> api_usage_contexts =
content::GetBrowsingTopicsApiUsage(topics_site_data_manager());
EXPECT_EQ(api_usage_contexts.size(), 1u);
EXPECT_EQ(api_usage_contexts[0].hashed_main_frame_host,
HashMainFrameHostForStorage("www.foo.com"));
EXPECT_EQ(api_usage_contexts[0].hashed_context_domain,
GetHashedDomain("bar.com"));
}
TEST_F(BrowsingTopicsServiceImplTest, HandleTopicsWebApi_DoesNotObserve) {
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Advance to the time after the epoch switch time.
task_environment()->FastForwardBy(kCalculatorDelay +
kMaxEpochIntroductionDelay);
EXPECT_EQ(
content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(),
0u);
NavigateToPage(GURL("https://www.foo.com"));
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/false, result));
EXPECT_EQ(result.size(), 1u);
std::vector<ApiUsageContext> api_usage_contexts =
content::GetBrowsingTopicsApiUsage(topics_site_data_manager());
EXPECT_TRUE(api_usage_contexts.empty());
}
TEST_F(BrowsingTopicsServiceImplTest, HandleTopicsWebApi_DoesNotGetTopics) {
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Advance to the time after the epoch switch time.
task_environment()->FastForwardBy(kCalculatorDelay +
kMaxEpochIntroductionDelay);
EXPECT_EQ(
content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(),
0u);
NavigateToPage(GURL("https://www.foo.com"));
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/false,
/*observe=*/true, result));
EXPECT_TRUE(result.empty());
std::vector<ApiUsageContext> api_usage_contexts =
content::GetBrowsingTopicsApiUsage(topics_site_data_manager());
EXPECT_EQ(api_usage_contexts.size(), 1u);
EXPECT_EQ(api_usage_contexts[0].hashed_main_frame_host,
HashMainFrameHostForStorage("www.foo.com"));
EXPECT_EQ(api_usage_contexts[0].hashed_context_domain,
GetHashedDomain("bar.com"));
}
TEST_F(
BrowsingTopicsServiceImplTest,
HandleTopicsWebApi_DoesNotGetTopics_SettingsDisabled_NoApiResultUkmEvent) {
ukm::TestAutoSetUkmRecorder ukm_recorder;
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Advance to the time after the epoch switch time.
task_environment()->FastForwardBy(kCalculatorDelay +
kMaxEpochIntroductionDelay);
EXPECT_EQ(
content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(),
0u);
NavigateToPage(GURL("https://www.foo.com"));
privacy_sandbox_settings_->SetTopicsBlockedForTesting();
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_FALSE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/false,
/*observe=*/true, result));
EXPECT_TRUE(result.empty());
std::vector<ApiResultUkmMetrics> metrics_entries =
ReadApiResultUkmMetrics(ukm_recorder);
EXPECT_TRUE(metrics_entries.empty());
}
TEST_F(BrowsingTopicsServiceImplTest, ApiResultUkm_ZeroAndOneTopic) {
ukm::TestAutoSetUkmRecorder ukm_recorder;
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish all calculations.
task_environment()->FastForwardBy(kCalculatorDelay);
EXPECT_EQ(browsing_topics_state().epochs().size(), 1u);
NavigateToPage(GURL("https://www.foo.com"));
std::vector<ApiResultUkmMetrics> metrics_entries =
ReadApiResultUkmMetrics(ukm_recorder);
EXPECT_EQ(0u, metrics_entries.size());
// Current time is before the epoch switch time. Expect one ukm event without
// any metrics.
{
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
}
metrics_entries = ReadApiResultUkmMetrics(ukm_recorder);
EXPECT_EQ(1u, metrics_entries.size());
EXPECT_FALSE(metrics_entries[0].failure_reason);
EXPECT_FALSE(metrics_entries[0].topic0.IsValid());
EXPECT_FALSE(metrics_entries[0].topic1.IsValid());
EXPECT_FALSE(metrics_entries[0].topic2.IsValid());
// Advance to the time after the epoch switch time.
task_environment()->AdvanceClock(kMaxEpochIntroductionDelay);
{
std::vector<blink::mojom::EpochTopicPtr> result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, result));
}
metrics_entries = ReadApiResultUkmMetrics(ukm_recorder);
EXPECT_EQ(2u, metrics_entries.size());
EXPECT_FALSE(metrics_entries[1].failure_reason);
EXPECT_TRUE(metrics_entries[1].topic0.IsValid());
EXPECT_EQ(metrics_entries[1].topic0.topic(), Topic(2));
EXPECT_TRUE(metrics_entries[1].topic0.is_true_topic());
EXPECT_FALSE(metrics_entries[1].topic0.should_be_filtered());
EXPECT_EQ(metrics_entries[1].topic0.taxonomy_version(), 1);
EXPECT_EQ(metrics_entries[1].topic0.model_version(), 5000000000LL);
EXPECT_FALSE(metrics_entries[1].topic1.IsValid());
EXPECT_FALSE(metrics_entries[1].topic2.IsValid());
}
TEST_F(BrowsingTopicsServiceImplTest, ApiResultUkm_3Topics) {
ukm::TestAutoSetUkmRecorder ukm_recorder;
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1,
/*padded_top_topics_start_index=*/0));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}},
{Topic(7), {GetHashedDomain("bar.com")}},
{Topic(8), {GetHashedDomain("bar.com")}},
{Topic(9), {GetHashedDomain("bar.com")}},
{Topic(10), {GetHashedDomain("bar.com")}}},
kTime1, /*padded_top_topics_start_index=*/0));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1, /*padded_top_topics_start_index=*/0));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}},
{Topic(7), {GetHashedDomain("bar.com")}},
{Topic(8), {GetHashedDomain("bar.com")}},
{Topic(9), {GetHashedDomain("bar.com")}},
{Topic(10), {GetHashedDomain("bar.com")}}},
kTime1));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish all calculations.
task_environment()->FastForwardBy(4 * kCalculatorDelay + 3 * kEpoch);
EXPECT_EQ(browsing_topics_state().epochs().size(), 4u);
NavigateToPage(GURL("https://www.foo.com"));
// Advance to the time after the epoch switch time.
task_environment()->AdvanceClock(kMaxEpochIntroductionDelay);
std::vector<blink::mojom::EpochTopicPtr> api_call_result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, api_call_result));
// The API returns 2 topics, but all 3 candidate topics are logged.
EXPECT_EQ(2u, api_call_result.size());
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_EQ(metrics_entries[0].topic0.topic(), Topic(7));
EXPECT_FALSE(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(), 5000000000LL);
EXPECT_TRUE(metrics_entries[0].topic1.IsValid());
EXPECT_EQ(metrics_entries[0].topic1.topic(), Topic(2));
EXPECT_FALSE(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(), 5000000000LL);
EXPECT_TRUE(metrics_entries[0].topic2.IsValid());
EXPECT_EQ(metrics_entries[0].topic2.topic(), Topic(7));
EXPECT_TRUE(metrics_entries[0].topic2.is_true_topic());
EXPECT_FALSE(metrics_entries[0].topic2.should_be_filtered());
EXPECT_EQ(metrics_entries[0].topic2.taxonomy_version(), 1);
EXPECT_EQ(metrics_entries[0].topic2.model_version(), 5000000000LL);
}
TEST_F(BrowsingTopicsServiceImplTest, GetTopTopicsForDisplay) {
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1,
/*padded_top_topics_start_index=*/1));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}},
{Topic(7), {GetHashedDomain("bar.com")}},
{Topic(8), {GetHashedDomain("bar.com")}},
{Topic(9), {GetHashedDomain("bar.com")}},
{Topic(10), {GetHashedDomain("bar.com")}}},
kTime1));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}},
{Topic(2), {GetHashedDomain("bar.com")}},
{Topic(3), {GetHashedDomain("bar.com")}},
{Topic(4), {GetHashedDomain("bar.com")}},
{Topic(5), {GetHashedDomain("bar.com")}}},
kTime1,
/*padded_top_topics_start_index=*/2));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}},
{Topic(7), {GetHashedDomain("bar.com")}},
{Topic(8), {GetHashedDomain("bar.com")}},
{Topic(9), {GetHashedDomain("bar.com")}},
{Topic(10), {GetHashedDomain("bar.com")}}},
kTime1));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish all calculations.
task_environment()->FastForwardBy(4 * kCalculatorDelay + 3 * kEpoch);
EXPECT_EQ(browsing_topics_state().epochs().size(), 4u);
NavigateToPage(GURL("https://www.foo.com"));
// Current time is before the epoch switch time.
std::vector<privacy_sandbox::CanonicalTopic> result =
browsing_topics_service_->GetTopTopicsForDisplay();
EXPECT_EQ(result.size(), 13u);
EXPECT_EQ(result[0].topic_id(), Topic(1));
EXPECT_EQ(result[1].topic_id(), Topic(6));
EXPECT_EQ(result[2].topic_id(), Topic(7));
EXPECT_EQ(result[3].topic_id(), Topic(8));
EXPECT_EQ(result[4].topic_id(), Topic(9));
EXPECT_EQ(result[5].topic_id(), Topic(10));
EXPECT_EQ(result[6].topic_id(), Topic(1));
EXPECT_EQ(result[7].topic_id(), Topic(2));
EXPECT_EQ(result[8].topic_id(), Topic(6));
EXPECT_EQ(result[9].topic_id(), Topic(7));
EXPECT_EQ(result[10].topic_id(), Topic(8));
EXPECT_EQ(result[11].topic_id(), Topic(9));
EXPECT_EQ(result[12].topic_id(), Topic(10));
}
TEST_F(BrowsingTopicsServiceImplTest,
GetBrowsingTopicsStateForWebUi_CalculationInProgress) {
base::Time start_time = base::Time::Now();
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
start_time));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
task_environment()->RunUntilIdle();
base::test::TestFuture<mojom::WebUIGetBrowsingTopicsStateResultPtr> future1;
base::test::TestFuture<mojom::WebUIGetBrowsingTopicsStateResultPtr> future2;
browsing_topics_service_->GetBrowsingTopicsStateForWebUi(
/*calculate_now=*/false, future1.GetCallback());
browsing_topics_service_->GetBrowsingTopicsStateForWebUi(
/*calculate_now=*/true, future2.GetCallback());
EXPECT_FALSE(future1.IsReady());
EXPECT_FALSE(future2.IsReady());
task_environment()->FastForwardBy(kCalculatorDelay);
// The callbacks are invoked after the calculation has finished.
EXPECT_TRUE(future1.IsReady());
EXPECT_TRUE(future2.IsReady());
mojom::WebUIGetBrowsingTopicsStateResultPtr result1 = future1.Take();
mojom::WebUIGetBrowsingTopicsStateResultPtr result2 = future2.Take();
EXPECT_EQ(result1, result2);
mojom::WebUIBrowsingTopicsStatePtr& webui_state1 =
result1->get_browsing_topics_state();
EXPECT_EQ(webui_state1->epochs.size(), 1u);
EXPECT_EQ(webui_state1->next_scheduled_calculation_time,
start_time + kCalculatorDelay + kEpoch);
}
TEST_F(BrowsingTopicsServiceImplTest,
GetBrowsingTopicsStateForWebUi_CalculationNow) {
base::Time start_time = base::Time::Now();
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
start_time));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
start_time + kCalculatorDelay + kOneTestDay));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
task_environment()->FastForwardBy(kCalculatorDelay);
EXPECT_EQ(browsing_topics_state().epochs().size(), 1u);
EXPECT_EQ(browsing_topics_state().next_scheduled_calculation_time(),
start_time + kCalculatorDelay + kEpoch);
// Advance by some time smaller than the periodic update interval.
task_environment()->FastForwardBy(kOneTestDay);
base::test::TestFuture<mojom::WebUIGetBrowsingTopicsStateResultPtr> future;
browsing_topics_service_->GetBrowsingTopicsStateForWebUi(
/*calculate_now=*/true, future.GetCallback());
EXPECT_FALSE(future.IsReady());
task_environment()->FastForwardBy(kCalculatorDelay);
EXPECT_TRUE(future.IsReady());
mojom::WebUIGetBrowsingTopicsStateResultPtr result = future.Take();
mojom::WebUIBrowsingTopicsStatePtr& webui_state =
result->get_browsing_topics_state();
EXPECT_EQ(webui_state->epochs.size(), 2u);
// The `next_scheduled_calculation_time` is reset to an epoch after.
EXPECT_EQ(webui_state->next_scheduled_calculation_time,
start_time + 2 * kCalculatorDelay + kOneTestDay + kEpoch);
}
TEST_F(BrowsingTopicsServiceImplTest, GetBrowsingTopicsStateForWebUi) {
base::Time start_time = base::Time::Now();
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {HashedDomain(123), HashedDomain(456)}},
{Topic(2), {}},
{Topic(0), {}}, // blocked
{Topic(4), {}},
{Topic(5), {}}},
start_time));
// Failed calculation.
mock_calculator_results.push(EpochTopics(start_time + kEpoch));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {}},
{Topic(7), {}},
{Topic(8), {}},
{Topic(9), {}},
{Topic(10), {}}},
start_time + 2 * kEpoch,
/*padded_top_topics_start_index=*/2));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish file loading and three calculations.
task_environment()->FastForwardBy(3 * kCalculatorDelay + 2 * kEpoch);
base::test::TestFuture<mojom::WebUIGetBrowsingTopicsStateResultPtr> future;
browsing_topics_service_->GetBrowsingTopicsStateForWebUi(
/*calculate_now=*/false, future.GetCallback());
EXPECT_TRUE(future.IsReady());
mojom::WebUIGetBrowsingTopicsStateResultPtr result = future.Take();
mojom::WebUIBrowsingTopicsStatePtr& webui_state =
result->get_browsing_topics_state();
EXPECT_EQ(webui_state->epochs.size(), 3u);
EXPECT_EQ(webui_state->next_scheduled_calculation_time,
start_time + 3 * kCalculatorDelay + 3 * kEpoch);
const mojom::WebUIEpochPtr& epoch0 = webui_state->epochs[0];
const mojom::WebUIEpochPtr& epoch1 = webui_state->epochs[1];
const mojom::WebUIEpochPtr& epoch2 = webui_state->epochs[2];
EXPECT_EQ(epoch0->calculation_time, start_time + 2 * kEpoch);
EXPECT_EQ(epoch0->model_version, "5000000000");
EXPECT_EQ(epoch0->taxonomy_version, "1");
EXPECT_EQ(epoch0->topics.size(), 5u);
EXPECT_EQ(epoch0->topics[0]->topic_id, 6);
EXPECT_EQ(epoch0->topics[0]->topic_name, u"Entertainment industry");
EXPECT_TRUE(epoch0->topics[0]->is_real_topic);
EXPECT_TRUE(epoch0->topics[0]->observed_by_domains.empty());
EXPECT_EQ(epoch0->topics[1]->topic_id, 7);
EXPECT_EQ(epoch0->topics[1]->topic_name, u"Humor");
EXPECT_TRUE(epoch0->topics[1]->is_real_topic);
EXPECT_TRUE(epoch0->topics[1]->observed_by_domains.empty());
EXPECT_EQ(epoch0->topics[2]->topic_id, 8);
EXPECT_EQ(epoch0->topics[2]->topic_name, u"Live comedy");
EXPECT_FALSE(epoch0->topics[2]->is_real_topic);
EXPECT_TRUE(epoch0->topics[2]->observed_by_domains.empty());
EXPECT_EQ(epoch0->topics[3]->topic_id, 9);
EXPECT_EQ(epoch0->topics[3]->topic_name, u"Live sporting events");
EXPECT_FALSE(epoch0->topics[3]->is_real_topic);
EXPECT_TRUE(epoch0->topics[3]->observed_by_domains.empty());
EXPECT_EQ(epoch0->topics[4]->topic_id, 10);
EXPECT_EQ(epoch0->topics[4]->topic_name, u"Magic");
EXPECT_FALSE(epoch0->topics[4]->is_real_topic);
EXPECT_TRUE(epoch0->topics[4]->observed_by_domains.empty());
EXPECT_EQ(epoch1->calculation_time, start_time + kEpoch);
EXPECT_EQ(epoch1->model_version, "0");
EXPECT_EQ(epoch1->taxonomy_version, "0");
EXPECT_EQ(epoch1->topics.size(), 0u);
EXPECT_EQ(epoch2->calculation_time, start_time);
EXPECT_EQ(epoch2->model_version, "5000000000");
EXPECT_EQ(epoch2->taxonomy_version, "1");
EXPECT_EQ(epoch2->topics.size(), 5u);
EXPECT_EQ(epoch2->topics[0]->topic_id, 1);
EXPECT_EQ(epoch2->topics[0]->topic_name, u"Arts & entertainment");
EXPECT_TRUE(epoch2->topics[0]->is_real_topic);
EXPECT_EQ(epoch2->topics[0]->observed_by_domains.size(), 2u);
EXPECT_EQ(epoch2->topics[0]->observed_by_domains[0], "123");
EXPECT_EQ(epoch2->topics[0]->observed_by_domains[1], "456");
EXPECT_EQ(epoch2->topics[1]->topic_id, 2);
EXPECT_EQ(epoch2->topics[1]->topic_name, u"Acting & theater");
EXPECT_TRUE(epoch2->topics[1]->is_real_topic);
EXPECT_TRUE(epoch2->topics[1]->observed_by_domains.empty());
EXPECT_EQ(epoch2->topics[2]->topic_id, 0);
EXPECT_EQ(epoch2->topics[2]->topic_name, u"Unknown");
EXPECT_TRUE(epoch2->topics[2]->is_real_topic);
EXPECT_TRUE(epoch2->topics[2]->observed_by_domains.empty());
EXPECT_EQ(epoch2->topics[3]->topic_id, 4);
EXPECT_EQ(epoch2->topics[3]->topic_name, u"Concerts & music festivals");
EXPECT_TRUE(epoch2->topics[3]->is_real_topic);
EXPECT_TRUE(epoch2->topics[3]->observed_by_domains.empty());
EXPECT_EQ(epoch2->topics[4]->topic_id, 5);
EXPECT_EQ(epoch2->topics[4]->topic_name, u"Dance");
EXPECT_TRUE(epoch2->topics[4]->is_real_topic);
EXPECT_TRUE(epoch2->topics[4]->observed_by_domains.empty());
}
TEST_F(BrowsingTopicsServiceImplTest, ClearTopic) {
base::Time start_time = base::Time::Now();
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
start_time));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {}},
{Topic(7), {}},
{Topic(8), {}},
{Topic(9), {}},
{Topic(10), {}}},
start_time + kCalculatorDelay + kEpoch));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish file loading and two calculations.
task_environment()->FastForwardBy(2 * kCalculatorDelay + kEpoch);
browsing_topics_service_->ClearTopic(
privacy_sandbox::CanonicalTopic(Topic(3), /*taxonomy_version=*/1));
std::vector<privacy_sandbox::CanonicalTopic> result =
browsing_topics_service_->GetTopTopicsForDisplay();
EXPECT_EQ(result.size(), 9u);
EXPECT_EQ(result[0].topic_id(), Topic(1));
EXPECT_EQ(result[1].topic_id(), Topic(2));
EXPECT_EQ(result[2].topic_id(), Topic(4));
EXPECT_EQ(result[3].topic_id(), Topic(5));
EXPECT_EQ(result[4].topic_id(), Topic(6));
EXPECT_EQ(result[5].topic_id(), Topic(7));
EXPECT_EQ(result[6].topic_id(), Topic(8));
EXPECT_EQ(result[7].topic_id(), Topic(9));
EXPECT_EQ(result[8].topic_id(), Topic(10));
}
TEST_F(BrowsingTopicsServiceImplTest, ClearTopicBeforeLoadFinish) {
base::Time start_time = base::Time::Now();
std::vector<EpochTopics> preexisting_epochs;
preexisting_epochs.push_back(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
start_time - kOneTestDay));
CreateBrowsingTopicsStateFile(
std::move(preexisting_epochs),
/*next_scheduled_calculation_time=*/start_time + kOneTestDay);
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(6), {}},
{Topic(7), {}},
{Topic(8), {}},
{Topic(9), {}},
{Topic(10), {}}},
kTime2));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
privacy_sandbox_settings_->SetTopicAllowed(
privacy_sandbox::CanonicalTopic(Topic(3), /*taxonomy_version=*/1), false);
// Finish file loading.
task_environment()->RunUntilIdle();
// If a topic in the settings is cleared before load finish, all pre-existing
// topics data will be cleared in the `BrowsingTopicsState` after load finish.
std::vector<privacy_sandbox::CanonicalTopic> result =
browsing_topics_service_->GetTopTopicsForDisplay();
EXPECT_EQ(result.size(), 0u);
}
TEST_F(BrowsingTopicsServiceImplTest, ClearAllTopicsData) {
base::Time start_time = base::Time::Now();
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}},
{Topic(2), {}},
{Topic(3), {}},
{Topic(4), {}},
{Topic(5), {}}},
start_time));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {}},
{Topic(7), {}},
{Topic(8), {}},
{Topic(9), {}},
{Topic(10), {}}},
start_time + kCalculatorDelay + kEpoch));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
// Finish file loading and two calculations.
task_environment()->FastForwardBy(2 * kCalculatorDelay + kEpoch);
// Add some arbitrary data to site data storage. The intent is just to test
// data deletion.
topics_site_data_manager()->OnBrowsingTopicsApiUsed(
HashMainFrameHostForStorage("a.com"), {HashedDomain(1), HashedDomain(2)},
base::Time::Now());
EXPECT_EQ(
content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(),
2u);
task_environment()->FastForwardBy(base::Microseconds(1));
browsing_topics_service_->ClearAllTopicsData();
task_environment()->RunUntilIdle();
std::vector<privacy_sandbox::CanonicalTopic> result =
browsing_topics_service_->GetTopTopicsForDisplay();
EXPECT_TRUE(browsing_topics_service_->GetTopTopicsForDisplay().empty());
EXPECT_TRUE(
content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).empty());
}
TEST_F(BrowsingTopicsServiceImplTest, ClearTopicsDataForOrigin) {
base::queue<EpochTopics> mock_calculator_results;
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(1), {GetHashedDomain("b.com")}},
{Topic(2), {GetHashedDomain("b.com")}},
{Topic(3), {GetHashedDomain("b.com")}},
{Topic(4), {GetHashedDomain("b.com")}},
{Topic(5), {GetHashedDomain("b.com")}}},
kTime1));
mock_calculator_results.push(
CreateTestEpochTopics({{Topic(6), {GetHashedDomain("b.com")}},
{Topic(7), {GetHashedDomain("b.com")}},
{Topic(8), {GetHashedDomain("b.com")}},
{Topic(9), {GetHashedDomain("b.com")}},
{Topic(10), {GetHashedDomain("b.com")}}},
kTime1 + kCalculatorDelay + kEpoch));
InitializeBrowsingTopicsService(std::move(mock_calculator_results));
NavigateToPage(GURL("https://a.com"));
// Finish file loading and two calculations.
task_environment()->FastForwardBy(2 * kCalculatorDelay + kEpoch);
{
std::vector<blink::mojom::EpochTopicPtr> api_call_result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.b.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, api_call_result));
EXPECT_EQ(api_call_result.size(), 1u);
EXPECT_EQ(api_call_result[0]->topic, 3);
}
// Add some arbitrary data to site data storage. The intent is just to test
// data deletion.
topics_site_data_manager()->OnBrowsingTopicsApiUsed(
HashMainFrameHostForStorage("d.com"),
{GetHashedDomain("b.com"), GetHashedDomain("c.com")}, base::Time::Now());
// The data is from both the manual `OnBrowsingTopicsApiUsed` call and the
// usage tracking due to the previous `HandleTopicsWebApi`.
EXPECT_EQ(
content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(),
3u);
task_environment()->FastForwardBy(base::Microseconds(1));
browsing_topics_service_->ClearTopicsDataForOrigin(
url::Origin::Create(GURL("https://b.com")));
task_environment()->RunUntilIdle();
// Note that this won't trigger another usage storing to the database, because
// the same context domain was seen in the page before.
{
std::vector<blink::mojom::EpochTopicPtr> api_call_result;
EXPECT_TRUE(browsing_topics_service_->HandleTopicsWebApi(
/*context_origin=*/url::Origin::Create(GURL("https://www.b.com")),
web_contents()->GetPrimaryMainFrame(), ApiCallerSource::kJavaScript,
/*get_topics=*/true,
/*observe=*/true, api_call_result));
// Since the domain "b.com" is removed. The candidate topic won't be
// returned.
EXPECT_TRUE(api_call_result.empty());
}
std::vector<browsing_topics::ApiUsageContext> api_usage_contexts =
content::GetBrowsingTopicsApiUsage(topics_site_data_manager());
// The context domain "b.com" should have been cleared.
EXPECT_EQ(api_usage_contexts.size(), 1u);
EXPECT_EQ(api_usage_contexts[0].hashed_main_frame_host,
HashMainFrameHostForStorage("d.com"));
EXPECT_EQ(api_usage_contexts[0].hashed_context_domain,
GetHashedDomain("c.com"));
}
} // namespace browsing_topics