1 // Copyright 2014 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.
5 #include "components/suggestions/suggestions_service.h"
11 #include "base/bind.h"
12 #include "base/memory/scoped_ptr.h"
13 #include "base/message_loop/message_loop.h"
14 #include "base/metrics/field_trial.h"
15 #include "base/prefs/pref_service.h"
16 #include "base/strings/utf_string_conversions.h"
17 #include "components/suggestions/blacklist_store.h"
18 #include "components/suggestions/image_manager.h"
19 #include "components/suggestions/proto/suggestions.pb.h"
20 #include "components/suggestions/suggestions_store.h"
21 #include "components/variations/entropy_provider.h"
22 #include "components/variations/variations_associated_data.h"
23 #include "net/http/http_response_headers.h"
24 #include "net/http/http_status_code.h"
25 #include "net/url_request/test_url_fetcher_factory.h"
26 #include "net/url_request/url_request_status.h"
27 #include "net/url_request/url_request_test_util.h"
28 #include "testing/gmock/include/gmock/gmock.h"
29 #include "testing/gtest/include/gtest/gtest.h"
33 using ::testing::Return
;
34 using testing::SetArgPointee
;
35 using ::testing::NiceMock
;
36 using ::testing::StrictMock
;
41 const char kFakeSuggestionsURL
[] = "https://mysuggestions.com/proto";
42 const char kFakeSuggestionsCommonParams
[] = "foo=bar";
43 const char kFakeBlacklistPath
[] = "/blacklist";
44 const char kFakeBlacklistUrlParam
[] = "baz";
46 const char kTestTitle
[] = "a title";
47 const char kTestUrl
[] = "http://go.com";
48 const char kBlacklistUrl
[] = "http://blacklist.com";
50 scoped_ptr
<net::FakeURLFetcher
> CreateURLFetcher(
51 const GURL
& url
, net::URLFetcherDelegate
* delegate
,
52 const std::string
& response_data
, net::HttpStatusCode response_code
,
53 net::URLRequestStatus::Status status
) {
54 scoped_ptr
<net::FakeURLFetcher
> fetcher(new net::FakeURLFetcher(
55 url
, delegate
, response_data
, response_code
, status
));
57 if (response_code
== net::HTTP_OK
) {
58 scoped_refptr
<net::HttpResponseHeaders
> download_headers(
59 new net::HttpResponseHeaders(""));
60 download_headers
->AddHeader("Content-Type: text/html");
61 fetcher
->set_response_headers(download_headers
);
63 return fetcher
.Pass();
66 std::string
GetExpectedBlacklistRequestUrl(const GURL
& blacklist_url
) {
67 std::stringstream request_url
;
68 request_url
<< kFakeSuggestionsURL
<< kFakeBlacklistPath
<< "?"
69 << kFakeSuggestionsCommonParams
<< "&" << kFakeBlacklistUrlParam
70 << "=" << net::EscapeQueryParamValue(blacklist_url
.spec(), true);
71 return request_url
.str();
74 // GMock matcher for protobuf equality.
75 MATCHER_P(EqualsProto
, message
, "") {
76 // This implementation assumes protobuf serialization is deterministic, which
77 // is true in practice but technically not something that code is supposed
78 // to rely on. However, it vastly simplifies the implementation.
79 std::string expected_serialized
, actual_serialized
;
80 message
.SerializeToString(&expected_serialized
);
81 arg
.SerializeToString(&actual_serialized
);
82 return expected_serialized
== actual_serialized
;
87 namespace suggestions
{
89 scoped_ptr
<SuggestionsProfile
> CreateSuggestionsProfile() {
90 scoped_ptr
<SuggestionsProfile
> profile(new SuggestionsProfile());
91 ChromeSuggestion
* suggestion
= profile
->add_suggestions();
92 suggestion
->set_title(kTestTitle
);
93 suggestion
->set_url(kTestUrl
);
94 return profile
.Pass();
97 class MockSuggestionsStore
: public suggestions::SuggestionsStore
{
99 MOCK_METHOD1(LoadSuggestions
, bool(SuggestionsProfile
*));
100 MOCK_METHOD1(StoreSuggestions
, bool(const SuggestionsProfile
&));
101 MOCK_METHOD0(ClearSuggestions
, void());
104 class MockImageManager
: public suggestions::ImageManager
{
106 MockImageManager() {}
107 virtual ~MockImageManager() {}
108 MOCK_METHOD1(Initialize
, void(const SuggestionsProfile
&));
109 MOCK_METHOD2(GetImageForURL
,
111 base::Callback
<void(const GURL
&, const SkBitmap
*)>));
114 class MockBlacklistStore
: public suggestions::BlacklistStore
{
116 MOCK_METHOD1(BlacklistUrl
, bool(const GURL
&));
117 MOCK_METHOD1(GetFirstUrlFromBlacklist
, bool(GURL
*));
118 MOCK_METHOD1(RemoveUrl
, bool(const GURL
&));
119 MOCK_METHOD1(FilterSuggestions
, void(SuggestionsProfile
*));
122 class SuggestionsServiceTest
: public testing::Test
{
124 void CheckSuggestionsData(const SuggestionsProfile
& suggestions_profile
) {
125 EXPECT_EQ(1, suggestions_profile
.suggestions_size());
126 EXPECT_EQ(kTestTitle
, suggestions_profile
.suggestions(0).title());
127 EXPECT_EQ(kTestUrl
, suggestions_profile
.suggestions(0).url());
128 ++suggestions_data_check_count_
;
131 void ExpectEmptySuggestionsProfile(const SuggestionsProfile
& profile
) {
132 EXPECT_EQ(0, profile
.suggestions_size());
133 ++suggestions_empty_data_count_
;
136 int suggestions_data_check_count_
;
137 int suggestions_empty_data_count_
;
140 SuggestionsServiceTest()
141 : suggestions_data_check_count_(0),
142 suggestions_empty_data_count_(0),
143 factory_(NULL
, base::Bind(&CreateURLFetcher
)),
144 mock_suggestions_store_(NULL
),
145 mock_thumbnail_manager_(NULL
) {}
147 virtual ~SuggestionsServiceTest() {}
149 virtual void SetUp() OVERRIDE
{
150 request_context_
= new net::TestURLRequestContextGetter(
151 io_message_loop_
.message_loop_proxy());
154 // Enables the "ChromeSuggestions.Group1" field trial.
155 void EnableFieldTrial(const std::string
& url
,
156 const std::string
& common_params
,
157 const std::string
& blacklist_path
,
158 const std::string
& blacklist_url_param
,
159 bool control_group
) {
160 // Clear the existing |field_trial_list_| to avoid firing a DCHECK.
161 field_trial_list_
.reset(NULL
);
162 field_trial_list_
.reset(
163 new base::FieldTrialList(new metrics::SHA1EntropyProvider("foo")));
165 variations::testing::ClearAllVariationParams();
166 std::map
<std::string
, std::string
> params
;
167 params
[kSuggestionsFieldTrialStateParam
] =
168 kSuggestionsFieldTrialStateEnabled
;
170 params
[kSuggestionsFieldTrialControlParam
] =
171 kSuggestionsFieldTrialStateEnabled
;
173 params
[kSuggestionsFieldTrialURLParam
] = url
;
174 params
[kSuggestionsFieldTrialCommonParamsParam
] = common_params
;
175 params
[kSuggestionsFieldTrialBlacklistPathParam
] = blacklist_path
;
176 params
[kSuggestionsFieldTrialBlacklistUrlParam
] = blacklist_url_param
;
177 variations::AssociateVariationParams(kSuggestionsFieldTrialName
, "Group1",
179 field_trial_
= base::FieldTrialList::CreateFieldTrial(
180 kSuggestionsFieldTrialName
, "Group1");
181 field_trial_
->group();
184 // Should not be called more than once per test since it stashes the
185 // SuggestionsStore in |mock_suggestions_store_|.
186 SuggestionsService
* CreateSuggestionsServiceWithMocks() {
187 mock_suggestions_store_
= new StrictMock
<MockSuggestionsStore
>();
188 mock_thumbnail_manager_
= new NiceMock
<MockImageManager
>();
189 mock_blacklist_store_
= new MockBlacklistStore();
190 return new SuggestionsService(
191 request_context_
, scoped_ptr
<SuggestionsStore
>(mock_suggestions_store_
),
192 scoped_ptr
<ImageManager
>(mock_thumbnail_manager_
),
193 scoped_ptr
<BlacklistStore
>(mock_blacklist_store_
));
196 void FetchSuggestionsDataNoTimeoutHelper(bool interleaved_requests
) {
197 // Field trial enabled with a specific suggestions URL.
198 EnableFieldTrial(kFakeSuggestionsURL
, kFakeSuggestionsCommonParams
,
199 kFakeBlacklistPath
, kFakeBlacklistUrlParam
, false);
200 scoped_ptr
<SuggestionsService
> suggestions_service(
201 CreateSuggestionsServiceWithMocks());
202 EXPECT_TRUE(suggestions_service
!= NULL
);
203 scoped_ptr
<SuggestionsProfile
> suggestions_profile(
204 CreateSuggestionsProfile());
206 // Set up net::FakeURLFetcherFactory.
207 std::string expected_url
=
208 (std::string(kFakeSuggestionsURL
) + "?") + kFakeSuggestionsCommonParams
;
209 factory_
.SetFakeResponse(GURL(expected_url
),
210 suggestions_profile
->SerializeAsString(),
211 net::HTTP_OK
, net::URLRequestStatus::SUCCESS
);
213 // Set up expectations on the SuggestionsStore. The number depends on
214 // whether the second request is issued (it won't be issued if the second
215 // fetch occurs before the first request has completed).
216 int expected_count
= interleaved_requests
? 1 : 2;
217 EXPECT_CALL(*mock_suggestions_store_
,
218 StoreSuggestions(EqualsProto(*suggestions_profile
)))
219 .Times(expected_count
)
220 .WillRepeatedly(Return(true));
222 // Expect a call to the blacklist store. Return that there's nothing to
224 EXPECT_CALL(*mock_blacklist_store_
, FilterSuggestions(_
))
225 .Times(expected_count
);
226 EXPECT_CALL(*mock_blacklist_store_
, GetFirstUrlFromBlacklist(_
))
227 .Times(expected_count
)
228 .WillRepeatedly(Return(false));
230 // Send the request. The data will be returned to the callback.
231 suggestions_service
->FetchSuggestionsDataNoTimeout(base::Bind(
232 &SuggestionsServiceTest::CheckSuggestionsData
, base::Unretained(this)));
234 if (!interleaved_requests
)
235 io_message_loop_
.RunUntilIdle(); // Let request complete.
237 // Send the request a second time.
238 suggestions_service
->FetchSuggestionsDataNoTimeout(base::Bind(
239 &SuggestionsServiceTest::CheckSuggestionsData
, base::Unretained(this)));
241 // (Testing only) wait until suggestion fetch is complete.
242 io_message_loop_
.RunUntilIdle();
244 // Ensure that CheckSuggestionsData() ran twice.
245 EXPECT_EQ(2, suggestions_data_check_count_
);
249 base::MessageLoopForIO io_message_loop_
;
250 net::FakeURLFetcherFactory factory_
;
251 // Only used if the SuggestionsService is built with mocks. Not owned.
252 MockSuggestionsStore
* mock_suggestions_store_
;
253 MockImageManager
* mock_thumbnail_manager_
;
254 MockBlacklistStore
* mock_blacklist_store_
;
255 scoped_refptr
<net::TestURLRequestContextGetter
> request_context_
;
258 scoped_ptr
<base::FieldTrialList
> field_trial_list_
;
259 scoped_refptr
<base::FieldTrial
> field_trial_
;
261 DISALLOW_COPY_AND_ASSIGN(SuggestionsServiceTest
);
264 TEST_F(SuggestionsServiceTest
, IsControlGroup
) {
265 // Field trial enabled.
266 EnableFieldTrial("", "", "", "", false);
267 EXPECT_FALSE(SuggestionsService::IsControlGroup());
269 EnableFieldTrial("", "", "", "", true);
270 EXPECT_TRUE(SuggestionsService::IsControlGroup());
273 TEST_F(SuggestionsServiceTest
, FetchSuggestionsDataNoTimeout
) {
274 FetchSuggestionsDataNoTimeoutHelper(false);
277 TEST_F(SuggestionsServiceTest
, FetchSuggestionsDataNoTimeoutInterleaved
) {
278 FetchSuggestionsDataNoTimeoutHelper(true);
281 TEST_F(SuggestionsServiceTest
, FetchSuggestionsDataRequestError
) {
282 // Field trial enabled with a specific suggestions URL.
283 EnableFieldTrial(kFakeSuggestionsURL
, kFakeSuggestionsCommonParams
,
284 kFakeBlacklistPath
, kFakeBlacklistUrlParam
, false);
285 scoped_ptr
<SuggestionsService
> suggestions_service(
286 CreateSuggestionsServiceWithMocks());
287 EXPECT_TRUE(suggestions_service
!= NULL
);
289 // Fake a request error.
290 std::string expected_url
=
291 (std::string(kFakeSuggestionsURL
) + "?") + kFakeSuggestionsCommonParams
;
292 factory_
.SetFakeResponse(GURL(expected_url
), "irrelevant", net::HTTP_OK
,
293 net::URLRequestStatus::FAILED
);
295 // Set up expectations on the SuggestionsStore.
296 EXPECT_CALL(*mock_suggestions_store_
, LoadSuggestions(_
))
297 .WillOnce(Return(true));
299 // Expect a call to the blacklist store. Return that there's nothing to
301 EXPECT_CALL(*mock_blacklist_store_
, FilterSuggestions(_
));
302 EXPECT_CALL(*mock_blacklist_store_
, GetFirstUrlFromBlacklist(_
))
303 .WillOnce(Return(false));
305 // Send the request. Empty data will be returned to the callback.
306 suggestions_service
->FetchSuggestionsData(
307 base::Bind(&SuggestionsServiceTest::ExpectEmptySuggestionsProfile
,
308 base::Unretained(this)));
310 // (Testing only) wait until suggestion fetch is complete.
311 io_message_loop_
.RunUntilIdle();
313 // Ensure that ExpectEmptySuggestionsProfile ran once.
314 EXPECT_EQ(1, suggestions_empty_data_count_
);
317 TEST_F(SuggestionsServiceTest
, FetchSuggestionsDataResponseNotOK
) {
318 // Field trial enabled with a specific suggestions URL.
319 EnableFieldTrial(kFakeSuggestionsURL
, kFakeSuggestionsCommonParams
,
320 kFakeBlacklistPath
, kFakeBlacklistUrlParam
, false);
321 scoped_ptr
<SuggestionsService
> suggestions_service(
322 CreateSuggestionsServiceWithMocks());
323 EXPECT_TRUE(suggestions_service
!= NULL
);
325 // Response code != 200.
326 std::string expected_url
=
327 (std::string(kFakeSuggestionsURL
) + "?") + kFakeSuggestionsCommonParams
;
328 factory_
.SetFakeResponse(GURL(expected_url
), "irrelevant",
329 net::HTTP_BAD_REQUEST
,
330 net::URLRequestStatus::SUCCESS
);
332 // Set up expectations on the SuggestionsStore.
333 EXPECT_CALL(*mock_suggestions_store_
, ClearSuggestions());
335 // Expect a call to the blacklist store. Return that there's nothing to
337 EXPECT_CALL(*mock_blacklist_store_
, GetFirstUrlFromBlacklist(_
))
338 .WillOnce(Return(false));
340 // Send the request. Empty data will be returned to the callback.
341 suggestions_service
->FetchSuggestionsData(
342 base::Bind(&SuggestionsServiceTest::ExpectEmptySuggestionsProfile
,
343 base::Unretained(this)));
345 // (Testing only) wait until suggestion fetch is complete.
346 io_message_loop_
.RunUntilIdle();
348 // Ensure that ExpectEmptySuggestionsProfile ran once.
349 EXPECT_EQ(1, suggestions_empty_data_count_
);
352 TEST_F(SuggestionsServiceTest
, BlacklistURL
) {
353 EnableFieldTrial(kFakeSuggestionsURL
, kFakeSuggestionsCommonParams
,
354 kFakeBlacklistPath
, kFakeBlacklistUrlParam
, false);
355 scoped_ptr
<SuggestionsService
> suggestions_service(
356 CreateSuggestionsServiceWithMocks());
357 EXPECT_TRUE(suggestions_service
!= NULL
);
359 GURL
blacklist_url(kBlacklistUrl
);
360 std::string request_url
= GetExpectedBlacklistRequestUrl(blacklist_url
);
361 scoped_ptr
<SuggestionsProfile
> suggestions_profile(
362 CreateSuggestionsProfile());
363 factory_
.SetFakeResponse(GURL(request_url
),
364 suggestions_profile
->SerializeAsString(),
365 net::HTTP_OK
, net::URLRequestStatus::SUCCESS
);
367 // Set up expectations on the SuggestionsStore.
368 EXPECT_CALL(*mock_suggestions_store_
,
369 StoreSuggestions(EqualsProto(*suggestions_profile
)))
370 .WillOnce(Return(true));
372 // Expected calls to the blacklist store.
373 EXPECT_CALL(*mock_blacklist_store_
, BlacklistUrl(Eq(blacklist_url
)))
374 .WillOnce(Return(true));
375 EXPECT_CALL(*mock_blacklist_store_
, RemoveUrl(Eq(blacklist_url
)))
376 .WillOnce(Return(true));
377 EXPECT_CALL(*mock_blacklist_store_
, FilterSuggestions(_
));
378 EXPECT_CALL(*mock_blacklist_store_
, GetFirstUrlFromBlacklist(_
))
379 .WillOnce(Return(false));
381 // Send the request. The data will be returned to the callback.
382 suggestions_service
->BlacklistURL(
383 blacklist_url
, base::Bind(&SuggestionsServiceTest::CheckSuggestionsData
,
384 base::Unretained(this)));
386 // (Testing only) wait until blacklist request is complete.
387 io_message_loop_
.RunUntilIdle();
389 // Ensure that CheckSuggestionsData() ran once.
390 EXPECT_EQ(1, suggestions_data_check_count_
);
393 // Initial blacklist request fails, triggering a scheduled upload which
395 TEST_F(SuggestionsServiceTest
, BlacklistURLFails
) {
396 EnableFieldTrial(kFakeSuggestionsURL
, kFakeSuggestionsCommonParams
,
397 kFakeBlacklistPath
, kFakeBlacklistUrlParam
, false);
398 scoped_ptr
<SuggestionsService
> suggestions_service(
399 CreateSuggestionsServiceWithMocks());
400 EXPECT_TRUE(suggestions_service
!= NULL
);
401 suggestions_service
->set_blacklist_delay(0); // Don't wait during a test!
402 scoped_ptr
<SuggestionsProfile
> suggestions_profile(
403 CreateSuggestionsProfile());
404 GURL
blacklist_url(kBlacklistUrl
);
406 // Set up behavior for the first call to blacklist.
407 std::string request_url
= GetExpectedBlacklistRequestUrl(blacklist_url
);
408 factory_
.SetFakeResponse(GURL(request_url
), "irrelevant", net::HTTP_OK
,
409 net::URLRequestStatus::FAILED
);
411 // Expectations specific to the first request.
412 EXPECT_CALL(*mock_blacklist_store_
, BlacklistUrl(Eq(blacklist_url
)))
413 .WillOnce(Return(true));
414 EXPECT_CALL(*mock_suggestions_store_
, LoadSuggestions(_
))
415 .WillOnce(DoAll(SetArgPointee
<0>(*suggestions_profile
), Return(true)));
417 // Expectations specific to the second request.
418 EXPECT_CALL(*mock_suggestions_store_
,
419 StoreSuggestions(EqualsProto(*suggestions_profile
)))
420 .WillOnce(Return(true));
421 EXPECT_CALL(*mock_blacklist_store_
, RemoveUrl(Eq(blacklist_url
)))
422 .WillOnce(Return(true));
424 // Expectations pertaining to both requests.
425 EXPECT_CALL(*mock_blacklist_store_
, FilterSuggestions(_
)).Times(2);
426 EXPECT_CALL(*mock_blacklist_store_
, GetFirstUrlFromBlacklist(_
))
427 .WillOnce(Return(true))
428 .WillOnce(DoAll(SetArgPointee
<0>(blacklist_url
), Return(true)))
429 .WillOnce(Return(false));
431 // Send the request. The data will be returned to the callback.
432 suggestions_service
->BlacklistURL(
433 blacklist_url
, base::Bind(&SuggestionsServiceTest::CheckSuggestionsData
,
434 base::Unretained(this)));
436 // The first FakeURLFetcher was created; we can now set up behavior for the
437 // second call to blacklist.
438 factory_
.SetFakeResponse(GURL(request_url
),
439 suggestions_profile
->SerializeAsString(),
440 net::HTTP_OK
, net::URLRequestStatus::SUCCESS
);
442 // (Testing only) wait until both requests are complete.
443 io_message_loop_
.RunUntilIdle();
444 // ... Other task gets posted to the message loop.
445 base::MessageLoop::current()->RunUntilIdle();
446 // ... And completes.
447 io_message_loop_
.RunUntilIdle();
449 // Ensure that CheckSuggestionsData() ran once.
450 EXPECT_EQ(1, suggestions_data_check_count_
);
453 TEST_F(SuggestionsServiceTest
, GetBlacklistedUrl
) {
454 EnableFieldTrial(kFakeSuggestionsURL
, kFakeSuggestionsCommonParams
,
455 kFakeBlacklistPath
, kFakeBlacklistUrlParam
, false);
457 scoped_ptr
<GURL
> request_url
;
458 scoped_ptr
<net::FakeURLFetcher
> fetcher
;
461 // Not a blacklist request.
462 request_url
.reset(new GURL("http://not-blacklisting.com/a?b=c"));
463 fetcher
= CreateURLFetcher(*request_url
, NULL
, "", net::HTTP_OK
,
464 net::URLRequestStatus::SUCCESS
);
465 EXPECT_FALSE(SuggestionsService::GetBlacklistedUrl(*fetcher
, &retrieved_url
));
467 // An actual blacklist request.
468 string blacklisted_url
= "http://blacklisted.com/a?b=c&d=e";
469 string encoded_blacklisted_url
=
470 "http%3A%2F%2Fblacklisted.com%2Fa%3Fb%3Dc%26d%3De";
471 string blacklist_request_prefix
=
472 "https://mysuggestions.com/proto/blacklist?foo=bar&baz=";
474 new GURL(blacklist_request_prefix
+ encoded_blacklisted_url
));
476 fetcher
= CreateURLFetcher(*request_url
, NULL
, "", net::HTTP_OK
,
477 net::URLRequestStatus::SUCCESS
);
478 EXPECT_TRUE(SuggestionsService::GetBlacklistedUrl(*fetcher
, &retrieved_url
));
479 EXPECT_EQ(blacklisted_url
, retrieved_url
.spec());
482 TEST_F(SuggestionsServiceTest
, UpdateBlacklistDelay
) {
483 scoped_ptr
<SuggestionsService
> suggestions_service(
484 CreateSuggestionsServiceWithMocks());
485 int initial_delay
= suggestions_service
->blacklist_delay();
487 // Delay unchanged on success.
488 suggestions_service
->UpdateBlacklistDelay(true);
489 EXPECT_EQ(initial_delay
, suggestions_service
->blacklist_delay());
491 // Delay increases on failure.
492 suggestions_service
->UpdateBlacklistDelay(false);
493 EXPECT_GT(suggestions_service
->blacklist_delay(), initial_delay
);
495 // Delay resets on success.
496 suggestions_service
->UpdateBlacklistDelay(true);
497 EXPECT_EQ(initial_delay
, suggestions_service
->blacklist_delay());
500 } // namespace suggestions