1// Copyright (c) 2012 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 <utility>
6#include <vector>
7
8#include "base/json/json_reader.h"
9#include "base/message_loop/message_loop.h"
10#include "base/prefs/pref_service.h"
11#include "base/strings/string_number_conversions.h"
12#include "base/strings/string_util.h"
13#include "base/strings/utf_string_conversions.h"
14#include "base/time/time.h"
15#include "base/values.h"
16#include "chrome/browser/browser_process.h"
17#include "chrome/browser/chrome_notification_types.h"
18#include "chrome/browser/prefs/browser_prefs.h"
19#include "chrome/browser/web_resource/notification_promo.h"
20#include "chrome/browser/web_resource/promo_resource_service.h"
21#include "chrome/common/pref_names.h"
22#include "chrome/common/url_constants.h"
23#include "chrome/test/base/scoped_testing_local_state.h"
24#include "chrome/test/base/testing_browser_process.h"
25#include "content/public/browser/notification_registrar.h"
26#include "content/public/browser/notification_service.h"
27#include "net/url_request/test_url_fetcher_factory.h"
28#include "testing/gtest/include/gtest/gtest.h"
29#include "third_party/icu/source/i18n/unicode/smpdtfmt.h"
30
31namespace {
32
33const char kDateFormat[] = "dd MMM yyyy HH:mm:ss zzzz";
34
35bool YearFromNow(double* date_epoch, std::string* date_string) {
36  *date_epoch = (base::Time::Now() + base::TimeDelta::FromDays(365)).ToTimeT();
37
38  UErrorCode status = U_ZERO_ERROR;
39  icu::SimpleDateFormat simple_formatter(icu::UnicodeString(kDateFormat),
40                                         icu::Locale("en_US"),
41                                         status);
42  if (!U_SUCCESS(status))
43    return false;
44
45  icu::UnicodeString date_unicode_string;
46  simple_formatter.format(static_cast<UDate>(*date_epoch * 1000),
47                          date_unicode_string,
48                          status);
49  if (!U_SUCCESS(status))
50    return false;
51
52  return base::UTF16ToUTF8(date_unicode_string.getBuffer(),
53                           static_cast<size_t>(date_unicode_string.length()),
54                           date_string);
55}
56
57}  // namespace
58
59class PromoResourceServiceTest : public testing::Test {
60 public:
61  // |promo_resource_service_| must be created after |local_state_|.
62  PromoResourceServiceTest()
63      : local_state_(TestingBrowserProcess::GetGlobal()),
64        promo_resource_service_(new PromoResourceService) {}
65
66 protected:
67  ScopedTestingLocalState local_state_;
68  scoped_refptr<PromoResourceService> promo_resource_service_;
69  base::MessageLoop loop_;
70};
71
72class NotificationPromoTest {
73 public:
74  NotificationPromoTest()
75      : received_notification_(false),
76        start_(0.0),
77        end_(0.0),
78        num_groups_(0),
79        initial_segment_(0),
80        increment_(1),
81        time_slice_(0),
82        max_group_(0),
83        max_views_(0),
84        closed_(false) {}
85
86  void Init(const std::string& json,
87            const std::string& promo_text,
88            double start,
89            int num_groups, int initial_segment, int increment,
90            int time_slice, int max_group, int max_views) {
91    double year_from_now_epoch;
92    std::string year_from_now_string;
93    ASSERT_TRUE(YearFromNow(&year_from_now_epoch, &year_from_now_string));
94
95    std::vector<std::string> replacements;
96    replacements.push_back(year_from_now_string);
97
98    std::string json_with_end_date(
99        ReplaceStringPlaceholders(json, replacements, NULL));
100    base::Value* value(base::JSONReader::Read(json_with_end_date));
101    ASSERT_TRUE(value);
102
103    base::DictionaryValue* dict = NULL;
104    value->GetAsDictionary(&dict);
105    ASSERT_TRUE(dict);
106    test_json_.reset(dict);
107
108    promo_type_ = NotificationPromo::NTP_NOTIFICATION_PROMO;
109    promo_text_ = promo_text;
110
111    start_ = start;
112    end_ = year_from_now_epoch;
113
114    num_groups_ = num_groups;
115    initial_segment_ = initial_segment;
116    increment_ = increment;
117    time_slice_ = time_slice;
118    max_group_ = max_group;
119
120    max_views_ = max_views;
121
122    closed_ = false;
123    received_notification_ = false;
124  }
125
126  void InitPromoFromJson(bool should_receive_notification) {
127    notification_promo_.InitFromJson(*test_json_, promo_type_);
128    EXPECT_EQ(should_receive_notification,
129              notification_promo_.new_notification());
130
131    // Test the fields.
132    TestNotification();
133  }
134
135  void TestNotification() {
136    // Check values.
137    EXPECT_EQ(notification_promo_.promo_text_, promo_text_);
138
139    EXPECT_EQ(notification_promo_.start_, start_);
140    EXPECT_EQ(notification_promo_.end_, end_);
141
142    EXPECT_EQ(notification_promo_.num_groups_, num_groups_);
143    EXPECT_EQ(notification_promo_.initial_segment_, initial_segment_);
144    EXPECT_EQ(notification_promo_.increment_, increment_);
145    EXPECT_EQ(notification_promo_.time_slice_, time_slice_);
146    EXPECT_EQ(notification_promo_.max_group_, max_group_);
147
148    EXPECT_EQ(notification_promo_.max_views_, max_views_);
149    EXPECT_EQ(notification_promo_.closed_, closed_);
150
151    // Check group within bounds.
152    EXPECT_GE(notification_promo_.group_, 0);
153    EXPECT_LT(notification_promo_.group_, num_groups_);
154
155    // Views should be 0 for now.
156    EXPECT_EQ(notification_promo_.views_, 0);
157  }
158
159  // Create a new NotificationPromo from prefs and compare to current
160  // notification.
161  void TestInitFromPrefs() {
162    NotificationPromo prefs_notification_promo;
163    prefs_notification_promo.InitFromPrefs(promo_type_);
164
165    EXPECT_EQ(notification_promo_.prefs_,
166              prefs_notification_promo.prefs_);
167    EXPECT_EQ(notification_promo_.promo_text_,
168              prefs_notification_promo.promo_text_);
169    EXPECT_EQ(notification_promo_.start_,
170              prefs_notification_promo.start_);
171    EXPECT_EQ(notification_promo_.end_,
172              prefs_notification_promo.end_);
173    EXPECT_EQ(notification_promo_.num_groups_,
174              prefs_notification_promo.num_groups_);
175    EXPECT_EQ(notification_promo_.initial_segment_,
176              prefs_notification_promo.initial_segment_);
177    EXPECT_EQ(notification_promo_.increment_,
178              prefs_notification_promo.increment_);
179    EXPECT_EQ(notification_promo_.time_slice_,
180              prefs_notification_promo.time_slice_);
181    EXPECT_EQ(notification_promo_.max_group_,
182              prefs_notification_promo.max_group_);
183    EXPECT_EQ(notification_promo_.max_views_,
184              prefs_notification_promo.max_views_);
185    EXPECT_EQ(notification_promo_.group_,
186              prefs_notification_promo.group_);
187    EXPECT_EQ(notification_promo_.views_,
188              prefs_notification_promo.views_);
189    EXPECT_EQ(notification_promo_.closed_,
190              prefs_notification_promo.closed_);
191  }
192
193  void TestGroup() {
194    // Test out of range groups.
195    const int incr = num_groups_ / 20;
196    for (int i = max_group_; i < num_groups_; i += incr) {
197      notification_promo_.group_ = i;
198      EXPECT_FALSE(notification_promo_.CanShow());
199    }
200
201    // Test in-range groups.
202    for (int i = 0; i < max_group_; i += incr) {
203      notification_promo_.group_ = i;
204      EXPECT_TRUE(notification_promo_.CanShow());
205    }
206
207    // When max_group_ is 0, all groups pass.
208    notification_promo_.max_group_ = 0;
209    for (int i = 0; i < num_groups_; i += incr) {
210      notification_promo_.group_ = i;
211      EXPECT_TRUE(notification_promo_.CanShow());
212    }
213    notification_promo_.WritePrefs();
214  }
215
216  void TestViews() {
217    notification_promo_.views_ = notification_promo_.max_views_ - 2;
218    notification_promo_.WritePrefs();
219
220    NotificationPromo::HandleViewed(promo_type_);
221    NotificationPromo new_promo;
222    new_promo.InitFromPrefs(promo_type_);
223    EXPECT_EQ(new_promo.max_views_ - 1, new_promo.views_);
224    EXPECT_TRUE(new_promo.CanShow());
225    NotificationPromo::HandleViewed(promo_type_);
226    new_promo.InitFromPrefs(promo_type_);
227    EXPECT_EQ(new_promo.max_views_, new_promo.views_);
228    EXPECT_FALSE(new_promo.CanShow());
229
230    // Test out of range views.
231    for (int i = max_views_; i < max_views_ * 2; ++i) {
232      new_promo.views_ = i;
233      EXPECT_FALSE(new_promo.CanShow());
234    }
235
236    // Test in range views.
237    for (int i = 0; i < max_views_; ++i) {
238      new_promo.views_ = i;
239      EXPECT_TRUE(new_promo.CanShow());
240    }
241    new_promo.WritePrefs();
242  }
243
244  void TestClosed() {
245    NotificationPromo new_promo;
246    new_promo.InitFromPrefs(promo_type_);
247    EXPECT_FALSE(new_promo.closed_);
248    EXPECT_TRUE(new_promo.CanShow());
249
250    NotificationPromo::HandleClosed(promo_type_);
251    new_promo.InitFromPrefs(promo_type_);
252    EXPECT_TRUE(new_promo.closed_);
253    EXPECT_FALSE(new_promo.CanShow());
254
255    new_promo.closed_ = false;
256    EXPECT_TRUE(new_promo.CanShow());
257    new_promo.WritePrefs();
258  }
259
260  void TestPromoText() {
261    notification_promo_.promo_text_.clear();
262    EXPECT_FALSE(notification_promo_.CanShow());
263
264    notification_promo_.promo_text_ = promo_text_;
265    EXPECT_TRUE(notification_promo_.CanShow());
266  }
267
268  void TestTime() {
269    const double now = base::Time::Now().ToDoubleT();
270    const double qhour = 15 * 60;
271
272    notification_promo_.group_ = 0;  // For simplicity.
273
274    notification_promo_.start_ = now - qhour;
275    notification_promo_.end_ = now + qhour;
276    EXPECT_TRUE(notification_promo_.CanShow());
277
278    // Start time has not arrived.
279    notification_promo_.start_ = now + qhour;
280    notification_promo_.end_ = now + qhour;
281    EXPECT_FALSE(notification_promo_.CanShow());
282
283    // End time has past.
284    notification_promo_.start_ = now - qhour;
285    notification_promo_.end_ = now - qhour;
286    EXPECT_FALSE(notification_promo_.CanShow());
287
288    notification_promo_.start_ = start_;
289    notification_promo_.end_ = end_;
290    EXPECT_TRUE(notification_promo_.CanShow());
291  }
292
293  void TestIncrement() {
294    const double now = base::Time::Now().ToDoubleT();
295    const double slice = 60;
296
297    notification_promo_.num_groups_ = 18;
298    notification_promo_.initial_segment_ = 5;
299    notification_promo_.increment_ = 3;
300    notification_promo_.time_slice_ = slice;
301
302    notification_promo_.start_ = now - 1;
303    notification_promo_.end_ = now + slice;
304
305    // Test initial segment.
306    notification_promo_.group_ = 4;
307    EXPECT_TRUE(notification_promo_.CanShow());
308    notification_promo_.group_ = 5;
309    EXPECT_FALSE(notification_promo_.CanShow());
310
311    // Test first increment.
312    notification_promo_.start_ -= slice;
313    notification_promo_.group_ = 7;
314    EXPECT_TRUE(notification_promo_.CanShow());
315    notification_promo_.group_ = 8;
316    EXPECT_FALSE(notification_promo_.CanShow());
317
318    // Test second increment.
319    notification_promo_.start_ -= slice;
320    notification_promo_.group_ = 10;
321    EXPECT_TRUE(notification_promo_.CanShow());
322    notification_promo_.group_ = 11;
323    EXPECT_FALSE(notification_promo_.CanShow());
324
325    // Test penultimate increment.
326    notification_promo_.start_ -= 2 * slice;
327    notification_promo_.group_ = 16;
328    EXPECT_TRUE(notification_promo_.CanShow());
329    notification_promo_.group_ = 17;
330    EXPECT_FALSE(notification_promo_.CanShow());
331
332    // Test last increment.
333    notification_promo_.start_ -= slice;
334    EXPECT_TRUE(notification_promo_.CanShow());
335  }
336
337  const NotificationPromo& promo() const { return notification_promo_; }
338
339 private:
340  NotificationPromo notification_promo_;
341  bool received_notification_;
342  scoped_ptr<base::DictionaryValue> test_json_;
343
344  NotificationPromo::PromoType promo_type_;
345  std::string promo_text_;
346
347  double start_;
348  double end_;
349
350  int num_groups_;
351  int initial_segment_;
352  int increment_;
353  int time_slice_;
354  int max_group_;
355
356  int max_views_;
357
358  bool closed_;
359};
360
361// Test that everything gets parsed correctly, notifications are sent,
362// and CanShow() is handled correctly under variety of conditions.
363// Additionally, test that the first string in |strings| is used if
364// no payload.promo_short_message is specified in the JSON response.
365TEST_F(PromoResourceServiceTest, NotificationPromoTest) {
366  // Check that prefs are set correctly.
367  NotificationPromoTest promo_test;
368
369  // Set up start date and promo line in a Dictionary as if parsed from the
370  // service. date[0].end is replaced with a date 1 year in the future.
371  promo_test.Init("{"
372                  "  \"ntp_notification_promo\": ["
373                  "    {"
374                  "      \"date\":"
375                  "        ["
376                  "          {"
377                  "            \"start\":\"3 Aug 1999 9:26:06 GMT\","
378                  "            \"end\":\"$1\""
379                  "          }"
380                  "        ],"
381                  "      \"strings\":"
382                  "        {"
383                  "          \"NTP4_HOW_DO_YOU_FEEL_ABOUT_CHROME\":"
384                  "              \"What do you think of Chrome?\""
385                  "        },"
386                  "      \"grouping\":"
387                  "        {"
388                  "          \"buckets\":1000,"
389                  "          \"segment\":200,"
390                  "          \"increment\":100,"
391                  "          \"increment_frequency\":3600,"
392                  "          \"increment_max\":400"
393                  "        },"
394                  "      \"payload\":"
395                  "        {"
396                  "          \"days_active\":7,"
397                  "          \"install_age_days\":21"
398                  "        },"
399                  "      \"max_views\":30"
400                  "    }"
401                  "  ]"
402                  "}",
403                  "What do you think of Chrome?",
404                  // The starting date is in 1999 to make tests pass
405                  // on Android devices with incorrect or unset date/time.
406                  933672366,  // unix epoch for 3 Aug 1999 9:26:06 GMT.
407                  1000, 200, 100, 3600, 400, 30);
408
409  promo_test.InitPromoFromJson(true);
410
411  // Second time should not trigger a notification.
412  promo_test.InitPromoFromJson(false);
413
414  promo_test.TestInitFromPrefs();
415
416  // Test various conditions of CanShow.
417  // TestGroup Has the side effect of setting us to a passing group.
418  promo_test.TestGroup();
419  promo_test.TestViews();
420  promo_test.TestClosed();
421  promo_test.TestPromoText();
422  promo_test.TestTime();
423  promo_test.TestIncrement();
424}
425
426// Test that payload.promo_message_short is used if present.
427TEST_F(PromoResourceServiceTest, NotificationPromoCompatNoStringsTest) {
428  // Check that prefs are set correctly.
429  NotificationPromoTest promo_test;
430
431  // Set up start date and promo line in a Dictionary as if parsed from the
432  // service. date[0].end is replaced with a date 1 year in the future.
433  promo_test.Init("{"
434                  "  \"ntp_notification_promo\": ["
435                  "    {"
436                  "      \"date\":"
437                  "        ["
438                  "          {"
439                  "            \"start\":\"3 Aug 1999 9:26:06 GMT\","
440                  "            \"end\":\"$1\""
441                  "          }"
442                  "        ],"
443                  "      \"grouping\":"
444                  "        {"
445                  "          \"buckets\":1000,"
446                  "          \"segment\":200,"
447                  "          \"increment\":100,"
448                  "          \"increment_frequency\":3600,"
449                  "          \"increment_max\":400"
450                  "        },"
451                  "      \"payload\":"
452                  "        {"
453                  "          \"promo_message_short\":"
454                  "              \"What do you think of Chrome?\","
455                  "          \"days_active\":7,"
456                  "          \"install_age_days\":21"
457                  "        },"
458                  "      \"max_views\":30"
459                  "    }"
460                  "  ]"
461                  "}",
462                  "What do you think of Chrome?",
463                  // The starting date is in 1999 to make tests pass
464                  // on Android devices with incorrect or unset date/time.
465                  933672366,  // unix epoch for 3 Aug 1999 9:26:06 GMT.
466                  1000, 200, 100, 3600, 400, 30);
467
468  promo_test.InitPromoFromJson(true);
469  // Second time should not trigger a notification.
470  promo_test.InitPromoFromJson(false);
471  promo_test.TestInitFromPrefs();
472}
473
474// Test that strings.|payload.promo_message_short| is used if present.
475TEST_F(PromoResourceServiceTest, NotificationPromoCompatPayloadStringsTest) {
476  // Check that prefs are set correctly.
477  NotificationPromoTest promo_test;
478
479  // Set up start date and promo line in a Dictionary as if parsed from the
480  // service. date[0].end is replaced with a date 1 year in the future.
481  promo_test.Init("{"
482                  "  \"ntp_notification_promo\": ["
483                  "    {"
484                  "      \"date\":"
485                  "        ["
486                  "          {"
487                  "            \"start\":\"3 Aug 1999 9:26:06 GMT\","
488                  "            \"end\":\"$1\""
489                  "          }"
490                  "        ],"
491                  "      \"grouping\":"
492                  "        {"
493                  "          \"buckets\":1000,"
494                  "          \"segment\":200,"
495                  "          \"increment\":100,"
496                  "          \"increment_frequency\":3600,"
497                  "          \"increment_max\":400"
498                  "        },"
499                  "      \"strings\":"
500                  "        {"
501                  "          \"bogus\":\"string\","
502                  "          \"GOOD_STRING\":"
503                  "              \"What do you think of Chrome?\""
504                  "        },"
505                  "      \"payload\":"
506                  "        {"
507                  "          \"promo_message_short\":"
508                  "              \"GOOD_STRING\","
509                  "          \"days_active\":7,"
510                  "          \"install_age_days\":21"
511                  "        },"
512                  "      \"max_views\":30"
513                  "    }"
514                  "  ]"
515                  "}",
516                  "What do you think of Chrome?",
517                  // The starting date is in 1999 to make tests pass
518                  // on Android devices with incorrect or unset date/time.
519                  933672366,  // unix epoch for 3 Aug 1999 9:26:06 GMT.
520                  1000, 200, 100, 3600, 400, 30);
521
522  promo_test.InitPromoFromJson(true);
523  // Second time should not trigger a notification.
524  promo_test.InitPromoFromJson(false);
525  promo_test.TestInitFromPrefs();
526}
527
528TEST_F(PromoResourceServiceTest, PromoServerURLTest) {
529  GURL promo_server_url = NotificationPromo::PromoServerURL();
530  EXPECT_FALSE(promo_server_url.is_empty());
531  EXPECT_TRUE(promo_server_url.is_valid());
532  EXPECT_TRUE(promo_server_url.SchemeIs(url::kHttpsScheme));
533  // TODO(achuith): Test this better.
534}
535
536#if defined(ENABLE_APP_LIST)
537TEST_F(PromoResourceServiceTest, AppLauncherPromoTest) {
538  // Check that prefs are set correctly.
539  NotificationPromoTest promo_test;
540
541  // Set up start date and promo line in a Dictionary as if parsed from the
542  // service. date[0].end is replaced with a date 1 year in the future.
543  promo_test.Init("{"
544                  "  \"ntp_notification_promo\": ["
545                  "    {"
546                  "      \"date\":"
547                  "        ["
548                  "          {"
549                  "            \"start\":\"3 Aug 1999 9:26:06 GMT\","
550                  "            \"end\":\"$1\""
551                  "          }"
552                  "        ],"
553                  "      \"grouping\":"
554                  "        {"
555                  "          \"buckets\":1000,"
556                  "          \"segment\":200,"
557                  "          \"increment\":100,"
558                  "          \"increment_frequency\":3600,"
559                  "          \"increment_max\":400"
560                  "        },"
561                  "      \"payload\":"
562                  "        {"
563                  "          \"promo_message_short\":"
564                  "              \"What do you think of Chrome?\","
565                  "          \"days_active\":7,"
566                  "          \"install_age_days\":21,"
567                  "          \"is_app_launcher_promo\":true"
568                  "        },"
569                  "      \"max_views\":30"
570                  "    }"
571                  "  ]"
572                  "}",
573                  "What do you think of Chrome?",
574                  // The starting date is in 1999 to make tests pass
575                  // on Android devices with incorrect or unset date/time.
576                  933672366,  // unix epoch for 3 Aug 1999 9:26:06 GMT.
577                  1000, 200, 100, 3600, 400, 30);
578  promo_test.InitPromoFromJson(true);
579  local_state_.Get()->SetBoolean(prefs::kAppLauncherIsEnabled, true);
580  EXPECT_FALSE(promo_test.promo().CanShow());
581}
582#endif
583