1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "chrome/browser/engagement/site_engagement_score.h"
6 
7 #include <utility>
8 
9 #include "base/macros.h"
10 #include "base/test/simple_test_clock.h"
11 #include "base/values.h"
12 #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
13 #include "chrome/browser/engagement/site_engagement_service.h"
14 #include "chrome/browser/profiles/profile.h"
15 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
16 #include "chrome/test/base/testing_profile.h"
17 #include "components/content_settings/core/browser/host_content_settings_map.h"
18 #include "testing/gtest/include/gtest/gtest.h"
19 
20 namespace {
21 
22 const int kLessAccumulationsThanNeededToMaxDailyEngagement = 2;
23 const int kMoreAccumulationsThanNeededToMaxDailyEngagement = 40;
24 const int kMoreAccumulationsThanNeededToMaxTotalEngagement = 200;
25 const int kLessDaysThanNeededToMaxTotalEngagement = 4;
26 const int kMoreDaysThanNeededToMaxTotalEngagement = 40;
27 const int kLessPeriodsThanNeededToDecayMaxScore = 2;
28 const int kMorePeriodsThanNeededToDecayMaxScore = 40;
29 const double kMaxRoundingDeviation = 0.0001;
30 
GetReferenceTime()31 base::Time GetReferenceTime() {
32   base::Time::Exploded exploded_reference_time;
33   exploded_reference_time.year = 2015;
34   exploded_reference_time.month = 1;
35   exploded_reference_time.day_of_month = 30;
36   exploded_reference_time.day_of_week = 5;
37   exploded_reference_time.hour = 11;
38   exploded_reference_time.minute = 0;
39   exploded_reference_time.second = 0;
40   exploded_reference_time.millisecond = 0;
41 
42   base::Time out_time;
43   EXPECT_TRUE(
44       base::Time::FromLocalExploded(exploded_reference_time, &out_time));
45   return out_time;
46 }
47 
48 }  // namespace
49 
50 class SiteEngagementScoreTest : public ChromeRenderViewHostTestHarness {
51  public:
SiteEngagementScoreTest()52   SiteEngagementScoreTest() : score_(&test_clock_, GURL(), nullptr) {}
53 
SetUp()54   void SetUp() override {
55     ChromeRenderViewHostTestHarness::SetUp();
56     // Disable the first engagement bonus for tests.
57     SiteEngagementScore::SetParamValuesForTesting();
58   }
59 
60  protected:
VerifyScore(const SiteEngagementScore & score,double expected_raw_score,double expected_points_added_today,base::Time expected_last_engagement_time)61   void VerifyScore(const SiteEngagementScore& score,
62                    double expected_raw_score,
63                    double expected_points_added_today,
64                    base::Time expected_last_engagement_time) {
65     EXPECT_EQ(expected_raw_score, score.raw_score_);
66     EXPECT_EQ(expected_points_added_today, score.points_added_today_);
67     EXPECT_EQ(expected_last_engagement_time, score.last_engagement_time_);
68   }
69 
UpdateScore(SiteEngagementScore * score,double raw_score,double points_added_today,base::Time last_engagement_time)70   void UpdateScore(SiteEngagementScore* score,
71                    double raw_score,
72                    double points_added_today,
73                    base::Time last_engagement_time) {
74     score->raw_score_ = raw_score;
75     score->points_added_today_ = points_added_today;
76     score->last_engagement_time_ = last_engagement_time;
77   }
78 
TestScoreInitializesAndUpdates(std::unique_ptr<base::DictionaryValue> score_dict,double expected_raw_score,double expected_points_added_today,base::Time expected_last_engagement_time)79   void TestScoreInitializesAndUpdates(
80       std::unique_ptr<base::DictionaryValue> score_dict,
81       double expected_raw_score,
82       double expected_points_added_today,
83       base::Time expected_last_engagement_time) {
84     std::unique_ptr<base::DictionaryValue> copy(score_dict->DeepCopy());
85     SiteEngagementScore initial_score(&test_clock_, GURL(),
86                                       std::move(score_dict));
87     VerifyScore(initial_score, expected_raw_score, expected_points_added_today,
88                 expected_last_engagement_time);
89 
90     // Updating the score dict should return false, as the score shouldn't
91     // have changed at this point.
92     EXPECT_FALSE(initial_score.UpdateScoreDict(copy.get()));
93 
94     // Update the score to new values and verify it updates the score dict
95     // correctly.
96     base::Time different_day =
97         GetReferenceTime() + base::TimeDelta::FromDays(1);
98     UpdateScore(&initial_score, 5, 10, different_day);
99     EXPECT_TRUE(initial_score.UpdateScoreDict(copy.get()));
100     SiteEngagementScore updated_score(&test_clock_, GURL(), std::move(copy));
101     VerifyScore(updated_score, 5, 10, different_day);
102   }
103 
SetParamValue(SiteEngagementScore::Variation variation,double value)104   void SetParamValue(SiteEngagementScore::Variation variation, double value) {
105     SiteEngagementScore::GetParamValues()[variation].second = value;
106   }
107 
108   base::SimpleTestClock test_clock_;
109   SiteEngagementScore score_;
110 };
111 
112 // Accumulate score many times on the same day. Ensure each time the score goes
113 // up, but not more than the maximum per day.
TEST_F(SiteEngagementScoreTest,AccumulateOnSameDay)114 TEST_F(SiteEngagementScoreTest, AccumulateOnSameDay) {
115   base::Time reference_time = GetReferenceTime();
116 
117   test_clock_.SetNow(reference_time);
118   for (int i = 0; i < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++i) {
119     score_.AddPoints(SiteEngagementScore::GetNavigationPoints());
120     EXPECT_EQ(std::min(SiteEngagementScore::GetMaxPointsPerDay(),
121                        (i + 1) * SiteEngagementScore::GetNavigationPoints()),
122               score_.GetTotalScore());
123   }
124 
125   EXPECT_EQ(SiteEngagementScore::GetMaxPointsPerDay(), score_.GetTotalScore());
126 }
127 
128 // Accumulate on the first day to max that day's engagement, then accumulate on
129 // a different day.
TEST_F(SiteEngagementScoreTest,AccumulateOnTwoDays)130 TEST_F(SiteEngagementScoreTest, AccumulateOnTwoDays) {
131   base::Time reference_time = GetReferenceTime();
132   base::Time later_date = reference_time + base::TimeDelta::FromDays(2);
133 
134   test_clock_.SetNow(reference_time);
135   for (int i = 0; i < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++i)
136     score_.AddPoints(SiteEngagementScore::GetNavigationPoints());
137 
138   EXPECT_EQ(SiteEngagementScore::GetMaxPointsPerDay(), score_.GetTotalScore());
139 
140   test_clock_.SetNow(later_date);
141   for (int i = 0; i < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++i) {
142     score_.AddPoints(SiteEngagementScore::GetNavigationPoints());
143     double day_score =
144         std::min(SiteEngagementScore::GetMaxPointsPerDay(),
145                  (i + 1) * SiteEngagementScore::GetNavigationPoints());
146     EXPECT_EQ(day_score + SiteEngagementScore::GetMaxPointsPerDay(),
147               score_.GetTotalScore());
148   }
149 
150   EXPECT_EQ(2 * SiteEngagementScore::GetMaxPointsPerDay(),
151             score_.GetTotalScore());
152 }
153 
154 // Accumulate score on many consecutive days and ensure the score doesn't exceed
155 // the maximum allowed.
TEST_F(SiteEngagementScoreTest,AccumulateALotOnManyDays)156 TEST_F(SiteEngagementScoreTest, AccumulateALotOnManyDays) {
157   base::Time current_day = GetReferenceTime();
158 
159   for (int i = 0; i < kMoreDaysThanNeededToMaxTotalEngagement; ++i) {
160     current_day += base::TimeDelta::FromDays(1);
161     test_clock_.SetNow(current_day);
162     for (int j = 0; j < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++j)
163       score_.AddPoints(SiteEngagementScore::GetNavigationPoints());
164 
165     EXPECT_EQ(std::min(SiteEngagementScore::kMaxPoints,
166                        (i + 1) * SiteEngagementScore::GetMaxPointsPerDay()),
167               score_.GetTotalScore());
168   }
169 
170   EXPECT_EQ(SiteEngagementScore::kMaxPoints, score_.GetTotalScore());
171 }
172 
173 // Accumulate a little on many consecutive days and ensure the score doesn't
174 // exceed the maximum allowed.
TEST_F(SiteEngagementScoreTest,AccumulateALittleOnManyDays)175 TEST_F(SiteEngagementScoreTest, AccumulateALittleOnManyDays) {
176   base::Time current_day = GetReferenceTime();
177 
178   for (int i = 0; i < kMoreAccumulationsThanNeededToMaxTotalEngagement; ++i) {
179     current_day += base::TimeDelta::FromDays(1);
180     test_clock_.SetNow(current_day);
181 
182     for (int j = 0; j < kLessAccumulationsThanNeededToMaxDailyEngagement; ++j)
183       score_.AddPoints(SiteEngagementScore::GetNavigationPoints());
184 
185     EXPECT_EQ(
186         std::min(SiteEngagementScore::kMaxPoints,
187                  (i + 1) * kLessAccumulationsThanNeededToMaxDailyEngagement *
188                      SiteEngagementScore::GetNavigationPoints()),
189         score_.GetTotalScore());
190   }
191 
192   EXPECT_EQ(SiteEngagementScore::kMaxPoints, score_.GetTotalScore());
193 }
194 
195 // Accumulate a bit, then check the score decays properly for a range of times.
TEST_F(SiteEngagementScoreTest,ScoresDecayOverTime)196 TEST_F(SiteEngagementScoreTest, ScoresDecayOverTime) {
197   base::Time current_day = GetReferenceTime();
198 
199   // First max the score.
200   for (int i = 0; i < kMoreDaysThanNeededToMaxTotalEngagement; ++i) {
201     current_day += base::TimeDelta::FromDays(1);
202     test_clock_.SetNow(current_day);
203 
204     for (int j = 0; j < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++j)
205       score_.AddPoints(SiteEngagementScore::GetNavigationPoints());
206   }
207 
208   EXPECT_EQ(SiteEngagementScore::kMaxPoints, score_.GetTotalScore());
209 
210   // The score should not have decayed before the first decay period has
211   // elapsed.
212   test_clock_.SetNow(current_day +
213                      base::TimeDelta::FromHours(
214                          SiteEngagementScore::GetDecayPeriodInHours() - 1));
215   EXPECT_EQ(SiteEngagementScore::kMaxPoints, score_.GetTotalScore());
216 
217   // The score should have decayed by one chunk after one decay period has
218   // elapsed.
219   test_clock_.SetNow(
220       current_day +
221       base::TimeDelta::FromHours(SiteEngagementScore::GetDecayPeriodInHours()));
222   EXPECT_EQ(
223       SiteEngagementScore::kMaxPoints - SiteEngagementScore::GetDecayPoints(),
224       score_.GetTotalScore());
225 
226   // The score should have decayed by the right number of chunks after a few
227   // decay periods have elapsed.
228   test_clock_.SetNow(
229       current_day +
230       base::TimeDelta::FromHours(kLessPeriodsThanNeededToDecayMaxScore *
231                                  SiteEngagementScore::GetDecayPeriodInHours()));
232   EXPECT_EQ(SiteEngagementScore::kMaxPoints -
233                 kLessPeriodsThanNeededToDecayMaxScore *
234                     SiteEngagementScore::GetDecayPoints(),
235             score_.GetTotalScore());
236 
237   // The score should not decay below zero.
238   test_clock_.SetNow(
239       current_day +
240       base::TimeDelta::FromHours(kMorePeriodsThanNeededToDecayMaxScore *
241                                  SiteEngagementScore::GetDecayPeriodInHours()));
242   EXPECT_EQ(0, score_.GetTotalScore());
243 }
244 
245 // Test that any expected decays are applied before adding points.
TEST_F(SiteEngagementScoreTest,DecaysAppliedBeforeAdd)246 TEST_F(SiteEngagementScoreTest, DecaysAppliedBeforeAdd) {
247   base::Time current_day = GetReferenceTime();
248 
249   // Get the score up to something that can handle a bit of decay before
250   for (int i = 0; i < kLessDaysThanNeededToMaxTotalEngagement; ++i) {
251     current_day += base::TimeDelta::FromDays(1);
252     test_clock_.SetNow(current_day);
253 
254     for (int j = 0; j < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++j)
255       score_.AddPoints(SiteEngagementScore::GetNavigationPoints());
256   }
257 
258   double initial_score = kLessDaysThanNeededToMaxTotalEngagement *
259                          SiteEngagementScore::GetMaxPointsPerDay();
260   EXPECT_EQ(initial_score, score_.GetTotalScore());
261 
262   // Go forward a few decay periods.
263   test_clock_.SetNow(
264       current_day +
265       base::TimeDelta::FromHours(kLessPeriodsThanNeededToDecayMaxScore *
266                                  SiteEngagementScore::GetDecayPeriodInHours()));
267 
268   double decayed_score = initial_score -
269                          kLessPeriodsThanNeededToDecayMaxScore *
270                              SiteEngagementScore::GetDecayPoints();
271   EXPECT_EQ(decayed_score, score_.GetTotalScore());
272 
273   // Now add some points.
274   score_.AddPoints(SiteEngagementScore::GetNavigationPoints());
275   EXPECT_EQ(decayed_score + SiteEngagementScore::GetNavigationPoints(),
276             score_.GetTotalScore());
277 }
278 
279 // Test that going back in time is handled properly.
TEST_F(SiteEngagementScoreTest,GoBackInTime)280 TEST_F(SiteEngagementScoreTest, GoBackInTime) {
281   base::Time current_day = GetReferenceTime();
282 
283   test_clock_.SetNow(current_day);
284   for (int i = 0; i < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++i)
285     score_.AddPoints(SiteEngagementScore::GetNavigationPoints());
286 
287   EXPECT_EQ(SiteEngagementScore::GetMaxPointsPerDay(), score_.GetTotalScore());
288 
289   // Adding to the score on an earlier date should be treated like another day,
290   // and should not cause any decay.
291   test_clock_.SetNow(current_day - base::TimeDelta::FromDays(
292                                        kMorePeriodsThanNeededToDecayMaxScore *
293                                        SiteEngagementScore::GetDecayPoints()));
294   for (int i = 0; i < kMoreAccumulationsThanNeededToMaxDailyEngagement; ++i) {
295     score_.AddPoints(SiteEngagementScore::GetNavigationPoints());
296     double day_score =
297         std::min(SiteEngagementScore::GetMaxPointsPerDay(),
298                  (i + 1) * SiteEngagementScore::GetNavigationPoints());
299     EXPECT_EQ(day_score + SiteEngagementScore::GetMaxPointsPerDay(),
300               score_.GetTotalScore());
301   }
302 
303   EXPECT_EQ(2 * SiteEngagementScore::GetMaxPointsPerDay(),
304             score_.GetTotalScore());
305 }
306 
307 // Test that scores are read / written correctly from / to empty score
308 // dictionaries.
TEST_F(SiteEngagementScoreTest,EmptyDictionary)309 TEST_F(SiteEngagementScoreTest, EmptyDictionary) {
310   std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue());
311   TestScoreInitializesAndUpdates(std::move(dict), 0, 0, base::Time());
312 }
313 
314 // Test that scores are read / written correctly from / to partially empty
315 // score dictionaries.
TEST_F(SiteEngagementScoreTest,PartiallyEmptyDictionary)316 TEST_F(SiteEngagementScoreTest, PartiallyEmptyDictionary) {
317   std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue());
318   dict->SetDouble(SiteEngagementScore::kPointsAddedTodayKey, 2);
319 
320   TestScoreInitializesAndUpdates(std::move(dict), 0, 2, base::Time());
321 }
322 
323 // Test that scores are read / written correctly from / to populated score
324 // dictionaries.
TEST_F(SiteEngagementScoreTest,PopulatedDictionary)325 TEST_F(SiteEngagementScoreTest, PopulatedDictionary) {
326   std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue());
327   dict->SetDouble(SiteEngagementScore::kRawScoreKey, 1);
328   dict->SetDouble(SiteEngagementScore::kPointsAddedTodayKey, 2);
329   dict->SetDouble(SiteEngagementScore::kLastEngagementTimeKey,
330                   GetReferenceTime().ToInternalValue());
331 
332   TestScoreInitializesAndUpdates(std::move(dict), 1, 2, GetReferenceTime());
333 }
334 
335 // Ensure bonus engagement is awarded for the first engagement of a day.
TEST_F(SiteEngagementScoreTest,FirstDailyEngagementBonus)336 TEST_F(SiteEngagementScoreTest, FirstDailyEngagementBonus) {
337   SetParamValue(SiteEngagementScore::FIRST_DAILY_ENGAGEMENT, 0.5);
338 
339   SiteEngagementScore score1(&test_clock_, GURL(),
340                              std::unique_ptr<base::DictionaryValue>());
341   SiteEngagementScore score2(&test_clock_, GURL(),
342                              std::unique_ptr<base::DictionaryValue>());
343   base::Time current_day = GetReferenceTime();
344 
345   test_clock_.SetNow(current_day);
346 
347   // The first engagement event gets the bonus.
348   score1.AddPoints(0.5);
349   EXPECT_EQ(1.0, score1.GetTotalScore());
350 
351   // Subsequent events do not.
352   score1.AddPoints(0.5);
353   EXPECT_EQ(1.5, score1.GetTotalScore());
354 
355   // Bonuses are awarded independently between scores.
356   score2.AddPoints(1.0);
357   EXPECT_EQ(1.5, score2.GetTotalScore());
358   score2.AddPoints(1.0);
359   EXPECT_EQ(2.5, score2.GetTotalScore());
360 
361   test_clock_.SetNow(current_day + base::TimeDelta::FromDays(1));
362 
363   // The first event for the next day gets the bonus.
364   score1.AddPoints(0.5);
365   EXPECT_EQ(2.5, score1.GetTotalScore());
366 
367   // Subsequent events do not.
368   score1.AddPoints(0.5);
369   EXPECT_EQ(3.0, score1.GetTotalScore());
370 
371   score2.AddPoints(1.0);
372   EXPECT_EQ(4.0, score2.GetTotalScore());
373   score2.AddPoints(1.0);
374   EXPECT_EQ(5.0, score2.GetTotalScore());
375 }
376 
377 // Test that resetting a score has the correct properties.
TEST_F(SiteEngagementScoreTest,Reset)378 TEST_F(SiteEngagementScoreTest, Reset) {
379   base::Time current_day = GetReferenceTime();
380 
381   test_clock_.SetNow(current_day);
382   score_.AddPoints(SiteEngagementScore::GetNavigationPoints());
383   EXPECT_EQ(SiteEngagementScore::GetNavigationPoints(), score_.GetTotalScore());
384 
385   current_day += base::TimeDelta::FromDays(7);
386   test_clock_.SetNow(current_day);
387 
388   score_.Reset(20.0, current_day);
389   EXPECT_DOUBLE_EQ(20.0, score_.GetTotalScore());
390   EXPECT_DOUBLE_EQ(0, score_.points_added_today_);
391   EXPECT_EQ(current_day, score_.last_engagement_time_);
392   EXPECT_TRUE(score_.last_shortcut_launch_time_.is_null());
393 
394   // Adding points after the reset should work as normal.
395   score_.AddPoints(5);
396   EXPECT_EQ(25.0, score_.GetTotalScore());
397 
398   // The decay should happen one decay period from the current time.
399   test_clock_.SetNow(current_day +
400                      base::TimeDelta::FromHours(
401                          SiteEngagementScore::GetDecayPeriodInHours() + 1));
402   EXPECT_EQ(25.0 - SiteEngagementScore::GetDecayPoints(),
403             score_.GetTotalScore());
404 
405   // Ensure that manually setting a time works as expected.
406   score_.AddPoints(5);
407   test_clock_.SetNow(GetReferenceTime());
408   base::Time now = test_clock_.Now();
409   score_.Reset(10.0, now);
410 
411   EXPECT_DOUBLE_EQ(10.0, score_.GetTotalScore());
412   EXPECT_DOUBLE_EQ(0, score_.points_added_today_);
413   EXPECT_EQ(now, score_.last_engagement_time_);
414   EXPECT_TRUE(score_.last_shortcut_launch_time_.is_null());
415 
416   base::Time old_now = test_clock_.Now();
417 
418   score_.set_last_shortcut_launch_time(test_clock_.Now());
419   test_clock_.SetNow(GetReferenceTime() + base::TimeDelta::FromDays(3));
420   now = test_clock_.Now();
421   score_.Reset(15.0, now);
422 
423   // 5 bonus from the last shortcut launch.
424   EXPECT_DOUBLE_EQ(20.0, score_.GetTotalScore());
425   EXPECT_DOUBLE_EQ(0, score_.points_added_today_);
426   EXPECT_EQ(now, score_.last_engagement_time_);
427   EXPECT_EQ(old_now, score_.last_shortcut_launch_time_);
428 }
429 
430 // Test proportional decay.
TEST_F(SiteEngagementScoreTest,ProportionalDecay)431 TEST_F(SiteEngagementScoreTest, ProportionalDecay) {
432   SetParamValue(SiteEngagementScore::DECAY_PROPORTION, 0.5);
433   SetParamValue(SiteEngagementScore::DECAY_POINTS, 0);
434   SetParamValue(SiteEngagementScore::MAX_POINTS_PER_DAY, 20);
435   base::Time current_day = GetReferenceTime();
436   test_clock_.SetNow(current_day);
437 
438   // Single decay period, expect the score to be halved once.
439   score_.AddPoints(2.0);
440   current_day += base::TimeDelta::FromDays(7);
441   test_clock_.SetNow(current_day);
442   EXPECT_DOUBLE_EQ(1.0, score_.GetTotalScore());
443 
444   // 3 decay periods, expect the score to be halved 3 times.
445   score_.AddPoints(15.0);
446   current_day += base::TimeDelta::FromDays(21);
447   test_clock_.SetNow(current_day);
448   EXPECT_DOUBLE_EQ(2.0, score_.GetTotalScore());
449 
450   // Ensure point removal happens after proportional decay.
451   score_.AddPoints(4.0);
452   EXPECT_DOUBLE_EQ(6.0, score_.GetTotalScore());
453   SetParamValue(SiteEngagementScore::DECAY_POINTS, 2.0);
454   current_day += base::TimeDelta::FromDays(7);
455   test_clock_.SetNow(current_day);
456   EXPECT_NEAR(1.0, score_.GetTotalScore(), kMaxRoundingDeviation);
457 }
458 
459 // Verify that GetDetails fills out all fields correctly.
TEST_F(SiteEngagementScoreTest,GetDetails)460 TEST_F(SiteEngagementScoreTest, GetDetails) {
461   // Advance the clock, otherwise Now() is the same as the null Time value.
462   test_clock_.Advance(base::TimeDelta::FromDays(365));
463 
464   GURL url("http://www.google.com/");
465 
466   // Replace |score_| with one with an actual URL, and with a settings map.
467   HostContentSettingsMap* settings_map =
468       HostContentSettingsMapFactory::GetForProfile(profile());
469   score_ = SiteEngagementScore(&test_clock_, url, settings_map);
470 
471   // Initially all component scores should be zero.
472   mojom::SiteEngagementDetails details = score_.GetDetails();
473   EXPECT_DOUBLE_EQ(0.0, details.total_score);
474   EXPECT_DOUBLE_EQ(0.0, details.installed_bonus);
475   EXPECT_DOUBLE_EQ(0.0, details.base_score);
476   EXPECT_EQ(url, details.origin);
477 
478   // Simulate the app having been launched.
479   score_.set_last_shortcut_launch_time(test_clock_.Now());
480   details = score_.GetDetails();
481   EXPECT_DOUBLE_EQ(details.installed_bonus, details.total_score);
482   EXPECT_LT(0.0, details.installed_bonus);
483   EXPECT_DOUBLE_EQ(0.0, details.base_score);
484 }
485