1 // Copyright 2022 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/browsing_topics/browsing_topics_state.h"
7 #include "base/files/file_util.h"
8 #include "base/files/scoped_temp_dir.h"
9 #include "base/functional/callback_helpers.h"
10 #include "base/json/json_file_value_serializer.h"
11 #include "base/json/values_util.h"
12 #include "base/ranges/algorithm.h"
13 #include "base/strings/strcat.h"
14 #include "base/test/metrics/histogram_tester.h"
15 #include "base/test/scoped_feature_list.h"
16 #include "base/test/task_environment.h"
17 #include "components/browsing_topics/util.h"
18 #include "testing/gtest/include/gtest/gtest.h"
19 #include "third_party/blink/public/common/features.h"
21 namespace browsing_topics {
25 constexpr base::Time kTime1 =
26 base::Time::FromDeltaSinceWindowsEpoch(base::Days(1));
27 constexpr base::Time kTime2 =
28 base::Time::FromDeltaSinceWindowsEpoch(base::Days(2));
29 constexpr base::Time kTime3 =
30 base::Time::FromDeltaSinceWindowsEpoch(base::Days(3));
31 constexpr base::Time kTime4 =
32 base::Time::FromDeltaSinceWindowsEpoch(base::Days(4));
33 constexpr base::Time kTime5 =
34 base::Time::FromDeltaSinceWindowsEpoch(base::Days(5));
36 constexpr browsing_topics::HmacKey kZeroKey = {};
37 constexpr browsing_topics::HmacKey kTestKey = {1};
38 constexpr browsing_topics::HmacKey kTestKey2 = {2};
40 constexpr int kConfigVersion = 1;
41 constexpr int kTaxonomyVersion = 1;
42 constexpr int64_t kModelVersion = 2;
43 constexpr size_t kPaddedTopTopicsStartIndex = 3;
45 EpochTopics CreateTestEpochTopics(base::Time calculation_time,
46 bool from_manually_triggered_calculation,
47 int config_version = kConfigVersion) {
48 std::vector<TopicAndDomains> top_topics_and_observing_domains;
49 top_topics_and_observing_domains.emplace_back(
50 TopicAndDomains(Topic(1), {HashedDomain(1)}));
51 top_topics_and_observing_domains.emplace_back(
52 TopicAndDomains(Topic(2), {HashedDomain(1), HashedDomain(2)}));
53 top_topics_and_observing_domains.emplace_back(
54 TopicAndDomains(Topic(3), {HashedDomain(1), HashedDomain(3)}));
55 top_topics_and_observing_domains.emplace_back(
56 TopicAndDomains(Topic(4), {HashedDomain(2), HashedDomain(3)}));
57 top_topics_and_observing_domains.emplace_back(
58 TopicAndDomains(Topic(5), {HashedDomain(1)}));
60 EpochTopics epoch_topics(std::move(top_topics_and_observing_domains),
61 kPaddedTopTopicsStartIndex, config_version,
62 kTaxonomyVersion, kModelVersion, calculation_time,
63 from_manually_triggered_calculation);
70 class BrowsingTopicsStateTest : public testing::Test {
72 BrowsingTopicsStateTest()
73 : task_environment_(new base::test::TaskEnvironment(
74 base::test::TaskEnvironment::TimeSource::MOCK_TIME)) {
75 OverrideHmacKeyForTesting(kTestKey);
77 EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
80 base::FilePath TestFilePath() {
81 return temp_dir_.GetPath().Append(FILE_PATH_LITERAL("BrowsingTopicsState"));
84 std::string GetTestFileContent() {
85 JSONFileValueDeserializer deserializer(TestFilePath());
86 std::unique_ptr<base::Value> value = deserializer.Deserialize(
87 /*error_code=*/nullptr,
88 /*error_message=*/nullptr);
91 return base::CollapseWhitespaceASCII(value->DebugString(), true);
94 void CreateOrOverrideTestFile(std::vector<EpochTopics> epochs,
95 base::Time next_scheduled_calculation_time,
96 std::string hex_encoded_hmac_key) {
97 base::Value::List epochs_list;
98 for (const EpochTopics& epoch : epochs) {
99 epochs_list.Append(epoch.ToDictValue());
102 base::Value::Dict dict;
103 dict.Set("epochs", std::move(epochs_list));
104 dict.Set("next_scheduled_calculation_time",
105 base::TimeToValue(next_scheduled_calculation_time));
106 dict.Set("hex_encoded_hmac_key", std::move(hex_encoded_hmac_key));
108 JSONFileValueSerializer(TestFilePath()).Serialize(dict);
111 void OnBrowsingTopicsStateLoaded() { observed_state_loaded_ = true; }
113 bool observed_state_loaded() const { return observed_state_loaded_; }
116 base::test::ScopedFeatureList feature_list_;
118 std::unique_ptr<base::test::TaskEnvironment> task_environment_;
120 base::ScopedTempDir temp_dir_;
122 bool observed_state_loaded_ = false;
125 TEST_F(BrowsingTopicsStateTest, InitFromNoFile_SaveToDiskAfterDelay) {
126 base::HistogramTester histograms;
128 BrowsingTopicsState state(
130 base::BindOnce(&BrowsingTopicsStateTest::OnBrowsingTopicsStateLoaded,
131 base::Unretained(this)));
133 EXPECT_FALSE(state.HasScheduledSaveForTesting());
134 EXPECT_FALSE(observed_state_loaded());
136 // UMA should not be recorded yet.
137 histograms.ExpectTotalCount(
138 "BrowsingTopics.BrowsingTopicsState.LoadFinishStatus", 0);
140 // Let the backend file read task finish.
141 task_environment_->RunUntilIdle();
143 histograms.ExpectUniqueSample(
144 "BrowsingTopics.BrowsingTopicsState.LoadFinishStatus", true,
145 /*expected_bucket_count=*/1);
147 EXPECT_TRUE(state.epochs().empty());
148 EXPECT_TRUE(state.next_scheduled_calculation_time().is_null());
149 EXPECT_TRUE(base::ranges::equal(state.hmac_key(), kTestKey));
151 EXPECT_TRUE(state.HasScheduledSaveForTesting());
152 EXPECT_TRUE(observed_state_loaded());
154 // Advance clock until immediately before saving takes place.
155 task_environment_->FastForwardBy(base::Milliseconds(2499));
156 EXPECT_TRUE(state.HasScheduledSaveForTesting());
157 EXPECT_FALSE(base::PathExists(TestFilePath()));
159 // Advance clock past the saving moment.
160 task_environment_->FastForwardBy(base::Milliseconds(1));
161 EXPECT_FALSE(state.HasScheduledSaveForTesting());
162 EXPECT_TRUE(base::PathExists(TestFilePath()));
164 GetTestFileContent(),
165 "{\"epochs\": [ ],\"hex_encoded_hmac_key\": "
166 "\"0100000000000000000000000000000000000000000000000000000000000000\","
167 "\"next_scheduled_calculation_time\": \"0\"}");
170 TEST_F(BrowsingTopicsStateTest,
171 UpdateNextScheduledCalculationTime_SaveToDiskAfterDelay) {
172 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
174 task_environment_->FastForwardBy(base::Milliseconds(3000));
175 EXPECT_FALSE(state.HasScheduledSaveForTesting());
177 state.UpdateNextScheduledCalculationTime();
179 EXPECT_TRUE(state.epochs().empty());
180 EXPECT_EQ(state.next_scheduled_calculation_time(),
181 base::Time::Now() + base::Days(7));
182 EXPECT_TRUE(base::ranges::equal(state.hmac_key(), kTestKey));
184 EXPECT_TRUE(state.HasScheduledSaveForTesting());
186 task_environment_->FastForwardBy(base::Milliseconds(2499));
187 EXPECT_TRUE(state.HasScheduledSaveForTesting());
189 task_environment_->FastForwardBy(base::Milliseconds(1));
190 EXPECT_FALSE(state.HasScheduledSaveForTesting());
192 std::string expected_content = base::StrCat(
193 {"{\"epochs\": [ ],\"hex_encoded_hmac_key\": "
194 "\"0100000000000000000000000000000000000000000000000000000000000000"
195 "\",\"next_scheduled_calculation_time\": \"",
196 base::NumberToString(state.next_scheduled_calculation_time()
197 .ToDeltaSinceWindowsEpoch()
201 EXPECT_EQ(GetTestFileContent(), expected_content);
204 TEST_F(BrowsingTopicsStateTest, AddEpoch) {
205 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
206 task_environment_->RunUntilIdle();
208 // Successful topics calculation at `kTime1`.
209 absl::optional<EpochTopics> maybe_removed_epoch_1 =
210 state.AddEpoch(CreateTestEpochTopics(
211 kTime1, /*from_manually_triggered_calculation=*/false));
213 EXPECT_EQ(state.epochs().size(), 1u);
214 EXPECT_FALSE(state.epochs()[0].empty());
215 EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1);
216 EXPECT_FALSE(maybe_removed_epoch_1.has_value());
218 // Successful topics calculation at `kTime2`.
219 absl::optional<EpochTopics> maybe_removed_epoch_2 =
220 state.AddEpoch(CreateTestEpochTopics(
221 kTime2, /*from_manually_triggered_calculation=*/false));
222 EXPECT_EQ(state.epochs().size(), 2u);
223 EXPECT_FALSE(state.epochs()[0].empty());
224 EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1);
225 EXPECT_FALSE(state.epochs()[1].empty());
226 EXPECT_EQ(state.epochs()[1].calculation_time(), kTime2);
227 EXPECT_FALSE(maybe_removed_epoch_2.has_value());
229 // Failed topics calculation.
230 absl::optional<EpochTopics> maybe_removed_epoch_3 =
231 state.AddEpoch(EpochTopics(kTime3));
232 EXPECT_EQ(state.epochs().size(), 3u);
233 EXPECT_FALSE(state.epochs()[0].empty());
234 EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1);
235 EXPECT_FALSE(state.epochs()[1].empty());
236 EXPECT_EQ(state.epochs()[1].calculation_time(), kTime2);
237 EXPECT_TRUE(state.epochs()[2].empty());
238 EXPECT_EQ(state.epochs()[2].calculation_time(), kTime3);
239 EXPECT_FALSE(maybe_removed_epoch_3.has_value());
241 // Successful topics calculation at `kTime4`.
242 absl::optional<EpochTopics> maybe_removed_epoch_4 =
243 state.AddEpoch(CreateTestEpochTopics(
244 kTime4, /*from_manually_triggered_calculation=*/false));
245 EXPECT_EQ(state.epochs().size(), 4u);
246 EXPECT_FALSE(state.epochs()[0].empty());
247 EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1);
248 EXPECT_FALSE(state.epochs()[1].empty());
249 EXPECT_EQ(state.epochs()[1].calculation_time(), kTime2);
250 EXPECT_TRUE(state.epochs()[2].empty());
251 EXPECT_FALSE(state.epochs()[3].empty());
252 EXPECT_EQ(state.epochs()[3].calculation_time(), kTime4);
253 EXPECT_FALSE(maybe_removed_epoch_4.has_value());
255 // Successful topics calculation at `kTime5`. When this epoch is added, the
256 // first one should be evicted.
257 absl::optional<EpochTopics> maybe_removed_epoch_5 =
258 state.AddEpoch(CreateTestEpochTopics(
259 kTime5, /*from_manually_triggered_calculation=*/false));
260 EXPECT_EQ(state.epochs().size(), 4u);
261 EXPECT_FALSE(state.epochs()[0].empty());
262 EXPECT_EQ(state.epochs()[0].calculation_time(), kTime2);
263 EXPECT_TRUE(state.epochs()[1].empty());
264 EXPECT_FALSE(state.epochs()[2].empty());
265 EXPECT_EQ(state.epochs()[2].calculation_time(), kTime4);
266 EXPECT_FALSE(state.epochs()[3].empty());
267 EXPECT_EQ(state.epochs()[3].calculation_time(), kTime5);
268 EXPECT_TRUE(maybe_removed_epoch_5.has_value());
269 EXPECT_EQ(maybe_removed_epoch_5.value().calculation_time(), kTime1);
271 // The `next_scheduled_calculation_time` and `hmac_key` are unaffected.
272 EXPECT_EQ(state.next_scheduled_calculation_time(), base::Time());
273 EXPECT_TRUE(base::ranges::equal(state.hmac_key(), kTestKey));
276 TEST_F(BrowsingTopicsStateTest, EpochsForSite_Empty) {
277 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
278 task_environment_->RunUntilIdle();
280 EXPECT_TRUE(state.EpochsForSite(/*top_domain=*/"foo.com").empty());
283 TEST_F(BrowsingTopicsStateTest, EpochsForSite_OneEpoch_SwitchTimeNotArrived) {
284 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
285 task_environment_->RunUntilIdle();
287 state.AddEpoch(CreateTestEpochTopics(
288 kTime1, /*from_manually_triggered_calculation=*/false));
289 state.UpdateNextScheduledCalculationTime();
291 // The random per-site delay happens to be between (one hour, one day).
292 ASSERT_GT(state.CalculateSiteStickyTimeDelta("foo.com"), base::Hours(1));
293 ASSERT_LT(state.CalculateSiteStickyTimeDelta("foo.com"), base::Days(1));
295 task_environment_->FastForwardBy(base::Hours(1));
296 EXPECT_TRUE(state.EpochsForSite(/*top_domain=*/"foo.com").empty());
299 TEST_F(BrowsingTopicsStateTest, EpochsForSite_OneEpoch_SwitchTimeArrived) {
300 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
301 task_environment_->RunUntilIdle();
303 state.AddEpoch(CreateTestEpochTopics(
304 kTime1, /*from_manually_triggered_calculation=*/false));
305 state.UpdateNextScheduledCalculationTime();
307 // The random per-site delay happens to be between (one hour, one day).
308 ASSERT_GT(state.CalculateSiteStickyTimeDelta("foo.com"), base::Hours(1));
309 ASSERT_LT(state.CalculateSiteStickyTimeDelta("foo.com"), base::Days(1));
311 task_environment_->FastForwardBy(base::Days(1));
313 std::vector<const EpochTopics*> epochs_for_site =
314 state.EpochsForSite(/*top_domain=*/"foo.com");
315 EXPECT_EQ(epochs_for_site.size(), 1u);
316 EXPECT_EQ(epochs_for_site[0], &state.epochs()[0]);
319 TEST_F(BrowsingTopicsStateTest, EpochsForSite_OneEpoch_ManuallyTriggered) {
320 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
321 task_environment_->RunUntilIdle();
323 state.AddEpoch(CreateTestEpochTopics(
324 kTime1, /*from_manually_triggered_calculation=*/true));
325 state.UpdateNextScheduledCalculationTime();
327 // There shouldn't be a delay when the latest epoch is manually triggered.
328 ASSERT_EQ(state.CalculateSiteStickyTimeDelta("foo.com"),
329 base::Microseconds(0));
330 task_environment_->FastForwardBy(base::Microseconds(10));
332 std::vector<const EpochTopics*> epochs_for_site =
333 state.EpochsForSite(/*top_domain=*/"foo.com");
334 EXPECT_EQ(epochs_for_site.size(), 1u);
335 EXPECT_EQ(epochs_for_site[0], &state.epochs()[0]);
338 TEST_F(BrowsingTopicsStateTest,
339 EpochsForSite_ThreeEpochs_SwitchTimeNotArrived) {
340 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
341 task_environment_->RunUntilIdle();
343 state.AddEpoch(CreateTestEpochTopics(
344 kTime1, /*from_manually_triggered_calculation=*/false));
345 state.AddEpoch(CreateTestEpochTopics(
346 kTime2, /*from_manually_triggered_calculation=*/false));
347 state.AddEpoch(CreateTestEpochTopics(
348 kTime3, /*from_manually_triggered_calculation=*/false));
349 state.UpdateNextScheduledCalculationTime();
351 task_environment_->FastForwardBy(base::Hours(1));
353 std::vector<const EpochTopics*> epochs_for_site =
354 state.EpochsForSite(/*top_domain=*/"foo.com");
355 EXPECT_EQ(epochs_for_site.size(), 2u);
356 EXPECT_EQ(epochs_for_site[0], &state.epochs()[0]);
357 EXPECT_EQ(epochs_for_site[1], &state.epochs()[1]);
360 TEST_F(BrowsingTopicsStateTest, EpochsForSite_ThreeEpochs_SwitchTimeArrived) {
361 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
362 task_environment_->RunUntilIdle();
364 state.AddEpoch(CreateTestEpochTopics(
365 kTime1, /*from_manually_triggered_calculation=*/false));
366 state.AddEpoch(CreateTestEpochTopics(
367 kTime2, /*from_manually_triggered_calculation=*/false));
368 state.AddEpoch(CreateTestEpochTopics(
369 kTime3, /*from_manually_triggered_calculation=*/false));
370 state.UpdateNextScheduledCalculationTime();
372 task_environment_->FastForwardBy(base::Days(1));
374 std::vector<const EpochTopics*> epochs_for_site =
375 state.EpochsForSite(/*top_domain=*/"foo.com");
376 EXPECT_EQ(epochs_for_site.size(), 3u);
377 EXPECT_EQ(epochs_for_site[0], &state.epochs()[0]);
378 EXPECT_EQ(epochs_for_site[1], &state.epochs()[1]);
379 EXPECT_EQ(epochs_for_site[2], &state.epochs()[2]);
382 TEST_F(BrowsingTopicsStateTest,
383 EpochsForSite_ThreeEpochs_LatestManuallyTriggered) {
384 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
385 task_environment_->RunUntilIdle();
387 state.AddEpoch(CreateTestEpochTopics(
388 kTime1, /*from_manually_triggered_calculation=*/false));
389 state.AddEpoch(CreateTestEpochTopics(
390 kTime2, /*from_manually_triggered_calculation=*/false));
391 state.AddEpoch(CreateTestEpochTopics(
392 kTime3, /*from_manually_triggered_calculation=*/true));
393 state.UpdateNextScheduledCalculationTime();
395 task_environment_->FastForwardBy(base::Microseconds(10));
397 std::vector<const EpochTopics*> epochs_for_site =
398 state.EpochsForSite(/*top_domain=*/"foo.com");
399 EXPECT_EQ(epochs_for_site.size(), 3u);
400 EXPECT_EQ(epochs_for_site[0], &state.epochs()[0]);
401 EXPECT_EQ(epochs_for_site[1], &state.epochs()[1]);
402 EXPECT_EQ(epochs_for_site[2], &state.epochs()[2]);
405 TEST_F(BrowsingTopicsStateTest,
406 EpochsForSite_ThreeEpochs_EarlierEpochManuallyTriggered) {
407 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
408 task_environment_->RunUntilIdle();
410 state.AddEpoch(CreateTestEpochTopics(
411 kTime1, /*from_manually_triggered_calculation=*/false));
412 state.AddEpoch(CreateTestEpochTopics(
413 kTime2, /*from_manually_triggered_calculation=*/true));
414 state.AddEpoch(CreateTestEpochTopics(
415 kTime3, /*from_manually_triggered_calculation=*/false));
416 state.UpdateNextScheduledCalculationTime();
418 task_environment_->FastForwardBy(base::Microseconds(10));
420 std::vector<const EpochTopics*> epochs_for_site =
421 state.EpochsForSite(/*top_domain=*/"foo.com");
422 // The latest epoch shouldn't be included because it wasn't manually
424 EXPECT_EQ(epochs_for_site.size(), 2u);
425 EXPECT_EQ(epochs_for_site[0], &state.epochs()[0]);
426 EXPECT_EQ(epochs_for_site[1], &state.epochs()[1]);
429 TEST_F(BrowsingTopicsStateTest, EpochsForSite_FourEpochs_SwitchTimeNotArrived) {
430 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
431 task_environment_->RunUntilIdle();
433 state.AddEpoch(CreateTestEpochTopics(
434 kTime1, /*from_manually_triggered_calculation=*/false));
435 state.AddEpoch(CreateTestEpochTopics(
436 kTime2, /*from_manually_triggered_calculation=*/false));
437 state.AddEpoch(CreateTestEpochTopics(
438 kTime3, /*from_manually_triggered_calculation=*/false));
439 state.AddEpoch(CreateTestEpochTopics(
440 kTime4, /*from_manually_triggered_calculation=*/false));
441 state.UpdateNextScheduledCalculationTime();
443 task_environment_->FastForwardBy(base::Hours(1));
445 std::vector<const EpochTopics*> epochs_for_site =
446 state.EpochsForSite(/*top_domain=*/"foo.com");
447 EXPECT_EQ(epochs_for_site.size(), 3u);
448 EXPECT_EQ(epochs_for_site[0], &state.epochs()[0]);
449 EXPECT_EQ(epochs_for_site[1], &state.epochs()[1]);
450 EXPECT_EQ(epochs_for_site[2], &state.epochs()[2]);
453 TEST_F(BrowsingTopicsStateTest, EpochsForSite_FourEpochs_SwitchTimeArrived) {
454 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
455 task_environment_->RunUntilIdle();
457 state.AddEpoch(CreateTestEpochTopics(
458 kTime1, /*from_manually_triggered_calculation=*/false));
459 state.AddEpoch(CreateTestEpochTopics(
460 kTime2, /*from_manually_triggered_calculation=*/false));
461 state.AddEpoch(CreateTestEpochTopics(
462 kTime3, /*from_manually_triggered_calculation=*/false));
463 state.AddEpoch(CreateTestEpochTopics(
464 kTime4, /*from_manually_triggered_calculation=*/false));
465 state.UpdateNextScheduledCalculationTime();
467 task_environment_->FastForwardBy(base::Days(1));
469 std::vector<const EpochTopics*> epochs_for_site =
470 state.EpochsForSite(/*top_domain=*/"foo.com");
471 EXPECT_EQ(epochs_for_site.size(), 3u);
472 EXPECT_EQ(epochs_for_site[0], &state.epochs()[1]);
473 EXPECT_EQ(epochs_for_site[1], &state.epochs()[2]);
474 EXPECT_EQ(epochs_for_site[2], &state.epochs()[3]);
477 TEST_F(BrowsingTopicsStateTest,
478 EpochsForSite_FourEpochs_LatestManuallyTriggered) {
479 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
480 task_environment_->RunUntilIdle();
482 state.AddEpoch(CreateTestEpochTopics(
483 kTime1, /*from_manually_triggered_calculation=*/false));
484 state.AddEpoch(CreateTestEpochTopics(
485 kTime2, /*from_manually_triggered_calculation=*/false));
486 state.AddEpoch(CreateTestEpochTopics(
487 kTime3, /*from_manually_triggered_calculation=*/false));
488 state.AddEpoch(CreateTestEpochTopics(
489 kTime4, /*from_manually_triggered_calculation=*/true));
491 state.UpdateNextScheduledCalculationTime();
493 task_environment_->FastForwardBy(base::Microseconds(10));
495 std::vector<const EpochTopics*> epochs_for_site =
496 state.EpochsForSite(/*top_domain=*/"foo.com");
497 EXPECT_EQ(epochs_for_site.size(), 3u);
498 EXPECT_EQ(epochs_for_site[0], &state.epochs()[1]);
499 EXPECT_EQ(epochs_for_site[1], &state.epochs()[2]);
500 EXPECT_EQ(epochs_for_site[2], &state.epochs()[3]);
503 TEST_F(BrowsingTopicsStateTest,
504 EpochsForSite_FourEpochs_EarlierEpochManuallyTriggered) {
505 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
506 task_environment_->RunUntilIdle();
508 state.AddEpoch(CreateTestEpochTopics(
509 kTime1, /*from_manually_triggered_calculation=*/false));
510 state.AddEpoch(CreateTestEpochTopics(
511 kTime2, /*from_manually_triggered_calculation=*/true));
512 state.AddEpoch(CreateTestEpochTopics(
513 kTime3, /*from_manually_triggered_calculation=*/false));
514 state.AddEpoch(CreateTestEpochTopics(
515 kTime4, /*from_manually_triggered_calculation=*/false));
516 state.UpdateNextScheduledCalculationTime();
518 task_environment_->FastForwardBy(base::Microseconds(10));
520 std::vector<const EpochTopics*> epochs_for_site =
521 state.EpochsForSite(/*top_domain=*/"foo.com");
522 // The latest epoch shouldn't be included because it wasn't manually
524 EXPECT_EQ(epochs_for_site.size(), 3u);
525 EXPECT_EQ(epochs_for_site[0], &state.epochs()[0]);
526 EXPECT_EQ(epochs_for_site[1], &state.epochs()[1]);
527 EXPECT_EQ(epochs_for_site[2], &state.epochs()[2]);
530 TEST_F(BrowsingTopicsStateTest, InitFromPreexistingFile_CorruptedHmacKey) {
531 base::HistogramTester histograms;
533 std::vector<EpochTopics> epochs;
534 epochs.emplace_back(CreateTestEpochTopics(
535 kTime1, /*from_manually_triggered_calculation=*/false));
537 CreateOrOverrideTestFile(std::move(epochs),
538 /*next_scheduled_calculation_time=*/kTime2,
539 /*hex_encoded_hmac_key=*/"123");
541 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
542 task_environment_->RunUntilIdle();
544 EXPECT_EQ(state.epochs().size(), 0u);
545 EXPECT_TRUE(state.next_scheduled_calculation_time().is_null());
546 EXPECT_TRUE(base::ranges::equal(state.hmac_key(), kZeroKey));
548 histograms.ExpectUniqueSample(
549 "BrowsingTopics.BrowsingTopicsState.LoadFinishStatus", false,
550 /*expected_bucket_count=*/1);
553 TEST_F(BrowsingTopicsStateTest, InitFromPreexistingFile_SameConfigVersion) {
554 base::HistogramTester histograms;
556 std::vector<EpochTopics> epochs;
557 epochs.emplace_back(CreateTestEpochTopics(
558 kTime1, /*from_manually_triggered_calculation=*/false));
560 CreateOrOverrideTestFile(std::move(epochs),
561 /*next_scheduled_calculation_time=*/kTime2,
562 /*hex_encoded_hmac_key=*/base::HexEncode(kTestKey2));
564 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
565 task_environment_->RunUntilIdle();
567 EXPECT_EQ(state.epochs().size(), 1u);
568 EXPECT_FALSE(state.epochs()[0].empty());
569 EXPECT_EQ(state.epochs()[0].model_version(), kModelVersion);
570 EXPECT_EQ(state.next_scheduled_calculation_time(), kTime2);
571 EXPECT_TRUE(base::ranges::equal(state.hmac_key(), kTestKey2));
573 histograms.ExpectUniqueSample(
574 "BrowsingTopics.BrowsingTopicsState.LoadFinishStatus", true,
575 /*expected_bucket_count=*/1);
578 TEST_F(BrowsingTopicsStateTest,
579 InitFromPreexistingFile_ForwardCompatibleConfigVersion) {
580 base::HistogramTester histograms;
582 std::vector<EpochTopics> epochs;
583 // Current version is 1 but it's forward compatible with 2.
584 EXPECT_EQ(CurrentConfigVersion(), 1);
585 epochs.emplace_back(CreateTestEpochTopics(
586 kTime1, /*from_manually_triggered_calculation=*/false,
587 /*config_version=*/2));
589 CreateOrOverrideTestFile(std::move(epochs),
590 /*next_scheduled_calculation_time=*/kTime2,
591 /*hex_encoded_hmac_key=*/base::HexEncode(kTestKey2));
593 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
594 task_environment_->RunUntilIdle();
596 EXPECT_EQ(state.epochs().size(), 1u);
597 EXPECT_FALSE(state.epochs()[0].empty());
598 EXPECT_EQ(state.epochs()[0].model_version(), kModelVersion);
599 EXPECT_EQ(state.next_scheduled_calculation_time(), kTime2);
600 EXPECT_TRUE(base::ranges::equal(state.hmac_key(), kTestKey2));
602 histograms.ExpectUniqueSample(
603 "BrowsingTopics.BrowsingTopicsState.LoadFinishStatus", true,
604 /*expected_bucket_count=*/1);
607 TEST_F(BrowsingTopicsStateTest,
608 InitFromPreexistingFile_BackwardCompatibleConfigVersion) {
609 base::HistogramTester histograms;
611 std::vector<EpochTopics> epochs;
612 // Current version is 2 but it's backward compatible with 1.
613 base::test::ScopedFeatureList feature_list;
614 feature_list.InitAndEnableFeatureWithParameters(
615 blink::features::kBrowsingTopicsParameters,
616 {{"prioritized_topics_list", "4,57"}});
617 EXPECT_EQ(CurrentConfigVersion(), 2);
618 epochs.emplace_back(CreateTestEpochTopics(
619 kTime1, /*from_manually_triggered_calculation=*/false,
620 /*config_version=*/1));
622 CreateOrOverrideTestFile(std::move(epochs),
623 /*next_scheduled_calculation_time=*/kTime2,
624 /*hex_encoded_hmac_key=*/base::HexEncode(kTestKey2));
626 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
627 task_environment_->RunUntilIdle();
629 EXPECT_EQ(state.epochs().size(), 1u);
630 EXPECT_FALSE(state.epochs()[0].empty());
631 EXPECT_EQ(state.epochs()[0].model_version(), kModelVersion);
632 EXPECT_EQ(state.next_scheduled_calculation_time(), kTime2);
633 EXPECT_TRUE(base::ranges::equal(state.hmac_key(), kTestKey2));
635 histograms.ExpectUniqueSample(
636 "BrowsingTopics.BrowsingTopicsState.LoadFinishStatus", true,
637 /*expected_bucket_count=*/1);
640 TEST_F(BrowsingTopicsStateTest,
641 InitFromPreexistingFile_IncompatibleConfigVersion) {
642 base::HistogramTester histograms;
644 std::vector<EpochTopics> epochs;
645 epochs.emplace_back(CreateTestEpochTopics(
646 kTime1, /*from_manually_triggered_calculation=*/false,
647 /*config_version=*/100));
649 CreateOrOverrideTestFile(std::move(epochs),
650 /*next_scheduled_calculation_time=*/kTime2,
651 /*hex_encoded_hmac_key=*/base::HexEncode(kTestKey2));
653 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
654 task_environment_->RunUntilIdle();
656 EXPECT_TRUE(state.epochs().empty());
657 EXPECT_TRUE(state.next_scheduled_calculation_time().is_null());
658 EXPECT_TRUE(base::ranges::equal(state.hmac_key(), kTestKey2));
660 histograms.ExpectUniqueSample(
661 "BrowsingTopics.BrowsingTopicsState.LoadFinishStatus", true,
662 /*expected_bucket_count=*/1);
665 TEST_F(BrowsingTopicsStateTest, ClearOneEpoch) {
666 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
667 task_environment_->RunUntilIdle();
669 state.AddEpoch(CreateTestEpochTopics(
670 kTime1, /*from_manually_triggered_calculation=*/false));
672 EXPECT_EQ(state.epochs().size(), 1u);
673 EXPECT_FALSE(state.epochs()[0].empty());
674 EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1);
676 state.AddEpoch(CreateTestEpochTopics(
677 kTime2, /*from_manually_triggered_calculation=*/false));
678 EXPECT_EQ(state.epochs().size(), 2u);
679 EXPECT_FALSE(state.epochs()[0].empty());
680 EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1);
681 EXPECT_FALSE(state.epochs()[1].empty());
682 EXPECT_EQ(state.epochs()[1].calculation_time(), kTime2);
684 state.ClearOneEpoch(/*epoch_index=*/0);
685 EXPECT_EQ(state.epochs().size(), 2u);
686 EXPECT_TRUE(state.epochs()[0].empty());
687 EXPECT_FALSE(state.epochs()[1].empty());
688 EXPECT_EQ(state.epochs()[1].calculation_time(), kTime2);
690 state.UpdateNextScheduledCalculationTime();
692 EXPECT_EQ(state.next_scheduled_calculation_time(),
693 base::Time::Now() + base::Days(7));
694 EXPECT_TRUE(base::ranges::equal(state.hmac_key(), kTestKey));
697 TEST_F(BrowsingTopicsStateTest, ClearAllTopics) {
698 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
699 task_environment_->RunUntilIdle();
701 state.AddEpoch(CreateTestEpochTopics(
702 kTime1, /*from_manually_triggered_calculation=*/false));
704 EXPECT_EQ(state.epochs().size(), 1u);
705 EXPECT_FALSE(state.epochs()[0].empty());
706 EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1);
708 state.AddEpoch(CreateTestEpochTopics(
709 kTime2, /*from_manually_triggered_calculation=*/false));
710 EXPECT_EQ(state.epochs().size(), 2u);
711 EXPECT_FALSE(state.epochs()[0].empty());
712 EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1);
713 EXPECT_FALSE(state.epochs()[1].empty());
714 EXPECT_EQ(state.epochs()[1].calculation_time(), kTime2);
716 state.UpdateNextScheduledCalculationTime();
718 state.ClearAllTopics();
719 EXPECT_EQ(state.epochs().size(), 0u);
721 EXPECT_EQ(state.next_scheduled_calculation_time(),
722 base::Time::Now() + base::Days(7));
723 EXPECT_TRUE(base::ranges::equal(state.hmac_key(), kTestKey));
726 TEST_F(BrowsingTopicsStateTest, ClearTopic) {
727 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
728 task_environment_->RunUntilIdle();
730 state.AddEpoch(CreateTestEpochTopics(
731 kTime1, /*from_manually_triggered_calculation=*/false));
732 state.AddEpoch(CreateTestEpochTopics(
733 kTime2, /*from_manually_triggered_calculation=*/false));
734 state.UpdateNextScheduledCalculationTime();
736 state.ClearTopic(Topic(3));
738 EXPECT_EQ(state.epochs().size(), 2u);
739 EXPECT_EQ(state.epochs()[0].top_topics_and_observing_domains()[0].topic(),
741 EXPECT_EQ(state.epochs()[0].top_topics_and_observing_domains()[1].topic(),
744 state.epochs()[0].top_topics_and_observing_domains()[2].IsValid());
745 EXPECT_EQ(state.epochs()[0].top_topics_and_observing_domains()[3].topic(),
747 EXPECT_EQ(state.epochs()[0].top_topics_and_observing_domains()[4].topic(),
750 EXPECT_EQ(state.epochs()[1].top_topics_and_observing_domains()[0].topic(),
752 EXPECT_EQ(state.epochs()[1].top_topics_and_observing_domains()[1].topic(),
755 state.epochs()[1].top_topics_and_observing_domains()[2].IsValid());
756 EXPECT_EQ(state.epochs()[1].top_topics_and_observing_domains()[3].topic(),
758 EXPECT_EQ(state.epochs()[1].top_topics_and_observing_domains()[4].topic(),
762 TEST_F(BrowsingTopicsStateTest, ClearContextDomain) {
763 BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing());
764 task_environment_->RunUntilIdle();
766 state.AddEpoch(CreateTestEpochTopics(
767 kTime1, /*from_manually_triggered_calculation=*/false));
768 state.AddEpoch(CreateTestEpochTopics(
769 kTime2, /*from_manually_triggered_calculation=*/false));
770 state.UpdateNextScheduledCalculationTime();
772 state.ClearContextDomain(HashedDomain(1));
775 state.epochs()[0].top_topics_and_observing_domains()[0].hashed_domains(),
776 std::set<HashedDomain>{});
778 state.epochs()[0].top_topics_and_observing_domains()[1].hashed_domains(),
779 std::set<HashedDomain>({HashedDomain(2)}));
781 state.epochs()[0].top_topics_and_observing_domains()[2].hashed_domains(),
782 std::set<HashedDomain>({HashedDomain(3)}));
784 state.epochs()[0].top_topics_and_observing_domains()[3].hashed_domains(),
785 std::set<HashedDomain>({HashedDomain(2), HashedDomain(3)}));
787 state.epochs()[0].top_topics_and_observing_domains()[4].hashed_domains(),
788 std::set<HashedDomain>{});
791 state.epochs()[1].top_topics_and_observing_domains()[0].hashed_domains(),
792 std::set<HashedDomain>{});
794 state.epochs()[1].top_topics_and_observing_domains()[1].hashed_domains(),
795 std::set<HashedDomain>({HashedDomain(2)}));
797 state.epochs()[1].top_topics_and_observing_domains()[2].hashed_domains(),
798 std::set<HashedDomain>({HashedDomain(3)}));
800 state.epochs()[1].top_topics_and_observing_domains()[3].hashed_domains(),
801 std::set<HashedDomain>({HashedDomain(2), HashedDomain(3)}));
803 state.epochs()[1].top_topics_and_observing_domains()[4].hashed_domains(),
804 std::set<HashedDomain>{});
807 TEST_F(BrowsingTopicsStateTest, ShouldSaveFileDespiteShutdownWhileScheduled) {
808 auto state = std::make_unique<BrowsingTopicsState>(temp_dir_.GetPath(),
810 task_environment_->RunUntilIdle();
812 ASSERT_TRUE(state->HasScheduledSaveForTesting());
813 EXPECT_FALSE(base::PathExists(TestFilePath()));
816 task_environment_.reset();
818 // TaskEnvironment and BrowsingTopicsState both have been destroyed, mimic-ing
819 // a browser shutdown.
821 EXPECT_TRUE(base::PathExists(TestFilePath()));
823 GetTestFileContent(),
824 "{\"epochs\": [ ],\"hex_encoded_hmac_key\": "
825 "\"0100000000000000000000000000000000000000000000000000000000000000\","
826 "\"next_scheduled_calculation_time\": \"0\"}");
829 } // namespace browsing_topics