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/epoch_topics.h"
7 #include "base/json/values_util.h"
8 #include "base/logging.h"
9 #include "components/browsing_topics/util.h"
10 #include "testing/gtest/include/gtest/gtest.h"
12 namespace browsing_topics {
16 constexpr base::Time kCalculationTime =
17 base::Time::FromDeltaSinceWindowsEpoch(base::Days(1));
18 constexpr browsing_topics::HmacKey kTestKey = {1};
19 constexpr size_t kTaxonomySize = 349;
20 constexpr int kConfigVersion = 1;
21 constexpr int kTaxonomyVersion = 1;
22 constexpr int64_t kModelVersion = 2;
23 constexpr size_t kPaddedTopTopicsStartIndex = 2;
25 std::vector<TopicAndDomains> CreateTestTopTopics() {
26 std::vector<TopicAndDomains> top_topics_and_observing_domains;
27 top_topics_and_observing_domains.emplace_back(
28 TopicAndDomains(Topic(1), {HashedDomain(1)}));
29 top_topics_and_observing_domains.emplace_back(
30 TopicAndDomains(Topic(2), {HashedDomain(1), HashedDomain(2)}));
31 top_topics_and_observing_domains.emplace_back(
32 TopicAndDomains(Topic(3), {HashedDomain(1), HashedDomain(3)}));
33 top_topics_and_observing_domains.emplace_back(
34 TopicAndDomains(Topic(4), {HashedDomain(2), HashedDomain(3)}));
35 top_topics_and_observing_domains.emplace_back(
36 TopicAndDomains(Topic(100), {HashedDomain(1)}));
37 return top_topics_and_observing_domains;
40 EpochTopics CreateTestEpochTopics() {
41 EpochTopics epoch_topics(CreateTestTopTopics(), kPaddedTopTopicsStartIndex,
42 kConfigVersion, kTaxonomyVersion, kModelVersion,
44 /*from_manually_triggered_calculation=*/true);
51 class EpochTopicsTest : public testing::Test {};
53 TEST_F(EpochTopicsTest, CandidateTopicForSite_InvalidIndividualTopics) {
54 std::vector<TopicAndDomains> top_topics_and_observing_domains;
55 for (int i = 0; i < 5; ++i) {
56 top_topics_and_observing_domains.emplace_back(TopicAndDomains());
59 EpochTopics epoch_topics(std::move(top_topics_and_observing_domains),
60 kPaddedTopTopicsStartIndex, kConfigVersion,
61 kTaxonomyVersion, kModelVersion, kCalculationTime,
62 /*from_manually_triggered_calculation=*/false);
63 EXPECT_FALSE(epoch_topics.empty());
65 CandidateTopic candidate_topic = epoch_topics.CandidateTopicForSite(
66 /*top_domain=*/"foo.com", /*hashed_context_domain=*/HashedDomain(2),
68 EXPECT_FALSE(candidate_topic.IsValid());
71 TEST_F(EpochTopicsTest, CandidateTopicForSite) {
72 EpochTopics epoch_topics = CreateTestEpochTopics();
74 EXPECT_FALSE(epoch_topics.empty());
75 EXPECT_EQ(epoch_topics.config_version(), kConfigVersion);
76 EXPECT_EQ(epoch_topics.taxonomy_version(), kTaxonomyVersion);
77 EXPECT_EQ(epoch_topics.model_version(), kModelVersion);
78 EXPECT_EQ(epoch_topics.calculation_time(), kCalculationTime);
81 std::string top_site = "foo.com";
82 uint64_t random_or_top_topic_decision_hash =
83 HashTopDomainForRandomOrTopTopicDecision(kTestKey, kCalculationTime,
86 // `random_or_top_topic_decision_hash` mod 100 is not less than 5. Thus one
87 // of the top 5 topics will be the candidate topic.
88 ASSERT_GE(random_or_top_topic_decision_hash % 100, 5ULL);
90 uint64_t top_topics_index_decision_hash =
91 HashTopDomainForTopTopicIndexDecision(kTestKey, kCalculationTime,
94 // The topic index is 1, thus the candidate topic is Topic(2). Only the
95 // context with HashedDomain(1) or HashedDomain(2) is allowed to see it.
96 ASSERT_EQ(top_topics_index_decision_hash % 5, 1ULL);
98 CandidateTopic candidate_topic = epoch_topics.CandidateTopicForSite(
99 top_site, HashedDomain(1), kTestKey);
101 EXPECT_EQ(candidate_topic.topic(), Topic(2));
102 EXPECT_TRUE(candidate_topic.is_true_topic());
103 EXPECT_FALSE(candidate_topic.should_be_filtered());
107 CandidateTopic candidate_topic = epoch_topics.CandidateTopicForSite(
108 top_site, HashedDomain(2), kTestKey);
110 EXPECT_EQ(candidate_topic.topic(), Topic(2));
111 EXPECT_TRUE(candidate_topic.is_true_topic());
112 EXPECT_FALSE(candidate_topic.should_be_filtered());
116 CandidateTopic candidate_topic = epoch_topics.CandidateTopicForSite(
117 top_site, HashedDomain(3), kTestKey);
119 EXPECT_EQ(candidate_topic.topic(), Topic(2));
120 EXPECT_TRUE(candidate_topic.is_true_topic());
121 EXPECT_TRUE(candidate_topic.should_be_filtered());
126 std::string top_site = "foo1.com";
127 uint64_t random_or_top_topic_decision_hash =
128 HashTopDomainForRandomOrTopTopicDecision(kTestKey, kCalculationTime,
131 // `random_or_top_topic_decision_hash` mod 100 is not less than 5. Thus one
132 // of the top 5 topics will be the candidate topic.
133 ASSERT_GE(random_or_top_topic_decision_hash % 100, 5ULL);
135 uint64_t top_topics_index_decision_hash =
136 HashTopDomainForTopTopicIndexDecision(kTestKey, kCalculationTime,
139 // The topic index is 2, thus the candidate topic is Topic(3). Only the
140 // context with HashedDomain(1) or HashedDomain(3) is allowed to see it.
141 ASSERT_EQ(top_topics_index_decision_hash % 5, 2ULL);
143 CandidateTopic candidate_topic = epoch_topics.CandidateTopicForSite(
144 top_site, HashedDomain(1), kTestKey);
146 EXPECT_EQ(candidate_topic.topic(), Topic(3));
147 EXPECT_FALSE(candidate_topic.is_true_topic());
148 EXPECT_FALSE(candidate_topic.should_be_filtered());
152 CandidateTopic candidate_topic = epoch_topics.CandidateTopicForSite(
153 top_site, HashedDomain(2), kTestKey);
155 EXPECT_EQ(candidate_topic.topic(), Topic(3));
156 EXPECT_FALSE(candidate_topic.is_true_topic());
157 EXPECT_TRUE(candidate_topic.should_be_filtered());
161 CandidateTopic candidate_topic = epoch_topics.CandidateTopicForSite(
162 top_site, HashedDomain(3), kTestKey);
164 EXPECT_EQ(candidate_topic.topic(), Topic(3));
165 EXPECT_FALSE(candidate_topic.is_true_topic());
166 EXPECT_FALSE(candidate_topic.should_be_filtered());
171 std::string top_site = "foo5.com";
172 uint64_t random_or_top_topic_decision_hash =
173 HashTopDomainForRandomOrTopTopicDecision(kTestKey, kCalculationTime,
176 // `random_or_top_topic_decision_hash` mod 100 is less than 5. Thus the
177 // random topic will be returned.
178 ASSERT_LT(random_or_top_topic_decision_hash % 100, 5ULL);
180 uint64_t random_topic_index_decision =
181 HashTopDomainForRandomTopicIndexDecision(kTestKey, kCalculationTime,
184 // The real topic would have been 4, but a random topic (186) is returned
185 // instead. Only callers that are able to receive 4 (domains 2 and 3) should
186 // receive the random topic.
187 ASSERT_EQ(random_topic_index_decision % kTaxonomySize, 185ULL);
190 CandidateTopic candidate_topic = epoch_topics.CandidateTopicForSite(
191 top_site, HashedDomain(1), kTestKey);
193 EXPECT_EQ(candidate_topic.topic(), Topic(186));
194 EXPECT_FALSE(candidate_topic.is_true_topic());
195 EXPECT_TRUE(candidate_topic.should_be_filtered());
199 CandidateTopic candidate_topic = epoch_topics.CandidateTopicForSite(
200 top_site, HashedDomain(2), kTestKey);
202 EXPECT_EQ(candidate_topic.topic(), Topic(186));
203 EXPECT_FALSE(candidate_topic.is_true_topic());
204 EXPECT_FALSE(candidate_topic.should_be_filtered());
208 CandidateTopic candidate_topic = epoch_topics.CandidateTopicForSite(
209 top_site, HashedDomain(3), kTestKey);
211 EXPECT_EQ(candidate_topic.topic(), Topic(186));
212 EXPECT_FALSE(candidate_topic.is_true_topic());
213 EXPECT_FALSE(candidate_topic.should_be_filtered());
218 TEST_F(EpochTopicsTest, ClearTopics) {
219 EpochTopics epoch_topics = CreateTestEpochTopics();
221 EXPECT_FALSE(epoch_topics.empty());
223 epoch_topics.ClearTopics();
225 EXPECT_TRUE(epoch_topics.empty());
227 CandidateTopic candidate_topic = epoch_topics.CandidateTopicForSite(
228 /*top_domain=*/"foo.com", HashedDomain(1), kTestKey);
230 EXPECT_FALSE(candidate_topic.IsValid());
233 TEST_F(EpochTopicsTest, ClearTopic_NoDescendants) {
234 EpochTopics epoch_topics = CreateTestEpochTopics();
236 EXPECT_FALSE(epoch_topics.empty());
238 epoch_topics.ClearTopic(Topic(3));
240 EXPECT_FALSE(epoch_topics.empty());
242 EXPECT_TRUE(epoch_topics.top_topics_and_observing_domains()[0].IsValid());
243 EXPECT_TRUE(epoch_topics.top_topics_and_observing_domains()[1].IsValid());
244 EXPECT_FALSE(epoch_topics.top_topics_and_observing_domains()[2].IsValid());
245 EXPECT_TRUE(epoch_topics.top_topics_and_observing_domains()[3].IsValid());
246 EXPECT_TRUE(epoch_topics.top_topics_and_observing_domains()[4].IsValid());
249 TEST_F(EpochTopicsTest, ClearTopic_WithDescendants) {
250 EpochTopics epoch_topics = CreateTestEpochTopics();
252 EXPECT_FALSE(epoch_topics.empty());
254 epoch_topics.ClearTopic(Topic(1));
256 EXPECT_FALSE(epoch_topics.empty());
258 EXPECT_FALSE(epoch_topics.top_topics_and_observing_domains()[0].IsValid());
259 EXPECT_FALSE(epoch_topics.top_topics_and_observing_domains()[1].IsValid());
260 EXPECT_FALSE(epoch_topics.top_topics_and_observing_domains()[2].IsValid());
261 EXPECT_FALSE(epoch_topics.top_topics_and_observing_domains()[3].IsValid());
262 EXPECT_TRUE(epoch_topics.top_topics_and_observing_domains()[4].IsValid());
265 TEST_F(EpochTopicsTest, ClearContextDomain) {
266 EpochTopics epoch_topics = CreateTestEpochTopics();
268 EXPECT_FALSE(epoch_topics.empty());
270 epoch_topics.ClearContextDomain(HashedDomain(1));
272 EXPECT_FALSE(epoch_topics.empty());
274 EXPECT_EQ(epoch_topics.top_topics_and_observing_domains()[0].hashed_domains(),
275 std::set<HashedDomain>{});
276 EXPECT_EQ(epoch_topics.top_topics_and_observing_domains()[1].hashed_domains(),
277 std::set<HashedDomain>({HashedDomain(2)}));
278 EXPECT_EQ(epoch_topics.top_topics_and_observing_domains()[2].hashed_domains(),
279 std::set<HashedDomain>({HashedDomain(3)}));
280 EXPECT_EQ(epoch_topics.top_topics_and_observing_domains()[3].hashed_domains(),
281 std::set<HashedDomain>({HashedDomain(2), HashedDomain(3)}));
282 EXPECT_EQ(epoch_topics.top_topics_and_observing_domains()[4].hashed_domains(),
283 std::set<HashedDomain>{});
286 TEST_F(EpochTopicsTest, FromEmptyDictionaryValue) {
287 EpochTopics read_epoch_topics =
288 EpochTopics::FromDictValue(base::Value::Dict());
290 EXPECT_TRUE(read_epoch_topics.empty());
291 EXPECT_EQ(read_epoch_topics.config_version(), 0);
292 EXPECT_EQ(read_epoch_topics.taxonomy_version(), 0);
293 EXPECT_EQ(read_epoch_topics.model_version(), 0);
294 EXPECT_EQ(read_epoch_topics.calculation_time(), base::Time());
296 CandidateTopic candidate_topic = read_epoch_topics.CandidateTopicForSite(
297 /*top_domain=*/"foo.com", HashedDomain(1), kTestKey);
299 EXPECT_FALSE(candidate_topic.IsValid());
302 TEST_F(EpochTopicsTest,
303 FromDictionaryValueWithoutConfigVersion_UseConfigVersion1) {
304 base::Value::Dict dict;
306 base::Value::List top_topics_and_observing_domains_list;
307 std::vector<TopicAndDomains> top_topics_and_domains = CreateTestTopTopics();
308 for (const TopicAndDomains& topic_and_domains : top_topics_and_domains) {
309 top_topics_and_observing_domains_list.Append(
310 topic_and_domains.ToDictValue());
313 dict.Set("top_topics_and_observing_domains",
314 std::move(top_topics_and_observing_domains_list));
315 dict.Set("padded_top_topics_start_index", 0);
316 dict.Set("taxonomy_version", 2);
317 dict.Set("model_version", base::Int64ToValue(3));
318 dict.Set("calculation_time", base::TimeToValue(kCalculationTime));
320 EpochTopics read_epoch_topics = EpochTopics::FromDictValue(std::move(dict));
322 EXPECT_FALSE(read_epoch_topics.empty());
323 EXPECT_EQ(read_epoch_topics.config_version(), 1);
324 EXPECT_EQ(read_epoch_topics.taxonomy_version(), 2);
325 EXPECT_EQ(read_epoch_topics.model_version(), 3);
326 EXPECT_EQ(read_epoch_topics.calculation_time(), kCalculationTime);
329 TEST_F(EpochTopicsTest, EmptyEpochTopics_ToAndFromDictValue) {
330 EpochTopics epoch_topics(kCalculationTime);
332 base::Value::Dict dict_value = epoch_topics.ToDictValue();
333 EpochTopics read_epoch_topics = EpochTopics::FromDictValue(dict_value);
335 EXPECT_TRUE(read_epoch_topics.empty());
336 EXPECT_EQ(read_epoch_topics.config_version(), 0);
337 EXPECT_EQ(read_epoch_topics.taxonomy_version(), 0);
338 EXPECT_EQ(read_epoch_topics.model_version(), 0);
339 EXPECT_EQ(read_epoch_topics.calculation_time(), kCalculationTime);
341 CandidateTopic candidate_topic = epoch_topics.CandidateTopicForSite(
342 /*top_domain=*/"foo.com", HashedDomain(1), kTestKey);
344 EXPECT_FALSE(candidate_topic.IsValid());
347 TEST_F(EpochTopicsTest, PopulatedEpochTopics_ToAndFromValue) {
348 EpochTopics epoch_topics = CreateTestEpochTopics();
350 base::Value::Dict dict_value = epoch_topics.ToDictValue();
351 EpochTopics read_epoch_topics = EpochTopics::FromDictValue(dict_value);
353 EXPECT_FALSE(read_epoch_topics.empty());
354 EXPECT_EQ(read_epoch_topics.config_version(), 1);
355 EXPECT_EQ(read_epoch_topics.taxonomy_version(), 1);
356 EXPECT_EQ(read_epoch_topics.model_version(), 2);
357 EXPECT_EQ(read_epoch_topics.calculation_time(), kCalculationTime);
359 // `from_manually_triggered_calculation` should not persist after being
361 EXPECT_TRUE(epoch_topics.from_manually_triggered_calculation());
362 EXPECT_FALSE(read_epoch_topics.from_manually_triggered_calculation());
364 CandidateTopic candidate_topic = epoch_topics.CandidateTopicForSite(
365 /*top_domain=*/"foo.com", HashedDomain(1), kTestKey);
367 EXPECT_EQ(candidate_topic.topic(), Topic(2));
370 } // namespace browsing_topics