OLD | NEW |
| (Empty) |
1 // Copyright 2015 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 "components/ntp_snippets/remote/remote_suggestions_provider.h" | |
6 | |
7 #include <memory> | |
8 #include <utility> | |
9 #include <vector> | |
10 | |
11 #include "base/command_line.h" | |
12 #include "base/files/file_path.h" | |
13 #include "base/files/scoped_temp_dir.h" | |
14 #include "base/json/json_reader.h" | |
15 #include "base/macros.h" | |
16 #include "base/memory/ptr_util.h" | |
17 #include "base/message_loop/message_loop.h" | |
18 #include "base/run_loop.h" | |
19 #include "base/strings/string_number_conversions.h" | |
20 #include "base/strings/string_util.h" | |
21 #include "base/strings/stringprintf.h" | |
22 #include "base/test/histogram_tester.h" | |
23 #include "base/test/simple_test_clock.h" | |
24 #include "base/threading/thread_task_runner_handle.h" | |
25 #include "base/time/time.h" | |
26 #include "components/image_fetcher/image_decoder.h" | |
27 #include "components/image_fetcher/image_fetcher.h" | |
28 #include "components/image_fetcher/image_fetcher_delegate.h" | |
29 #include "components/ntp_snippets/category.h" | |
30 #include "components/ntp_snippets/category_info.h" | |
31 #include "components/ntp_snippets/category_rankers/category_ranker.h" | |
32 #include "components/ntp_snippets/category_rankers/constant_category_ranker.h" | |
33 #include "components/ntp_snippets/category_rankers/mock_category_ranker.h" | |
34 #include "components/ntp_snippets/ntp_snippets_constants.h" | |
35 #include "components/ntp_snippets/pref_names.h" | |
36 #include "components/ntp_snippets/remote/ntp_snippet.h" | |
37 #include "components/ntp_snippets/remote/ntp_snippets_fetcher.h" | |
38 #include "components/ntp_snippets/remote/ntp_snippets_scheduler.h" | |
39 #include "components/ntp_snippets/remote/remote_suggestions_database.h" | |
40 #include "components/ntp_snippets/remote/test_utils.h" | |
41 #include "components/ntp_snippets/user_classifier.h" | |
42 #include "components/prefs/testing_pref_service.h" | |
43 #include "components/signin/core/browser/fake_profile_oauth2_token_service.h" | |
44 #include "components/signin/core/browser/fake_signin_manager.h" | |
45 #include "components/variations/variations_params_manager.h" | |
46 #include "net/url_request/test_url_fetcher_factory.h" | |
47 #include "net/url_request/url_request_test_util.h" | |
48 #include "testing/gmock/include/gmock/gmock.h" | |
49 #include "testing/gtest/include/gtest/gtest.h" | |
50 #include "ui/gfx/image/image.h" | |
51 #include "ui/gfx/image/image_unittest_util.h" | |
52 | |
53 using image_fetcher::ImageFetcher; | |
54 using image_fetcher::ImageFetcherDelegate; | |
55 using testing::_; | |
56 using testing::ElementsAre; | |
57 using testing::Eq; | |
58 using testing::InSequence; | |
59 using testing::Invoke; | |
60 using testing::IsEmpty; | |
61 using testing::Mock; | |
62 using testing::MockFunction; | |
63 using testing::NiceMock; | |
64 using testing::Not; | |
65 using testing::SaveArg; | |
66 using testing::SizeIs; | |
67 using testing::StartsWith; | |
68 using testing::WithArgs; | |
69 | |
70 namespace ntp_snippets { | |
71 | |
72 namespace { | |
73 | |
74 MATCHER_P(IdEq, value, "") { | |
75 return arg->id() == value; | |
76 } | |
77 | |
78 MATCHER_P(IdWithinCategoryEq, expected_id, "") { | |
79 return arg.id().id_within_category() == expected_id; | |
80 } | |
81 | |
82 MATCHER_P(IsCategory, id, "") { | |
83 return arg.id() == static_cast<int>(id); | |
84 } | |
85 | |
86 MATCHER_P(HasCode, code, "") { | |
87 return arg.code == code; | |
88 } | |
89 | |
90 const base::Time::Exploded kDefaultCreationTime = {2015, 11, 4, 25, 13, 46, 45}; | |
91 const char kTestContentSuggestionsServerEndpoint[] = | |
92 "https://localunittest-chromecontentsuggestions-pa.googleapis.com/v1/" | |
93 "suggestions/fetch"; | |
94 const char kAPIKey[] = "fakeAPIkey"; | |
95 const char kTestContentSuggestionsServerWithAPIKey[] = | |
96 "https://localunittest-chromecontentsuggestions-pa.googleapis.com/v1/" | |
97 "suggestions/fetch?key=fakeAPIkey"; | |
98 | |
99 const char kSnippetUrl[] = "http://localhost/foobar"; | |
100 const char kSnippetTitle[] = "Title"; | |
101 const char kSnippetText[] = "Snippet"; | |
102 const char kSnippetSalientImage[] = "http://localhost/salient_image"; | |
103 const char kSnippetPublisherName[] = "Foo News"; | |
104 const char kSnippetAmpUrl[] = "http://localhost/amp"; | |
105 | |
106 const char kSnippetUrl2[] = "http://foo.com/bar"; | |
107 | |
108 const char kTestJsonDefaultCategoryTitle[] = "Some title"; | |
109 | |
110 const int kUnknownRemoteCategoryId = 1234; | |
111 | |
112 base::Time GetDefaultCreationTime() { | |
113 base::Time out_time; | |
114 EXPECT_TRUE(base::Time::FromUTCExploded(kDefaultCreationTime, &out_time)); | |
115 return out_time; | |
116 } | |
117 | |
118 base::Time GetDefaultExpirationTime() { | |
119 return base::Time::Now() + base::TimeDelta::FromHours(1); | |
120 } | |
121 | |
122 std::string GetCategoryJson(const std::vector<std::string>& snippets, | |
123 int remote_category_id, | |
124 const std::string& category_title) { | |
125 return base::StringPrintf( | |
126 " {\n" | |
127 " \"id\": %d,\n" | |
128 " \"localizedTitle\": \"%s\",\n" | |
129 " \"suggestions\": [%s]\n" | |
130 " }\n", | |
131 remote_category_id, category_title.c_str(), | |
132 base::JoinString(snippets, ", ").c_str()); | |
133 } | |
134 | |
135 class MultiCategoryJsonBuilder { | |
136 public: | |
137 MultiCategoryJsonBuilder() {} | |
138 | |
139 MultiCategoryJsonBuilder& AddCategoryWithCustomTitle( | |
140 const std::vector<std::string>& snippets, | |
141 int remote_category_id, | |
142 const std::string& category_title) { | |
143 category_json_.push_back( | |
144 GetCategoryJson(snippets, remote_category_id, category_title)); | |
145 return *this; | |
146 } | |
147 | |
148 MultiCategoryJsonBuilder& AddCategory( | |
149 const std::vector<std::string>& snippets, | |
150 int remote_category_id) { | |
151 return AddCategoryWithCustomTitle( | |
152 snippets, remote_category_id, | |
153 "Title" + base::IntToString(remote_category_id)); | |
154 } | |
155 | |
156 std::string Build() { | |
157 return base::StringPrintf( | |
158 "{\n" | |
159 " \"categories\": [\n" | |
160 "%s\n" | |
161 " ]\n" | |
162 "}\n", | |
163 base::JoinString(category_json_, " ,\n").c_str()); | |
164 } | |
165 | |
166 private: | |
167 std::vector<std::string> category_json_; | |
168 }; | |
169 | |
170 // TODO(vitaliii): Remove these convenience functions as they do not provide | |
171 // that much value and add additional redirections obscuring the code. | |
172 std::string GetTestJson(const std::vector<std::string>& snippets, | |
173 const std::string& category_title) { | |
174 return MultiCategoryJsonBuilder() | |
175 .AddCategoryWithCustomTitle(snippets, /*remote_category_id=*/1, | |
176 category_title) | |
177 .Build(); | |
178 } | |
179 | |
180 std::string GetTestJson(const std::vector<std::string>& snippets) { | |
181 return GetTestJson(snippets, kTestJsonDefaultCategoryTitle); | |
182 } | |
183 | |
184 std::string FormatTime(const base::Time& t) { | |
185 base::Time::Exploded x; | |
186 t.UTCExplode(&x); | |
187 return base::StringPrintf("%04d-%02d-%02dT%02d:%02d:%02dZ", x.year, x.month, | |
188 x.day_of_month, x.hour, x.minute, x.second); | |
189 } | |
190 | |
191 std::string GetSnippetWithUrlAndTimesAndSource( | |
192 const std::vector<std::string>& ids, | |
193 const std::string& url, | |
194 const base::Time& creation_time, | |
195 const base::Time& expiry_time, | |
196 const std::string& publisher, | |
197 const std::string& amp_url) { | |
198 const std::string ids_string = base::JoinString(ids, "\",\n \""); | |
199 return base::StringPrintf( | |
200 "{\n" | |
201 " \"ids\": [\n" | |
202 " \"%s\"\n" | |
203 " ],\n" | |
204 " \"title\": \"%s\",\n" | |
205 " \"snippet\": \"%s\",\n" | |
206 " \"fullPageUrl\": \"%s\",\n" | |
207 " \"creationTime\": \"%s\",\n" | |
208 " \"expirationTime\": \"%s\",\n" | |
209 " \"attribution\": \"%s\",\n" | |
210 " \"imageUrl\": \"%s\",\n" | |
211 " \"ampUrl\": \"%s\"\n" | |
212 " }", | |
213 ids_string.c_str(), kSnippetTitle, kSnippetText, url.c_str(), | |
214 FormatTime(creation_time).c_str(), FormatTime(expiry_time).c_str(), | |
215 publisher.c_str(), kSnippetSalientImage, amp_url.c_str()); | |
216 } | |
217 | |
218 std::string GetSnippetWithSources(const std::string& source_url, | |
219 const std::string& publisher, | |
220 const std::string& amp_url) { | |
221 return GetSnippetWithUrlAndTimesAndSource( | |
222 {kSnippetUrl}, source_url, GetDefaultCreationTime(), | |
223 GetDefaultExpirationTime(), publisher, amp_url); | |
224 } | |
225 | |
226 std::string GetSnippetWithUrlAndTimes(const std::string& url, | |
227 const base::Time& content_creation_time, | |
228 const base::Time& expiry_time) { | |
229 return GetSnippetWithUrlAndTimesAndSource({url}, url, content_creation_time, | |
230 expiry_time, kSnippetPublisherName, | |
231 kSnippetAmpUrl); | |
232 } | |
233 | |
234 std::string GetSnippetWithTimes(const base::Time& content_creation_time, | |
235 const base::Time& expiry_time) { | |
236 return GetSnippetWithUrlAndTimes(kSnippetUrl, content_creation_time, | |
237 expiry_time); | |
238 } | |
239 | |
240 std::string GetSnippetWithUrl(const std::string& url) { | |
241 return GetSnippetWithUrlAndTimes(url, GetDefaultCreationTime(), | |
242 GetDefaultExpirationTime()); | |
243 } | |
244 | |
245 std::string GetSnippet() { | |
246 return GetSnippetWithUrlAndTimes(kSnippetUrl, GetDefaultCreationTime(), | |
247 GetDefaultExpirationTime()); | |
248 } | |
249 | |
250 std::string GetSnippetN(int n) { | |
251 return GetSnippetWithUrlAndTimes(base::StringPrintf("%s/%d", kSnippetUrl, n), | |
252 GetDefaultCreationTime(), | |
253 GetDefaultExpirationTime()); | |
254 } | |
255 | |
256 std::string GetExpiredSnippet() { | |
257 return GetSnippetWithTimes(GetDefaultCreationTime(), base::Time::Now()); | |
258 } | |
259 | |
260 std::string GetInvalidSnippet() { | |
261 std::string json_str = GetSnippet(); | |
262 // Make the json invalid by removing the final closing brace. | |
263 return json_str.substr(0, json_str.size() - 1); | |
264 } | |
265 | |
266 std::string GetIncompleteSnippet() { | |
267 std::string json_str = GetSnippet(); | |
268 // Rename the "url" entry. The result is syntactically valid json that will | |
269 // fail to parse as snippets. | |
270 size_t pos = json_str.find("\"fullPageUrl\""); | |
271 if (pos == std::string::npos) { | |
272 NOTREACHED(); | |
273 return std::string(); | |
274 } | |
275 json_str[pos + 1] = 'x'; | |
276 return json_str; | |
277 } | |
278 | |
279 using ServeImageCallback = base::Callback<void( | |
280 const std::string&, | |
281 base::Callback<void(const std::string&, const gfx::Image&)>)>; | |
282 | |
283 void ServeOneByOneImage( | |
284 image_fetcher::ImageFetcherDelegate* notify, | |
285 const std::string& id, | |
286 base::Callback<void(const std::string&, const gfx::Image&)> callback) { | |
287 base::ThreadTaskRunnerHandle::Get()->PostTask( | |
288 FROM_HERE, base::Bind(callback, id, gfx::test::CreateImage(1, 1))); | |
289 notify->OnImageDataFetched(id, "1-by-1-image-data"); | |
290 } | |
291 | |
292 gfx::Image FetchImage(RemoteSuggestionsProvider* service, | |
293 const ContentSuggestion::ID& suggestion_id) { | |
294 gfx::Image result; | |
295 base::RunLoop run_loop; | |
296 service->FetchSuggestionImage(suggestion_id, | |
297 base::Bind( | |
298 [](base::Closure signal, gfx::Image* output, | |
299 const gfx::Image& loaded) { | |
300 *output = loaded; | |
301 signal.Run(); | |
302 }, | |
303 run_loop.QuitClosure(), &result)); | |
304 run_loop.Run(); | |
305 return result; | |
306 } | |
307 | |
308 void ParseJson(const std::string& json, | |
309 const SuccessCallback& success_callback, | |
310 const ErrorCallback& error_callback) { | |
311 base::JSONReader json_reader; | |
312 std::unique_ptr<base::Value> value = json_reader.ReadToValue(json); | |
313 if (value) { | |
314 success_callback.Run(std::move(value)); | |
315 } else { | |
316 error_callback.Run(json_reader.GetErrorMessage()); | |
317 } | |
318 } | |
319 | |
320 // Factory for FakeURLFetcher objects that always generate errors. | |
321 class FailingFakeURLFetcherFactory : public net::URLFetcherFactory { | |
322 public: | |
323 std::unique_ptr<net::URLFetcher> CreateURLFetcher( | |
324 int id, | |
325 const GURL& url, | |
326 net::URLFetcher::RequestType request_type, | |
327 net::URLFetcherDelegate* d) override { | |
328 return base::MakeUnique<net::FakeURLFetcher>( | |
329 url, d, /*response_data=*/std::string(), net::HTTP_NOT_FOUND, | |
330 net::URLRequestStatus::FAILED); | |
331 } | |
332 }; | |
333 | |
334 class MockScheduler : public NTPSnippetsScheduler { | |
335 public: | |
336 MOCK_METHOD2(Schedule, | |
337 bool(base::TimeDelta period_wifi, | |
338 base::TimeDelta period_fallback)); | |
339 MOCK_METHOD0(Unschedule, bool()); | |
340 }; | |
341 | |
342 class MockImageFetcher : public ImageFetcher { | |
343 public: | |
344 MOCK_METHOD1(SetImageFetcherDelegate, void(ImageFetcherDelegate*)); | |
345 MOCK_METHOD1(SetDataUseServiceName, void(DataUseServiceName)); | |
346 MOCK_METHOD3( | |
347 StartOrQueueNetworkRequest, | |
348 void(const std::string&, | |
349 const GURL&, | |
350 base::Callback<void(const std::string&, const gfx::Image&)>)); | |
351 }; | |
352 | |
353 class FakeContentSuggestionsProviderObserver | |
354 : public ContentSuggestionsProvider::Observer { | |
355 public: | |
356 FakeContentSuggestionsProviderObserver() = default; | |
357 | |
358 void OnNewSuggestions(ContentSuggestionsProvider* provider, | |
359 Category category, | |
360 std::vector<ContentSuggestion> suggestions) override { | |
361 suggestions_[category] = std::move(suggestions); | |
362 } | |
363 | |
364 void OnCategoryStatusChanged(ContentSuggestionsProvider* provider, | |
365 Category category, | |
366 CategoryStatus new_status) override { | |
367 statuses_[category] = new_status; | |
368 } | |
369 | |
370 void OnSuggestionInvalidated( | |
371 ContentSuggestionsProvider* provider, | |
372 const ContentSuggestion::ID& suggestion_id) override {} | |
373 | |
374 const std::map<Category, CategoryStatus, Category::CompareByID>& statuses() | |
375 const { | |
376 return statuses_; | |
377 } | |
378 | |
379 CategoryStatus StatusForCategory(Category category) const { | |
380 auto it = statuses_.find(category); | |
381 if (it == statuses_.end()) { | |
382 return CategoryStatus::NOT_PROVIDED; | |
383 } | |
384 return it->second; | |
385 } | |
386 | |
387 const std::vector<ContentSuggestion>& SuggestionsForCategory( | |
388 Category category) { | |
389 return suggestions_[category]; | |
390 } | |
391 | |
392 private: | |
393 std::map<Category, CategoryStatus, Category::CompareByID> statuses_; | |
394 std::map<Category, std::vector<ContentSuggestion>, Category::CompareByID> | |
395 suggestions_; | |
396 | |
397 DISALLOW_COPY_AND_ASSIGN(FakeContentSuggestionsProviderObserver); | |
398 }; | |
399 | |
400 class FakeImageDecoder : public image_fetcher::ImageDecoder { | |
401 public: | |
402 FakeImageDecoder() {} | |
403 ~FakeImageDecoder() override = default; | |
404 void DecodeImage( | |
405 const std::string& image_data, | |
406 const image_fetcher::ImageDecodedCallback& callback) override { | |
407 callback.Run(decoded_image_); | |
408 } | |
409 | |
410 void SetDecodedImage(const gfx::Image& image) { decoded_image_ = image; } | |
411 | |
412 private: | |
413 gfx::Image decoded_image_; | |
414 }; | |
415 | |
416 } // namespace | |
417 | |
418 class RemoteSuggestionsProviderTest : public ::testing::Test { | |
419 public: | |
420 RemoteSuggestionsProviderTest() | |
421 : params_manager_(ntp_snippets::kStudyName, | |
422 {{"content_suggestions_backend", | |
423 kTestContentSuggestionsServerEndpoint}, | |
424 {"fetching_personalization", "non_personal"}}), | |
425 fake_url_fetcher_factory_( | |
426 /*default_factory=*/&failing_url_fetcher_factory_), | |
427 test_url_(kTestContentSuggestionsServerWithAPIKey), | |
428 category_ranker_(base::MakeUnique<ConstantCategoryRanker>()), | |
429 user_classifier_(/*pref_service=*/nullptr), | |
430 image_fetcher_(nullptr), | |
431 image_decoder_(nullptr), | |
432 database_(nullptr) { | |
433 RemoteSuggestionsProvider::RegisterProfilePrefs( | |
434 utils_.pref_service()->registry()); | |
435 RequestThrottler::RegisterProfilePrefs(utils_.pref_service()->registry()); | |
436 | |
437 EXPECT_TRUE(database_dir_.CreateUniqueTempDir()); | |
438 } | |
439 | |
440 ~RemoteSuggestionsProviderTest() override { | |
441 // We need to run the message loop after deleting the database, because | |
442 // ProtoDatabaseImpl deletes the actual LevelDB asynchronously on the task | |
443 // runner. Without this, we'd get reports of memory leaks. | |
444 base::RunLoop().RunUntilIdle(); | |
445 } | |
446 | |
447 // TODO(vitaliii): Rewrite this function to initialize a test class member | |
448 // instead of creating a new service. | |
449 std::unique_ptr<RemoteSuggestionsProvider> MakeSnippetsService( | |
450 bool set_empty_response = true) { | |
451 auto service = MakeSnippetsServiceWithoutInitialization(); | |
452 WaitForSnippetsServiceInitialization(service.get(), set_empty_response); | |
453 return service; | |
454 } | |
455 | |
456 std::unique_ptr<RemoteSuggestionsProvider> | |
457 MakeSnippetsServiceWithoutInitialization() { | |
458 scoped_refptr<base::SingleThreadTaskRunner> task_runner( | |
459 base::ThreadTaskRunnerHandle::Get()); | |
460 scoped_refptr<net::TestURLRequestContextGetter> request_context_getter = | |
461 new net::TestURLRequestContextGetter(task_runner.get()); | |
462 | |
463 utils_.ResetSigninManager(); | |
464 std::unique_ptr<NTPSnippetsFetcher> snippets_fetcher = | |
465 base::MakeUnique<NTPSnippetsFetcher>( | |
466 utils_.fake_signin_manager(), fake_token_service_.get(), | |
467 std::move(request_context_getter), utils_.pref_service(), nullptr, | |
468 base::Bind(&ParseJson), kAPIKey, &user_classifier_); | |
469 | |
470 utils_.fake_signin_manager()->SignIn("foo@bar.com"); | |
471 | |
472 auto image_fetcher = base::MakeUnique<NiceMock<MockImageFetcher>>(); | |
473 | |
474 image_fetcher_ = image_fetcher.get(); | |
475 EXPECT_CALL(*image_fetcher, SetImageFetcherDelegate(_)); | |
476 auto image_decoder = base::MakeUnique<FakeImageDecoder>(); | |
477 image_decoder_ = image_decoder.get(); | |
478 EXPECT_FALSE(observer_); | |
479 observer_ = base::MakeUnique<FakeContentSuggestionsProviderObserver>(); | |
480 auto database = base::MakeUnique<RemoteSuggestionsDatabase>( | |
481 database_dir_.GetPath(), task_runner); | |
482 database_ = database.get(); | |
483 return base::MakeUnique<RemoteSuggestionsProvider>( | |
484 observer_.get(), utils_.pref_service(), "fr", category_ranker_.get(), | |
485 &user_classifier_, &scheduler_, std::move(snippets_fetcher), | |
486 std::move(image_fetcher), std::move(image_decoder), std::move(database), | |
487 base::MakeUnique<RemoteSuggestionsStatusService>( | |
488 utils_.fake_signin_manager(), utils_.pref_service())); | |
489 } | |
490 | |
491 void WaitForSnippetsServiceInitialization(RemoteSuggestionsProvider* service, | |
492 bool set_empty_response) { | |
493 EXPECT_EQ(RemoteSuggestionsProvider::State::NOT_INITED, service->state_); | |
494 | |
495 // Add an initial fetch response, as the service tries to fetch when there | |
496 // is nothing in the DB. | |
497 if (set_empty_response) { | |
498 SetUpFetchResponse(GetTestJson(std::vector<std::string>())); | |
499 } | |
500 | |
501 // TODO(treib): Find a better way to wait for initialization to finish. | |
502 base::RunLoop().RunUntilIdle(); | |
503 EXPECT_NE(RemoteSuggestionsProvider::State::NOT_INITED, service->state_); | |
504 } | |
505 | |
506 void ResetSnippetsService(std::unique_ptr<RemoteSuggestionsProvider>* service, | |
507 bool set_empty_response) { | |
508 service->reset(); | |
509 observer_.reset(); | |
510 *service = MakeSnippetsService(set_empty_response); | |
511 } | |
512 | |
513 void SetCategoryRanker(std::unique_ptr<CategoryRanker> category_ranker) { | |
514 category_ranker_ = std::move(category_ranker); | |
515 } | |
516 | |
517 ContentSuggestion::ID MakeArticleID(const std::string& id_within_category) { | |
518 return ContentSuggestion::ID(articles_category(), id_within_category); | |
519 } | |
520 | |
521 Category articles_category() { | |
522 return Category::FromKnownCategory(KnownCategories::ARTICLES); | |
523 } | |
524 | |
525 ContentSuggestion::ID MakeOtherID(const std::string& id_within_category) { | |
526 return ContentSuggestion::ID(other_category(), id_within_category); | |
527 } | |
528 | |
529 // TODO(tschumann): Get rid of the convenience other_category() and | |
530 // unknown_category() helpers -- tests can just define their own. | |
531 Category other_category() { return Category::FromRemoteCategory(2); } | |
532 | |
533 Category unknown_category() { | |
534 return Category::FromRemoteCategory(kUnknownRemoteCategoryId); | |
535 } | |
536 | |
537 protected: | |
538 const GURL& test_url() { return test_url_; } | |
539 FakeContentSuggestionsProviderObserver& observer() { return *observer_; } | |
540 MockScheduler& mock_scheduler() { return scheduler_; } | |
541 // TODO(tschumann): Make this a strict-mock. We want to avoid unneccesary | |
542 // network requests. | |
543 NiceMock<MockImageFetcher>* image_fetcher() { return image_fetcher_; } | |
544 FakeImageDecoder* image_decoder() { return image_decoder_; } | |
545 PrefService* pref_service() { return utils_.pref_service(); } | |
546 RemoteSuggestionsDatabase* database() { return database_; } | |
547 | |
548 // Provide the json to be returned by the fake fetcher. | |
549 void SetUpFetchResponse(const std::string& json) { | |
550 fake_url_fetcher_factory_.SetFakeResponse(test_url_, json, net::HTTP_OK, | |
551 net::URLRequestStatus::SUCCESS); | |
552 } | |
553 | |
554 // Have the fake fetcher fail due to a HTTP error like a 404. | |
555 void SetUpHttpError() { | |
556 fake_url_fetcher_factory_.SetFakeResponse(test_url_, /*json=*/std::string(), | |
557 net::HTTP_NOT_FOUND, | |
558 net::URLRequestStatus::SUCCESS); | |
559 } | |
560 | |
561 void LoadFromJSONString(RemoteSuggestionsProvider* service, | |
562 const std::string& json) { | |
563 SetUpFetchResponse(json); | |
564 service->FetchSnippets(true); | |
565 base::RunLoop().RunUntilIdle(); | |
566 } | |
567 | |
568 void LoadMoreFromJSONString(RemoteSuggestionsProvider* service, | |
569 const Category& category, | |
570 const std::string& json, | |
571 const std::set<std::string>& known_ids, | |
572 FetchDoneCallback callback) { | |
573 SetUpFetchResponse(json); | |
574 service->Fetch(category, known_ids, callback); | |
575 base::RunLoop().RunUntilIdle(); | |
576 } | |
577 | |
578 private: | |
579 variations::testing::VariationParamsManager params_manager_; | |
580 test::RemoteSuggestionsTestUtils utils_; | |
581 base::MessageLoop message_loop_; | |
582 FailingFakeURLFetcherFactory failing_url_fetcher_factory_; | |
583 // Instantiation of factory automatically sets itself as URLFetcher's factory. | |
584 net::FakeURLFetcherFactory fake_url_fetcher_factory_; | |
585 const GURL test_url_; | |
586 std::unique_ptr<OAuth2TokenService> fake_token_service_; | |
587 std::unique_ptr<CategoryRanker> category_ranker_; | |
588 UserClassifier user_classifier_; | |
589 NiceMock<MockScheduler> scheduler_; | |
590 std::unique_ptr<FakeContentSuggestionsProviderObserver> observer_; | |
591 NiceMock<MockImageFetcher>* image_fetcher_; | |
592 FakeImageDecoder* image_decoder_; | |
593 | |
594 base::ScopedTempDir database_dir_; | |
595 RemoteSuggestionsDatabase* database_; | |
596 | |
597 DISALLOW_COPY_AND_ASSIGN(RemoteSuggestionsProviderTest); | |
598 }; | |
599 | |
600 TEST_F(RemoteSuggestionsProviderTest, ScheduleOnStart) { | |
601 // We should get two |Schedule| calls: The first when initialization | |
602 // completes, the second one after the automatic (since the service doesn't | |
603 // have any data yet) fetch finishes. | |
604 EXPECT_CALL(mock_scheduler(), Schedule(_, _)).Times(2); | |
605 EXPECT_CALL(mock_scheduler(), Unschedule()).Times(0); | |
606 auto service = MakeSnippetsService(); | |
607 | |
608 // When we have no snippets are all, loading the service initiates a fetch. | |
609 EXPECT_EQ("OK", service->snippets_fetcher()->last_status()); | |
610 } | |
611 | |
612 TEST_F(RemoteSuggestionsProviderTest, DontRescheduleOnStart) { | |
613 EXPECT_CALL(mock_scheduler(), Schedule(_, _)).Times(2); | |
614 EXPECT_CALL(mock_scheduler(), Unschedule()).Times(0); | |
615 SetUpFetchResponse(GetTestJson({GetSnippet()})); | |
616 auto service = MakeSnippetsService(/*set_empty_response=*/false); | |
617 | |
618 // When recreating the service, we should not get any |Schedule| calls: | |
619 // The tasks are already scheduled with the correct intervals, so nothing on | |
620 // initialization, and the service has data from the DB, so no automatic fetch | |
621 // should happen. | |
622 Mock::VerifyAndClearExpectations(&mock_scheduler()); | |
623 EXPECT_CALL(mock_scheduler(), Schedule(_, _)).Times(0); | |
624 EXPECT_CALL(mock_scheduler(), Unschedule()).Times(0); | |
625 ResetSnippetsService(&service, /*set_empty_response=*/true); | |
626 } | |
627 | |
628 TEST_F(RemoteSuggestionsProviderTest, RescheduleAfterSuccessfulFetch) { | |
629 // We should get two |Schedule| calls: The first when initialization | |
630 // completes, the second one after the automatic (since the service doesn't | |
631 // have any data yet) fetch finishes. | |
632 EXPECT_CALL(mock_scheduler(), Schedule(_, _)).Times(2); | |
633 auto service = MakeSnippetsService(); | |
634 | |
635 // A successful fetch should trigger another |Schedule|. | |
636 EXPECT_CALL(mock_scheduler(), Schedule(_, _)); | |
637 LoadFromJSONString(service.get(), GetTestJson({GetSnippet()})); | |
638 } | |
639 | |
640 TEST_F(RemoteSuggestionsProviderTest, DontRescheduleAfterFailedFetch) { | |
641 // We should get two |Schedule| calls: The first when initialization | |
642 // completes, the second one after the automatic (since the service doesn't | |
643 // have any data yet) fetch finishes. | |
644 EXPECT_CALL(mock_scheduler(), Schedule(_, _)).Times(2); | |
645 auto service = MakeSnippetsService(); | |
646 | |
647 // A failed fetch should NOT trigger another |Schedule|. | |
648 EXPECT_CALL(mock_scheduler(), Schedule(_, _)).Times(0); | |
649 LoadFromJSONString(service.get(), GetTestJson({GetInvalidSnippet()})); | |
650 } | |
651 | |
652 TEST_F(RemoteSuggestionsProviderTest, IgnoreRescheduleBeforeInit) { | |
653 // We should get two |Schedule| calls: The first when initialization | |
654 // completes, the second one after the automatic (since the service doesn't | |
655 // have any data yet) fetch finishes. | |
656 EXPECT_CALL(mock_scheduler(), Schedule(_, _)).Times(2); | |
657 // The |RescheduleFetching| call shouldn't do anything (in particular not | |
658 // result in an |Unschedule|), since the service isn't initialized yet. | |
659 EXPECT_CALL(mock_scheduler(), Unschedule()).Times(0); | |
660 auto service = MakeSnippetsServiceWithoutInitialization(); | |
661 service->RescheduleFetching(false); | |
662 WaitForSnippetsServiceInitialization(service.get(), | |
663 /*set_empty_response=*/true); | |
664 } | |
665 | |
666 TEST_F(RemoteSuggestionsProviderTest, HandleForcedRescheduleBeforeInit) { | |
667 { | |
668 InSequence s; | |
669 // The |RescheduleFetching| call with force=true should result in an | |
670 // |Unschedule|, since the service isn't initialized yet. | |
671 EXPECT_CALL(mock_scheduler(), Unschedule()).Times(1); | |
672 // We should get two |Schedule| calls: The first when initialization | |
673 // completes, the second one after the automatic (since the service doesn't | |
674 // have any data yet) fetch finishes. | |
675 EXPECT_CALL(mock_scheduler(), Schedule(_, _)).Times(2); | |
676 } | |
677 auto service = MakeSnippetsServiceWithoutInitialization(); | |
678 service->RescheduleFetching(true); | |
679 WaitForSnippetsServiceInitialization(service.get(), | |
680 /*set_empty_response=*/true); | |
681 } | |
682 | |
683 TEST_F(RemoteSuggestionsProviderTest, RescheduleOnStateChange) { | |
684 { | |
685 InSequence s; | |
686 // Initial startup. | |
687 EXPECT_CALL(mock_scheduler(), Schedule(_, _)).Times(2); | |
688 // Service gets disabled. | |
689 EXPECT_CALL(mock_scheduler(), Unschedule()); | |
690 // Service gets enabled again. | |
691 EXPECT_CALL(mock_scheduler(), Schedule(_, _)).Times(2); | |
692 } | |
693 auto service = MakeSnippetsService(); | |
694 ASSERT_TRUE(service->ready()); | |
695 | |
696 service->OnStatusChanged(RemoteSuggestionsStatus::ENABLED_AND_SIGNED_IN, | |
697 RemoteSuggestionsStatus::EXPLICITLY_DISABLED); | |
698 ASSERT_FALSE(service->ready()); | |
699 base::RunLoop().RunUntilIdle(); | |
700 | |
701 service->OnStatusChanged(RemoteSuggestionsStatus::EXPLICITLY_DISABLED, | |
702 RemoteSuggestionsStatus::ENABLED_AND_SIGNED_OUT); | |
703 ASSERT_TRUE(service->ready()); | |
704 base::RunLoop().RunUntilIdle(); | |
705 } | |
706 | |
707 TEST_F(RemoteSuggestionsProviderTest, DontUnscheduleOnShutdown) { | |
708 EXPECT_CALL(mock_scheduler(), Schedule(_, _)).Times(2); | |
709 EXPECT_CALL(mock_scheduler(), Unschedule()).Times(0); | |
710 | |
711 auto service = MakeSnippetsService(); | |
712 | |
713 service.reset(); | |
714 base::RunLoop().RunUntilIdle(); | |
715 } | |
716 | |
717 TEST_F(RemoteSuggestionsProviderTest, Full) { | |
718 std::string json_str(GetTestJson({GetSnippet()})); | |
719 | |
720 auto service = MakeSnippetsService(); | |
721 | |
722 LoadFromJSONString(service.get(), json_str); | |
723 | |
724 ASSERT_THAT(observer().SuggestionsForCategory(articles_category()), | |
725 SizeIs(1)); | |
726 ASSERT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
727 | |
728 const ContentSuggestion& suggestion = | |
729 observer().SuggestionsForCategory(articles_category()).front(); | |
730 | |
731 EXPECT_EQ(MakeArticleID(kSnippetUrl), suggestion.id()); | |
732 EXPECT_EQ(kSnippetTitle, base::UTF16ToUTF8(suggestion.title())); | |
733 EXPECT_EQ(kSnippetText, base::UTF16ToUTF8(suggestion.snippet_text())); | |
734 EXPECT_EQ(GetDefaultCreationTime(), suggestion.publish_date()); | |
735 EXPECT_EQ(kSnippetPublisherName, | |
736 base::UTF16ToUTF8(suggestion.publisher_name())); | |
737 } | |
738 | |
739 TEST_F(RemoteSuggestionsProviderTest, CategoryTitle) { | |
740 const base::string16 test_default_title = | |
741 base::UTF8ToUTF16(kTestJsonDefaultCategoryTitle); | |
742 | |
743 // Don't send an initial response -- we want to test what happens without any | |
744 // server status. | |
745 auto service = MakeSnippetsService(/*set_empty_response=*/false); | |
746 | |
747 // The articles category should be there by default, and have a title. | |
748 CategoryInfo info_before = service->GetCategoryInfo(articles_category()); | |
749 ASSERT_THAT(info_before.title(), Not(IsEmpty())); | |
750 ASSERT_THAT(info_before.title(), Not(Eq(test_default_title))); | |
751 EXPECT_THAT(info_before.has_more_action(), Eq(true)); | |
752 EXPECT_THAT(info_before.has_reload_action(), Eq(true)); | |
753 EXPECT_THAT(info_before.has_view_all_action(), Eq(false)); | |
754 EXPECT_THAT(info_before.show_if_empty(), Eq(true)); | |
755 | |
756 std::string json_str_with_title(GetTestJson({GetSnippet()})); | |
757 LoadFromJSONString(service.get(), json_str_with_title); | |
758 | |
759 ASSERT_THAT(observer().SuggestionsForCategory(articles_category()), | |
760 SizeIs(1)); | |
761 ASSERT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
762 | |
763 // The response contained a title, |kTestJsonDefaultCategoryTitle|. | |
764 // Make sure we updated the title in the CategoryInfo. | |
765 CategoryInfo info_with_title = service->GetCategoryInfo(articles_category()); | |
766 EXPECT_THAT(info_before.title(), Not(Eq(info_with_title.title()))); | |
767 EXPECT_THAT(test_default_title, Eq(info_with_title.title())); | |
768 EXPECT_THAT(info_before.has_more_action(), Eq(true)); | |
769 EXPECT_THAT(info_before.has_reload_action(), Eq(true)); | |
770 EXPECT_THAT(info_before.has_view_all_action(), Eq(false)); | |
771 EXPECT_THAT(info_before.show_if_empty(), Eq(true)); | |
772 } | |
773 | |
774 TEST_F(RemoteSuggestionsProviderTest, MultipleCategories) { | |
775 auto service = MakeSnippetsService(); | |
776 std::string json_str = | |
777 MultiCategoryJsonBuilder() | |
778 .AddCategory({GetSnippetN(0)}, /*remote_category_id=*/1) | |
779 .AddCategory({GetSnippetN(1)}, /*remote_category_id=*/2) | |
780 .Build(); | |
781 LoadFromJSONString(service.get(), json_str); | |
782 | |
783 ASSERT_THAT(observer().statuses(), | |
784 Eq(std::map<Category, CategoryStatus, Category::CompareByID>{ | |
785 {articles_category(), CategoryStatus::AVAILABLE}, | |
786 {other_category(), CategoryStatus::AVAILABLE}, | |
787 })); | |
788 | |
789 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
790 EXPECT_THAT(service->GetSnippetsForTesting(other_category()), SizeIs(1)); | |
791 | |
792 ASSERT_THAT(observer().SuggestionsForCategory(articles_category()), | |
793 SizeIs(1)); | |
794 | |
795 ASSERT_THAT(observer().SuggestionsForCategory(other_category()), SizeIs(1)); | |
796 | |
797 { | |
798 const ContentSuggestion& suggestion = | |
799 observer().SuggestionsForCategory(articles_category()).front(); | |
800 EXPECT_EQ(MakeArticleID(std::string(kSnippetUrl) + "/0"), suggestion.id()); | |
801 EXPECT_EQ(kSnippetTitle, base::UTF16ToUTF8(suggestion.title())); | |
802 EXPECT_EQ(kSnippetText, base::UTF16ToUTF8(suggestion.snippet_text())); | |
803 EXPECT_EQ(GetDefaultCreationTime(), suggestion.publish_date()); | |
804 EXPECT_EQ(kSnippetPublisherName, | |
805 base::UTF16ToUTF8(suggestion.publisher_name())); | |
806 } | |
807 | |
808 { | |
809 const ContentSuggestion& suggestion = | |
810 observer().SuggestionsForCategory(other_category()).front(); | |
811 EXPECT_EQ(MakeOtherID(std::string(kSnippetUrl) + "/1"), suggestion.id()); | |
812 EXPECT_EQ(kSnippetTitle, base::UTF16ToUTF8(suggestion.title())); | |
813 EXPECT_EQ(kSnippetText, base::UTF16ToUTF8(suggestion.snippet_text())); | |
814 EXPECT_EQ(GetDefaultCreationTime(), suggestion.publish_date()); | |
815 EXPECT_EQ(kSnippetPublisherName, | |
816 base::UTF16ToUTF8(suggestion.publisher_name())); | |
817 } | |
818 } | |
819 | |
820 TEST_F(RemoteSuggestionsProviderTest, ArticleCategoryInfo) { | |
821 auto service = MakeSnippetsService(); | |
822 CategoryInfo article_info = service->GetCategoryInfo(articles_category()); | |
823 EXPECT_THAT(article_info.has_more_action(), Eq(true)); | |
824 EXPECT_THAT(article_info.has_reload_action(), Eq(true)); | |
825 EXPECT_THAT(article_info.has_view_all_action(), Eq(false)); | |
826 EXPECT_THAT(article_info.show_if_empty(), Eq(true)); | |
827 } | |
828 | |
829 TEST_F(RemoteSuggestionsProviderTest, ExperimentalCategoryInfo) { | |
830 auto service = MakeSnippetsService(); | |
831 std::string json_str = | |
832 MultiCategoryJsonBuilder() | |
833 .AddCategory({GetSnippetN(0)}, /*remote_category_id=*/1) | |
834 .AddCategory({GetSnippetN(1)}, kUnknownRemoteCategoryId) | |
835 .Build(); | |
836 // Load data with multiple categories so that a new experimental category gets | |
837 // registered. | |
838 LoadFromJSONString(service.get(), json_str); | |
839 | |
840 CategoryInfo info = service->GetCategoryInfo(unknown_category()); | |
841 EXPECT_THAT(info.has_more_action(), Eq(false)); | |
842 EXPECT_THAT(info.has_reload_action(), Eq(false)); | |
843 EXPECT_THAT(info.has_view_all_action(), Eq(false)); | |
844 EXPECT_THAT(info.show_if_empty(), Eq(false)); | |
845 } | |
846 | |
847 TEST_F(RemoteSuggestionsProviderTest, AddRemoteCategoriesToCategoryRanker) { | |
848 auto mock_ranker = base::MakeUnique<MockCategoryRanker>(); | |
849 MockCategoryRanker* raw_mock_ranker = mock_ranker.get(); | |
850 SetCategoryRanker(std::move(mock_ranker)); | |
851 std::string json_str = | |
852 MultiCategoryJsonBuilder() | |
853 .AddCategory({GetSnippetN(0)}, /*remote_category_id=*/11) | |
854 .AddCategory({GetSnippetN(1)}, /*remote_category_id=*/13) | |
855 .AddCategory({GetSnippetN(2)}, /*remote_category_id=*/12) | |
856 .Build(); | |
857 SetUpFetchResponse(json_str); | |
858 { | |
859 // The order of categories is determined by the order in which they are | |
860 // added. Thus, the latter is tested here. | |
861 InSequence s; | |
862 EXPECT_CALL(*raw_mock_ranker, | |
863 AppendCategoryIfNecessary(Category::FromRemoteCategory(11))); | |
864 EXPECT_CALL(*raw_mock_ranker, | |
865 AppendCategoryIfNecessary(Category::FromRemoteCategory(13))); | |
866 EXPECT_CALL(*raw_mock_ranker, | |
867 AppendCategoryIfNecessary(Category::FromRemoteCategory(12))); | |
868 } | |
869 auto service = MakeSnippetsService(/*set_empty_response=*/false); | |
870 } | |
871 | |
872 TEST_F(RemoteSuggestionsProviderTest, PersistCategoryInfos) { | |
873 auto service = MakeSnippetsService(); | |
874 // TODO(vitaliii): Use |articles_category()| instead of constant ID below. | |
875 std::string json_str = | |
876 MultiCategoryJsonBuilder() | |
877 .AddCategoryWithCustomTitle( | |
878 {GetSnippetN(0)}, /*remote_category_id=*/1, "Articles for You") | |
879 .AddCategoryWithCustomTitle({GetSnippetN(1)}, | |
880 kUnknownRemoteCategoryId, "Other Things") | |
881 .Build(); | |
882 LoadFromJSONString(service.get(), json_str); | |
883 | |
884 ASSERT_EQ(observer().StatusForCategory(articles_category()), | |
885 CategoryStatus::AVAILABLE); | |
886 ASSERT_EQ(observer().StatusForCategory(unknown_category()), | |
887 CategoryStatus::AVAILABLE); | |
888 | |
889 CategoryInfo info_articles_before = | |
890 service->GetCategoryInfo(articles_category()); | |
891 CategoryInfo info_unknown_before = | |
892 service->GetCategoryInfo(unknown_category()); | |
893 | |
894 // Recreate the service to simulate a Chrome restart. | |
895 ResetSnippetsService(&service, /*set_empty_response=*/true); | |
896 | |
897 // The categories should have been restored. | |
898 ASSERT_NE(observer().StatusForCategory(articles_category()), | |
899 CategoryStatus::NOT_PROVIDED); | |
900 ASSERT_NE(observer().StatusForCategory(unknown_category()), | |
901 CategoryStatus::NOT_PROVIDED); | |
902 | |
903 EXPECT_EQ(observer().StatusForCategory(articles_category()), | |
904 CategoryStatus::AVAILABLE); | |
905 EXPECT_EQ(observer().StatusForCategory(unknown_category()), | |
906 CategoryStatus::AVAILABLE); | |
907 | |
908 CategoryInfo info_articles_after = | |
909 service->GetCategoryInfo(articles_category()); | |
910 CategoryInfo info_unknown_after = | |
911 service->GetCategoryInfo(unknown_category()); | |
912 | |
913 EXPECT_EQ(info_articles_before.title(), info_articles_after.title()); | |
914 EXPECT_EQ(info_unknown_before.title(), info_unknown_after.title()); | |
915 } | |
916 | |
917 TEST_F(RemoteSuggestionsProviderTest, PersistRemoteCategoryOrder) { | |
918 // We create a service with a normal ranker to store the order. | |
919 std::string json_str = | |
920 MultiCategoryJsonBuilder() | |
921 .AddCategory({GetSnippetN(0)}, /*remote_category_id=*/11) | |
922 .AddCategory({GetSnippetN(1)}, /*remote_category_id=*/13) | |
923 .AddCategory({GetSnippetN(2)}, /*remote_category_id=*/12) | |
924 .Build(); | |
925 SetUpFetchResponse(json_str); | |
926 auto service = MakeSnippetsService(/*set_empty_response=*/false); | |
927 | |
928 // We manually recreate the service to simulate Chrome restart and enforce a | |
929 // mock ranker. The response is cleared to ensure that the order is not | |
930 // fetched. | |
931 SetUpFetchResponse(""); | |
932 auto mock_ranker = base::MakeUnique<MockCategoryRanker>(); | |
933 MockCategoryRanker* raw_mock_ranker = mock_ranker.get(); | |
934 SetCategoryRanker(std::move(mock_ranker)); | |
935 { | |
936 // The order of categories is determined by the order in which they are | |
937 // added. Thus, the latter is tested here. | |
938 InSequence s; | |
939 // Article category always exists and, therefore, it is stored in prefs too. | |
940 EXPECT_CALL(*raw_mock_ranker, | |
941 AppendCategoryIfNecessary(articles_category())); | |
942 | |
943 EXPECT_CALL(*raw_mock_ranker, | |
944 AppendCategoryIfNecessary(Category::FromRemoteCategory(11))); | |
945 EXPECT_CALL(*raw_mock_ranker, | |
946 AppendCategoryIfNecessary(Category::FromRemoteCategory(13))); | |
947 EXPECT_CALL(*raw_mock_ranker, | |
948 AppendCategoryIfNecessary(Category::FromRemoteCategory(12))); | |
949 } | |
950 ResetSnippetsService(&service, /*set_empty_response=*/false); | |
951 } | |
952 | |
953 TEST_F(RemoteSuggestionsProviderTest, PersistSuggestions) { | |
954 auto service = MakeSnippetsService(); | |
955 std::string json_str = | |
956 MultiCategoryJsonBuilder() | |
957 .AddCategory({GetSnippetN(0)}, /*remote_category_id=*/1) | |
958 .AddCategory({GetSnippetN(2)}, /*remote_category_id=*/2) | |
959 .Build(); | |
960 LoadFromJSONString(service.get(), json_str); | |
961 | |
962 ASSERT_THAT(observer().SuggestionsForCategory(articles_category()), | |
963 SizeIs(1)); | |
964 ASSERT_THAT(observer().SuggestionsForCategory(other_category()), SizeIs(1)); | |
965 | |
966 // Recreate the service to simulate a Chrome restart. | |
967 ResetSnippetsService(&service, /*set_empty_response=*/true); | |
968 | |
969 // The suggestions in both categories should have been restored. | |
970 EXPECT_THAT(observer().SuggestionsForCategory(articles_category()), | |
971 SizeIs(1)); | |
972 EXPECT_THAT(observer().SuggestionsForCategory(other_category()), SizeIs(1)); | |
973 } | |
974 | |
975 TEST_F(RemoteSuggestionsProviderTest, DontNotifyIfNotAvailable) { | |
976 // Get some suggestions into the database. | |
977 auto service = MakeSnippetsService(); | |
978 std::string json_str = | |
979 MultiCategoryJsonBuilder() | |
980 .AddCategory({GetSnippetN(0)}, | |
981 /*remote_category_id=*/1) | |
982 .AddCategory({GetSnippetN(1)}, /*remote_category_id=*/2) | |
983 .Build(); | |
984 LoadFromJSONString(service.get(), json_str); | |
985 | |
986 ASSERT_THAT(observer().SuggestionsForCategory(articles_category()), | |
987 SizeIs(1)); | |
988 ASSERT_THAT(observer().SuggestionsForCategory(other_category()), SizeIs(1)); | |
989 | |
990 service.reset(); | |
991 | |
992 // Set the pref that disables remote suggestions. | |
993 pref_service()->SetBoolean(prefs::kEnableSnippets, false); | |
994 | |
995 // Recreate the service to simulate a Chrome start. | |
996 ResetSnippetsService(&service, /*set_empty_response=*/true); | |
997 | |
998 ASSERT_THAT(RemoteSuggestionsProvider::State::DISABLED, Eq(service->state_)); | |
999 | |
1000 // Now the observer should not have received any suggestions. | |
1001 EXPECT_THAT(observer().SuggestionsForCategory(articles_category()), | |
1002 IsEmpty()); | |
1003 EXPECT_THAT(observer().SuggestionsForCategory(other_category()), IsEmpty()); | |
1004 } | |
1005 | |
1006 TEST_F(RemoteSuggestionsProviderTest, Clear) { | |
1007 auto service = MakeSnippetsService(); | |
1008 | |
1009 std::string json_str(GetTestJson({GetSnippet()})); | |
1010 | |
1011 LoadFromJSONString(service.get(), json_str); | |
1012 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
1013 | |
1014 service->ClearCachedSuggestions(articles_category()); | |
1015 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), IsEmpty()); | |
1016 } | |
1017 | |
1018 TEST_F(RemoteSuggestionsProviderTest, ReplaceSnippets) { | |
1019 auto service = MakeSnippetsService(); | |
1020 | |
1021 std::string first("http://first"); | |
1022 LoadFromJSONString(service.get(), GetTestJson({GetSnippetWithUrl(first)})); | |
1023 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), | |
1024 ElementsAre(IdEq(first))); | |
1025 | |
1026 std::string second("http://second"); | |
1027 LoadFromJSONString(service.get(), GetTestJson({GetSnippetWithUrl(second)})); | |
1028 // The snippets loaded last replace all that was loaded previously. | |
1029 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), | |
1030 ElementsAre(IdEq(second))); | |
1031 } | |
1032 | |
1033 TEST_F(RemoteSuggestionsProviderTest, LoadsAdditionalSnippets) { | |
1034 auto service = MakeSnippetsService(); | |
1035 | |
1036 LoadFromJSONString(service.get(), | |
1037 GetTestJson({GetSnippetWithUrl("http://first")})); | |
1038 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), | |
1039 ElementsAre(IdEq("http://first"))); | |
1040 | |
1041 auto expect_only_second_suggestion_received = base::Bind([]( | |
1042 Status status, std::vector<ContentSuggestion> suggestions) { | |
1043 EXPECT_THAT(suggestions, SizeIs(1)); | |
1044 EXPECT_THAT(suggestions[0].id().id_within_category(), Eq("http://second")); | |
1045 }); | |
1046 LoadMoreFromJSONString(service.get(), articles_category(), | |
1047 GetTestJson({GetSnippetWithUrl("http://second")}), | |
1048 /*known_ids=*/std::set<std::string>(), | |
1049 expect_only_second_suggestion_received); | |
1050 | |
1051 // Verify we can resolve the image of the new snippets. | |
1052 ServeImageCallback cb = | |
1053 base::Bind(&ServeOneByOneImage, &service->GetImageFetcherForTesting()); | |
1054 EXPECT_CALL(*image_fetcher(), StartOrQueueNetworkRequest(_, _, _)) | |
1055 .Times(2) | |
1056 .WillRepeatedly(WithArgs<0, 2>(Invoke(&cb, &ServeImageCallback::Run))); | |
1057 image_decoder()->SetDecodedImage(gfx::test::CreateImage(1, 1)); | |
1058 gfx::Image image = FetchImage(service.get(), MakeArticleID("http://first")); | |
1059 EXPECT_FALSE(image.IsEmpty()); | |
1060 EXPECT_EQ(1, image.Width()); | |
1061 | |
1062 image = FetchImage(service.get(), MakeArticleID("http://second")); | |
1063 EXPECT_FALSE(image.IsEmpty()); | |
1064 EXPECT_EQ(1, image.Width()); | |
1065 | |
1066 // Verify that the observer received the update as well. We should see the | |
1067 // newly-fetched items filled up with existing ones. | |
1068 EXPECT_THAT(observer().SuggestionsForCategory(articles_category()), | |
1069 ElementsAre(IdWithinCategoryEq("http://first"), | |
1070 IdWithinCategoryEq("http://second"))); | |
1071 } | |
1072 | |
1073 // The tests TestMergingFetchedMoreSnippetsFillup and | |
1074 // TestMergingFetchedMoreSnippetsReplaceAll simulate the following user story: | |
1075 // 1) fetch suggestions in NTP A | |
1076 // 2) fetch more suggestions in NTP A. | |
1077 // 3) open new NTP B: See the last 10 results visible in step 2). | |
1078 // 4) fetch more suggestions in NTP B. Make sure no results from step 1) which | |
1079 // were superseded in step 2) get merged back in again. | |
1080 // TODO(tschumann): Test step 4) on a higher level instead of peeking into the | |
1081 // internal 'dismissed' data. The proper check is to make sure we tell the | |
1082 // backend to exclude these snippets. | |
1083 TEST_F(RemoteSuggestionsProviderTest, TestMergingFetchedMoreSnippetsFillup) { | |
1084 auto service = MakeSnippetsService(/*set_empty_response=*/false); | |
1085 LoadFromJSONString( | |
1086 service.get(), | |
1087 GetTestJson( | |
1088 {GetSnippetWithUrl("http://id-1"), GetSnippetWithUrl("http://id-2"), | |
1089 GetSnippetWithUrl("http://id-3"), GetSnippetWithUrl("http://id-4"), | |
1090 GetSnippetWithUrl("http://id-5"), GetSnippetWithUrl("http://id-6"), | |
1091 GetSnippetWithUrl("http://id-7"), GetSnippetWithUrl("http://id-8"), | |
1092 GetSnippetWithUrl("http://id-9"), | |
1093 GetSnippetWithUrl("http://id-10")})); | |
1094 EXPECT_THAT( | |
1095 observer().SuggestionsForCategory(articles_category()), | |
1096 ElementsAre( | |
1097 IdWithinCategoryEq("http://id-1"), IdWithinCategoryEq("http://id-2"), | |
1098 IdWithinCategoryEq("http://id-3"), IdWithinCategoryEq("http://id-4"), | |
1099 IdWithinCategoryEq("http://id-5"), IdWithinCategoryEq("http://id-6"), | |
1100 IdWithinCategoryEq("http://id-7"), IdWithinCategoryEq("http://id-8"), | |
1101 IdWithinCategoryEq("http://id-9"), | |
1102 IdWithinCategoryEq("http://id-10"))); | |
1103 | |
1104 auto expect_receiving_two_new_snippets = | |
1105 base::Bind([](Status status, std::vector<ContentSuggestion> suggestions) { | |
1106 ASSERT_THAT(suggestions, SizeIs(2)); | |
1107 EXPECT_THAT(suggestions[0], IdWithinCategoryEq("http://more-id-1")); | |
1108 EXPECT_THAT(suggestions[1], IdWithinCategoryEq("http://more-id-2")); | |
1109 }); | |
1110 LoadMoreFromJSONString( | |
1111 service.get(), articles_category(), | |
1112 GetTestJson({GetSnippetWithUrl("http://more-id-1"), | |
1113 GetSnippetWithUrl("http://more-id-2")}), | |
1114 /*known_ids=*/{"http://id-1", "http://id-2", "http://id-3", "http://id-4", | |
1115 "http://id-5", "http://id-6", "http://id-7", "http://id-8", | |
1116 "http://id-9", "http://id-10"}, | |
1117 expect_receiving_two_new_snippets); | |
1118 | |
1119 // Verify that the observer received the update as well. We should see the | |
1120 // newly-fetched items filled up with existing ones. The merging is done | |
1121 // mimicking a scrolling behavior. | |
1122 EXPECT_THAT( | |
1123 observer().SuggestionsForCategory(articles_category()), | |
1124 ElementsAre( | |
1125 IdWithinCategoryEq("http://id-3"), IdWithinCategoryEq("http://id-4"), | |
1126 IdWithinCategoryEq("http://id-5"), IdWithinCategoryEq("http://id-6"), | |
1127 IdWithinCategoryEq("http://id-7"), IdWithinCategoryEq("http://id-8"), | |
1128 IdWithinCategoryEq("http://id-9"), IdWithinCategoryEq("http://id-10"), | |
1129 IdWithinCategoryEq("http://more-id-1"), | |
1130 IdWithinCategoryEq("http://more-id-2"))); | |
1131 // Verify the superseded suggestions got marked as dismissed. | |
1132 EXPECT_THAT(service->GetDismissedSnippetsForTesting(articles_category()), | |
1133 ElementsAre(IdEq("http://id-1"), IdEq("http://id-2"))); | |
1134 } | |
1135 | |
1136 TEST_F(RemoteSuggestionsProviderTest, | |
1137 TestMergingFetchedMoreSnippetsReplaceAll) { | |
1138 auto service = MakeSnippetsService(/*set_empty_response=*/false); | |
1139 LoadFromJSONString( | |
1140 service.get(), | |
1141 GetTestJson( | |
1142 {GetSnippetWithUrl("http://id-1"), GetSnippetWithUrl("http://id-2"), | |
1143 GetSnippetWithUrl("http://id-3"), GetSnippetWithUrl("http://id-4"), | |
1144 GetSnippetWithUrl("http://id-5"), GetSnippetWithUrl("http://id-6"), | |
1145 GetSnippetWithUrl("http://id-7"), GetSnippetWithUrl("http://id-8"), | |
1146 GetSnippetWithUrl("http://id-9"), | |
1147 GetSnippetWithUrl("http://id-10")})); | |
1148 EXPECT_THAT( | |
1149 observer().SuggestionsForCategory(articles_category()), | |
1150 ElementsAre( | |
1151 IdWithinCategoryEq("http://id-1"), IdWithinCategoryEq("http://id-2"), | |
1152 IdWithinCategoryEq("http://id-3"), IdWithinCategoryEq("http://id-4"), | |
1153 IdWithinCategoryEq("http://id-5"), IdWithinCategoryEq("http://id-6"), | |
1154 IdWithinCategoryEq("http://id-7"), IdWithinCategoryEq("http://id-8"), | |
1155 IdWithinCategoryEq("http://id-9"), | |
1156 IdWithinCategoryEq("http://id-10"))); | |
1157 | |
1158 auto expect_receiving_ten_new_snippets = | |
1159 base::Bind([](Status status, std::vector<ContentSuggestion> suggestions) { | |
1160 EXPECT_THAT(suggestions, ElementsAre( | |
1161 IdWithinCategoryEq("http://more-id-1"), | |
1162 IdWithinCategoryEq("http://more-id-2"), | |
1163 IdWithinCategoryEq("http://more-id-3"), | |
1164 IdWithinCategoryEq("http://more-id-4"), | |
1165 IdWithinCategoryEq("http://more-id-5"), | |
1166 IdWithinCategoryEq("http://more-id-6"), | |
1167 IdWithinCategoryEq("http://more-id-7"), | |
1168 IdWithinCategoryEq("http://more-id-8"), | |
1169 IdWithinCategoryEq("http://more-id-9"), | |
1170 IdWithinCategoryEq("http://more-id-10"))); | |
1171 }); | |
1172 LoadMoreFromJSONString( | |
1173 service.get(), articles_category(), | |
1174 GetTestJson({GetSnippetWithUrl("http://more-id-1"), | |
1175 GetSnippetWithUrl("http://more-id-2"), | |
1176 GetSnippetWithUrl("http://more-id-3"), | |
1177 GetSnippetWithUrl("http://more-id-4"), | |
1178 GetSnippetWithUrl("http://more-id-5"), | |
1179 GetSnippetWithUrl("http://more-id-6"), | |
1180 GetSnippetWithUrl("http://more-id-7"), | |
1181 GetSnippetWithUrl("http://more-id-8"), | |
1182 GetSnippetWithUrl("http://more-id-9"), | |
1183 GetSnippetWithUrl("http://more-id-10")}), | |
1184 /*known_ids=*/{"http://id-1", "http://id-2", "http://id-3", "http://id-4", | |
1185 "http://id-5", "http://id-6", "http://id-7", "http://id-8", | |
1186 "http://id-9", "http://id-10"}, | |
1187 expect_receiving_ten_new_snippets); | |
1188 EXPECT_THAT(observer().SuggestionsForCategory(articles_category()), | |
1189 ElementsAre(IdWithinCategoryEq("http://more-id-1"), | |
1190 IdWithinCategoryEq("http://more-id-2"), | |
1191 IdWithinCategoryEq("http://more-id-3"), | |
1192 IdWithinCategoryEq("http://more-id-4"), | |
1193 IdWithinCategoryEq("http://more-id-5"), | |
1194 IdWithinCategoryEq("http://more-id-6"), | |
1195 IdWithinCategoryEq("http://more-id-7"), | |
1196 IdWithinCategoryEq("http://more-id-8"), | |
1197 IdWithinCategoryEq("http://more-id-9"), | |
1198 IdWithinCategoryEq("http://more-id-10"))); | |
1199 // Verify the superseded suggestions got marked as dismissed. | |
1200 EXPECT_THAT( | |
1201 service->GetDismissedSnippetsForTesting(articles_category()), | |
1202 ElementsAre(IdEq("http://id-1"), IdEq("http://id-2"), IdEq("http://id-3"), | |
1203 IdEq("http://id-4"), IdEq("http://id-5"), IdEq("http://id-6"), | |
1204 IdEq("http://id-7"), IdEq("http://id-8"), IdEq("http://id-9"), | |
1205 IdEq("http://id-10"))); | |
1206 } | |
1207 | |
1208 // TODO(tschumann): We don't have test making sure the NTPSnippetsFetcher | |
1209 // actually gets the proper parameters. Add tests with an injected | |
1210 // NTPSnippetsFetcher to verify the parameters, including proper handling of | |
1211 // dismissed and known_ids. | |
1212 | |
1213 namespace { | |
1214 | |
1215 // Workaround for gMock's lack of support for movable types. | |
1216 void SuggestionsLoaded( | |
1217 MockFunction<void(Status, const std::vector<ContentSuggestion>&)>* loaded, | |
1218 Status status, | |
1219 std::vector<ContentSuggestion> suggestions) { | |
1220 loaded->Call(status, suggestions); | |
1221 } | |
1222 | |
1223 } // namespace | |
1224 | |
1225 TEST_F(RemoteSuggestionsProviderTest, ReturnFetchRequestEmptyBeforeInit) { | |
1226 auto service = MakeSnippetsServiceWithoutInitialization(); | |
1227 MockFunction<void(Status, const std::vector<ContentSuggestion>&)> loaded; | |
1228 EXPECT_CALL(loaded, Call(HasCode(StatusCode::TEMPORARY_ERROR), IsEmpty())); | |
1229 service->Fetch(articles_category(), std::set<std::string>(), | |
1230 base::Bind(&SuggestionsLoaded, &loaded)); | |
1231 base::RunLoop().RunUntilIdle(); | |
1232 } | |
1233 | |
1234 TEST_F(RemoteSuggestionsProviderTest, ReturnTemporaryErrorForInvalidJson) { | |
1235 auto service = MakeSnippetsService(); | |
1236 | |
1237 MockFunction<void(Status, const std::vector<ContentSuggestion>&)> loaded; | |
1238 EXPECT_CALL(loaded, Call(HasCode(StatusCode::TEMPORARY_ERROR), IsEmpty())); | |
1239 LoadMoreFromJSONString(service.get(), articles_category(), | |
1240 "invalid json string}]}", | |
1241 /*known_ids=*/std::set<std::string>(), | |
1242 base::Bind(&SuggestionsLoaded, &loaded)); | |
1243 EXPECT_THAT(service->snippets_fetcher()->last_status(), | |
1244 StartsWith("Received invalid JSON")); | |
1245 } | |
1246 | |
1247 TEST_F(RemoteSuggestionsProviderTest, ReturnTemporaryErrorForInvalidSnippet) { | |
1248 auto service = MakeSnippetsService(); | |
1249 | |
1250 MockFunction<void(Status, const std::vector<ContentSuggestion>&)> loaded; | |
1251 EXPECT_CALL(loaded, Call(HasCode(StatusCode::TEMPORARY_ERROR), IsEmpty())); | |
1252 LoadMoreFromJSONString(service.get(), articles_category(), | |
1253 GetTestJson({GetIncompleteSnippet()}), | |
1254 /*known_ids=*/std::set<std::string>(), | |
1255 base::Bind(&SuggestionsLoaded, &loaded)); | |
1256 EXPECT_THAT(service->snippets_fetcher()->last_status(), | |
1257 StartsWith("Invalid / empty list")); | |
1258 } | |
1259 | |
1260 TEST_F(RemoteSuggestionsProviderTest, ReturnTemporaryErrorForRequestFailure) { | |
1261 // Created SnippetsService will fail by default with unsuccessful request. | |
1262 auto service = MakeSnippetsService(/*set_empty_response=*/false); | |
1263 | |
1264 MockFunction<void(Status, const std::vector<ContentSuggestion>&)> loaded; | |
1265 EXPECT_CALL(loaded, Call(HasCode(StatusCode::TEMPORARY_ERROR), IsEmpty())); | |
1266 service->Fetch(articles_category(), | |
1267 /*known_ids=*/std::set<std::string>(), | |
1268 base::Bind(&SuggestionsLoaded, &loaded)); | |
1269 base::RunLoop().RunUntilIdle(); | |
1270 } | |
1271 | |
1272 TEST_F(RemoteSuggestionsProviderTest, ReturnTemporaryErrorForHttpFailure) { | |
1273 auto service = MakeSnippetsService(); | |
1274 SetUpHttpError(); | |
1275 | |
1276 MockFunction<void(Status, const std::vector<ContentSuggestion>&)> loaded; | |
1277 EXPECT_CALL(loaded, Call(HasCode(StatusCode::TEMPORARY_ERROR), IsEmpty())); | |
1278 service->Fetch(articles_category(), | |
1279 /*known_ids=*/std::set<std::string>(), | |
1280 base::Bind(&SuggestionsLoaded, &loaded)); | |
1281 base::RunLoop().RunUntilIdle(); | |
1282 } | |
1283 | |
1284 TEST_F(RemoteSuggestionsProviderTest, LoadInvalidJson) { | |
1285 auto service = MakeSnippetsService(); | |
1286 | |
1287 LoadFromJSONString(service.get(), GetTestJson({GetInvalidSnippet()})); | |
1288 EXPECT_THAT(service->snippets_fetcher()->last_status(), | |
1289 StartsWith("Received invalid JSON")); | |
1290 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), IsEmpty()); | |
1291 } | |
1292 | |
1293 TEST_F(RemoteSuggestionsProviderTest, LoadInvalidJsonWithExistingSnippets) { | |
1294 auto service = MakeSnippetsService(); | |
1295 | |
1296 LoadFromJSONString(service.get(), GetTestJson({GetSnippet()})); | |
1297 ASSERT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
1298 ASSERT_EQ("OK", service->snippets_fetcher()->last_status()); | |
1299 | |
1300 LoadFromJSONString(service.get(), GetTestJson({GetInvalidSnippet()})); | |
1301 EXPECT_THAT(service->snippets_fetcher()->last_status(), | |
1302 StartsWith("Received invalid JSON")); | |
1303 // This should not have changed the existing snippets. | |
1304 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
1305 } | |
1306 | |
1307 TEST_F(RemoteSuggestionsProviderTest, LoadIncompleteJson) { | |
1308 auto service = MakeSnippetsService(); | |
1309 | |
1310 LoadFromJSONString(service.get(), GetTestJson({GetIncompleteSnippet()})); | |
1311 EXPECT_EQ("Invalid / empty list.", | |
1312 service->snippets_fetcher()->last_status()); | |
1313 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), IsEmpty()); | |
1314 } | |
1315 | |
1316 TEST_F(RemoteSuggestionsProviderTest, LoadIncompleteJsonWithExistingSnippets) { | |
1317 auto service = MakeSnippetsService(); | |
1318 | |
1319 LoadFromJSONString(service.get(), GetTestJson({GetSnippet()})); | |
1320 ASSERT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
1321 | |
1322 LoadFromJSONString(service.get(), GetTestJson({GetIncompleteSnippet()})); | |
1323 EXPECT_EQ("Invalid / empty list.", | |
1324 service->snippets_fetcher()->last_status()); | |
1325 // This should not have changed the existing snippets. | |
1326 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
1327 } | |
1328 | |
1329 TEST_F(RemoteSuggestionsProviderTest, Dismiss) { | |
1330 auto service = MakeSnippetsService(); | |
1331 | |
1332 std::string json_str( | |
1333 GetTestJson({GetSnippetWithSources("http://site.com", "Source 1", "")})); | |
1334 | |
1335 LoadFromJSONString(service.get(), json_str); | |
1336 | |
1337 ASSERT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
1338 // Load the image to store it in the database. | |
1339 ServeImageCallback cb = | |
1340 base::Bind(&ServeOneByOneImage, &service->GetImageFetcherForTesting()); | |
1341 EXPECT_CALL(*image_fetcher(), StartOrQueueNetworkRequest(_, _, _)) | |
1342 .WillOnce(WithArgs<0, 2>(Invoke(&cb, &ServeImageCallback::Run))); | |
1343 image_decoder()->SetDecodedImage(gfx::test::CreateImage(1, 1)); | |
1344 gfx::Image image = FetchImage(service.get(), MakeArticleID(kSnippetUrl)); | |
1345 EXPECT_FALSE(image.IsEmpty()); | |
1346 EXPECT_EQ(1, image.Width()); | |
1347 | |
1348 // Dismissing a non-existent snippet shouldn't do anything. | |
1349 service->DismissSuggestion(MakeArticleID("http://othersite.com")); | |
1350 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
1351 | |
1352 // Dismiss the snippet. | |
1353 service->DismissSuggestion(MakeArticleID(kSnippetUrl)); | |
1354 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), IsEmpty()); | |
1355 | |
1356 // Verify we can still load the image of the discarded snippet (other NTPs | |
1357 // might still reference it). This should come from the database -- no network | |
1358 // fetch necessary. | |
1359 image_decoder()->SetDecodedImage(gfx::test::CreateImage(1, 1)); | |
1360 image = FetchImage(service.get(), MakeArticleID(kSnippetUrl)); | |
1361 EXPECT_FALSE(image.IsEmpty()); | |
1362 EXPECT_EQ(1, image.Width()); | |
1363 | |
1364 // Make sure that fetching the same snippet again does not re-add it. | |
1365 LoadFromJSONString(service.get(), json_str); | |
1366 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), IsEmpty()); | |
1367 | |
1368 // The snippet should stay dismissed even after re-creating the service. | |
1369 ResetSnippetsService(&service, /*set_empty_response=*/true); | |
1370 LoadFromJSONString(service.get(), json_str); | |
1371 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), IsEmpty()); | |
1372 | |
1373 // The snippet can be added again after clearing dismissed snippets. | |
1374 service->ClearDismissedSuggestionsForDebugging(articles_category()); | |
1375 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), IsEmpty()); | |
1376 LoadFromJSONString(service.get(), json_str); | |
1377 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
1378 } | |
1379 | |
1380 TEST_F(RemoteSuggestionsProviderTest, GetDismissed) { | |
1381 auto service = MakeSnippetsService(); | |
1382 | |
1383 LoadFromJSONString(service.get(), GetTestJson({GetSnippet()})); | |
1384 | |
1385 service->DismissSuggestion(MakeArticleID(kSnippetUrl)); | |
1386 | |
1387 service->GetDismissedSuggestionsForDebugging( | |
1388 articles_category(), | |
1389 base::Bind( | |
1390 [](RemoteSuggestionsProvider* service, | |
1391 RemoteSuggestionsProviderTest* test, | |
1392 std::vector<ContentSuggestion> dismissed_suggestions) { | |
1393 EXPECT_EQ(1u, dismissed_suggestions.size()); | |
1394 for (auto& suggestion : dismissed_suggestions) { | |
1395 EXPECT_EQ(test->MakeArticleID(kSnippetUrl), suggestion.id()); | |
1396 } | |
1397 }, | |
1398 service.get(), this)); | |
1399 base::RunLoop().RunUntilIdle(); | |
1400 | |
1401 // There should be no dismissed snippet after clearing the list. | |
1402 service->ClearDismissedSuggestionsForDebugging(articles_category()); | |
1403 service->GetDismissedSuggestionsForDebugging( | |
1404 articles_category(), | |
1405 base::Bind( | |
1406 [](RemoteSuggestionsProvider* service, | |
1407 RemoteSuggestionsProviderTest* test, | |
1408 std::vector<ContentSuggestion> dismissed_suggestions) { | |
1409 EXPECT_EQ(0u, dismissed_suggestions.size()); | |
1410 }, | |
1411 service.get(), this)); | |
1412 base::RunLoop().RunUntilIdle(); | |
1413 } | |
1414 | |
1415 TEST_F(RemoteSuggestionsProviderTest, CreationTimestampParseFail) { | |
1416 auto service = MakeSnippetsService(); | |
1417 | |
1418 std::string json = | |
1419 GetSnippetWithTimes(GetDefaultCreationTime(), GetDefaultExpirationTime()); | |
1420 base::ReplaceFirstSubstringAfterOffset( | |
1421 &json, 0, FormatTime(GetDefaultCreationTime()), "aaa1448459205"); | |
1422 std::string json_str(GetTestJson({json})); | |
1423 | |
1424 LoadFromJSONString(service.get(), json_str); | |
1425 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), IsEmpty()); | |
1426 } | |
1427 | |
1428 TEST_F(RemoteSuggestionsProviderTest, RemoveExpiredDismissedContent) { | |
1429 auto service = MakeSnippetsService(); | |
1430 | |
1431 std::string json_str1(GetTestJson({GetExpiredSnippet()})); | |
1432 // Load it. | |
1433 LoadFromJSONString(service.get(), json_str1); | |
1434 // Load the image to store it in the database. | |
1435 // TODO(tschumann): Introduce some abstraction to nicely work with image | |
1436 // fetching expectations. | |
1437 ServeImageCallback cb = | |
1438 base::Bind(&ServeOneByOneImage, &service->GetImageFetcherForTesting()); | |
1439 EXPECT_CALL(*image_fetcher(), StartOrQueueNetworkRequest(_, _, _)) | |
1440 .WillOnce(WithArgs<0, 2>(Invoke(&cb, &ServeImageCallback::Run))); | |
1441 image_decoder()->SetDecodedImage(gfx::test::CreateImage(1, 1)); | |
1442 gfx::Image image = FetchImage(service.get(), MakeArticleID(kSnippetUrl)); | |
1443 EXPECT_FALSE(image.IsEmpty()); | |
1444 EXPECT_EQ(1, image.Width()); | |
1445 | |
1446 // Dismiss the suggestion | |
1447 service->DismissSuggestion( | |
1448 ContentSuggestion::ID(articles_category(), kSnippetUrl)); | |
1449 | |
1450 // Load a different snippet - this will clear the expired dismissed ones. | |
1451 std::string json_str2(GetTestJson({GetSnippetWithUrl(kSnippetUrl2)})); | |
1452 LoadFromJSONString(service.get(), json_str2); | |
1453 | |
1454 EXPECT_THAT(service->GetDismissedSnippetsForTesting(articles_category()), | |
1455 IsEmpty()); | |
1456 | |
1457 // Verify the image got removed, too. | |
1458 EXPECT_TRUE(FetchImage(service.get(), MakeArticleID(kSnippetUrl)).IsEmpty()); | |
1459 } | |
1460 | |
1461 TEST_F(RemoteSuggestionsProviderTest, ExpiredContentNotRemoved) { | |
1462 auto service = MakeSnippetsService(); | |
1463 | |
1464 std::string json_str(GetTestJson({GetExpiredSnippet()})); | |
1465 | |
1466 LoadFromJSONString(service.get(), json_str); | |
1467 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
1468 } | |
1469 | |
1470 TEST_F(RemoteSuggestionsProviderTest, TestSingleSource) { | |
1471 auto service = MakeSnippetsService(); | |
1472 | |
1473 std::string json_str(GetTestJson({GetSnippetWithSources( | |
1474 "http://source1.com", "Source 1", "http://source1.amp.com")})); | |
1475 | |
1476 LoadFromJSONString(service.get(), json_str); | |
1477 ASSERT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
1478 const NTPSnippet& snippet = | |
1479 *service->GetSnippetsForTesting(articles_category()).front(); | |
1480 EXPECT_EQ(snippet.id(), kSnippetUrl); | |
1481 EXPECT_EQ(snippet.url(), GURL("http://source1.com")); | |
1482 EXPECT_EQ(snippet.publisher_name(), std::string("Source 1")); | |
1483 EXPECT_EQ(snippet.amp_url(), GURL("http://source1.amp.com")); | |
1484 } | |
1485 | |
1486 TEST_F(RemoteSuggestionsProviderTest, TestSingleSourceWithMalformedUrl) { | |
1487 auto service = MakeSnippetsService(); | |
1488 | |
1489 std::string json_str(GetTestJson({GetSnippetWithSources( | |
1490 "ceci n'est pas un url", "Source 1", "http://source1.amp.com")})); | |
1491 | |
1492 LoadFromJSONString(service.get(), json_str); | |
1493 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), IsEmpty()); | |
1494 } | |
1495 | |
1496 TEST_F(RemoteSuggestionsProviderTest, TestSingleSourceWithMissingData) { | |
1497 auto service = MakeSnippetsService(); | |
1498 | |
1499 std::string json_str( | |
1500 GetTestJson({GetSnippetWithSources("http://source1.com", "", "")})); | |
1501 | |
1502 LoadFromJSONString(service.get(), json_str); | |
1503 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), IsEmpty()); | |
1504 } | |
1505 | |
1506 TEST_F(RemoteSuggestionsProviderTest, LogNumArticlesHistogram) { | |
1507 auto service = MakeSnippetsService(); | |
1508 | |
1509 base::HistogramTester tester; | |
1510 LoadFromJSONString(service.get(), GetTestJson({GetInvalidSnippet()})); | |
1511 | |
1512 EXPECT_THAT(tester.GetAllSamples("NewTabPage.Snippets.NumArticles"), | |
1513 ElementsAre(base::Bucket(/*min=*/0, /*count=*/1))); | |
1514 | |
1515 // Invalid JSON shouldn't contribute to NumArticlesFetched. | |
1516 EXPECT_THAT(tester.GetAllSamples("NewTabPage.Snippets.NumArticlesFetched"), | |
1517 IsEmpty()); | |
1518 | |
1519 // Valid JSON with empty list. | |
1520 LoadFromJSONString(service.get(), GetTestJson(std::vector<std::string>())); | |
1521 EXPECT_THAT(tester.GetAllSamples("NewTabPage.Snippets.NumArticles"), | |
1522 ElementsAre(base::Bucket(/*min=*/0, /*count=*/2))); | |
1523 EXPECT_THAT(tester.GetAllSamples("NewTabPage.Snippets.NumArticlesFetched"), | |
1524 ElementsAre(base::Bucket(/*min=*/0, /*count=*/1))); | |
1525 | |
1526 // Snippet list should be populated with size 1. | |
1527 LoadFromJSONString(service.get(), GetTestJson({GetSnippet()})); | |
1528 EXPECT_THAT(tester.GetAllSamples("NewTabPage.Snippets.NumArticles"), | |
1529 ElementsAre(base::Bucket(/*min=*/0, /*count=*/2), | |
1530 base::Bucket(/*min=*/1, /*count=*/1))); | |
1531 EXPECT_THAT(tester.GetAllSamples("NewTabPage.Snippets.NumArticlesFetched"), | |
1532 ElementsAre(base::Bucket(/*min=*/0, /*count=*/1), | |
1533 base::Bucket(/*min=*/1, /*count=*/1))); | |
1534 | |
1535 // Duplicate snippet shouldn't increase the list size. | |
1536 LoadFromJSONString(service.get(), GetTestJson({GetSnippet()})); | |
1537 EXPECT_THAT(tester.GetAllSamples("NewTabPage.Snippets.NumArticles"), | |
1538 ElementsAre(base::Bucket(/*min=*/0, /*count=*/2), | |
1539 base::Bucket(/*min=*/1, /*count=*/2))); | |
1540 EXPECT_THAT(tester.GetAllSamples("NewTabPage.Snippets.NumArticlesFetched"), | |
1541 ElementsAre(base::Bucket(/*min=*/0, /*count=*/1), | |
1542 base::Bucket(/*min=*/1, /*count=*/2))); | |
1543 EXPECT_THAT( | |
1544 tester.GetAllSamples("NewTabPage.Snippets.NumArticlesZeroDueToDiscarded"), | |
1545 IsEmpty()); | |
1546 | |
1547 // Dismissing a snippet should decrease the list size. This will only be | |
1548 // logged after the next fetch. | |
1549 service->DismissSuggestion(MakeArticleID(kSnippetUrl)); | |
1550 LoadFromJSONString(service.get(), GetTestJson({GetSnippet()})); | |
1551 EXPECT_THAT(tester.GetAllSamples("NewTabPage.Snippets.NumArticles"), | |
1552 ElementsAre(base::Bucket(/*min=*/0, /*count=*/3), | |
1553 base::Bucket(/*min=*/1, /*count=*/2))); | |
1554 // Dismissed snippets shouldn't influence NumArticlesFetched. | |
1555 EXPECT_THAT(tester.GetAllSamples("NewTabPage.Snippets.NumArticlesFetched"), | |
1556 ElementsAre(base::Bucket(/*min=*/0, /*count=*/1), | |
1557 base::Bucket(/*min=*/1, /*count=*/3))); | |
1558 EXPECT_THAT( | |
1559 tester.GetAllSamples("NewTabPage.Snippets.NumArticlesZeroDueToDiscarded"), | |
1560 ElementsAre(base::Bucket(/*min=*/1, /*count=*/1))); | |
1561 | |
1562 // There is only a single, dismissed snippet in the database, so recreating | |
1563 // the service will require us to re-fetch. | |
1564 tester.ExpectTotalCount("NewTabPage.Snippets.NumArticlesFetched", 4); | |
1565 ResetSnippetsService(&service, /*set_empty_response=*/true); | |
1566 EXPECT_EQ(observer().StatusForCategory(articles_category()), | |
1567 CategoryStatus::AVAILABLE); | |
1568 tester.ExpectTotalCount("NewTabPage.Snippets.NumArticlesFetched", 5); | |
1569 EXPECT_THAT( | |
1570 tester.GetAllSamples("NewTabPage.Snippets.NumArticlesZeroDueToDiscarded"), | |
1571 ElementsAre(base::Bucket(/*min=*/1, /*count=*/2))); | |
1572 | |
1573 // But if there's a non-dismissed snippet in the database, recreating it | |
1574 // shouldn't trigger a fetch. | |
1575 LoadFromJSONString( | |
1576 service.get(), | |
1577 GetTestJson({GetSnippetWithUrl("http://not-dismissed.com")})); | |
1578 tester.ExpectTotalCount("NewTabPage.Snippets.NumArticlesFetched", 6); | |
1579 ResetSnippetsService(&service, /*set_empty_response=*/true); | |
1580 tester.ExpectTotalCount("NewTabPage.Snippets.NumArticlesFetched", 6); | |
1581 } | |
1582 | |
1583 TEST_F(RemoteSuggestionsProviderTest, DismissShouldRespectAllKnownUrls) { | |
1584 auto service = MakeSnippetsService(); | |
1585 | |
1586 const base::Time creation = GetDefaultCreationTime(); | |
1587 const base::Time expiry = GetDefaultExpirationTime(); | |
1588 const std::vector<std::string> source_urls = { | |
1589 "http://mashable.com/2016/05/11/stolen", | |
1590 "http://www.aol.com/article/2016/05/stolen-doggie"}; | |
1591 const std::vector<std::string> publishers = {"Mashable", "AOL"}; | |
1592 const std::vector<std::string> amp_urls = { | |
1593 "http://mashable-amphtml.googleusercontent.com/1", | |
1594 "http://t2.gstatic.com/images?q=tbn:3"}; | |
1595 | |
1596 // Add the snippet from the mashable domain. | |
1597 LoadFromJSONString(service.get(), | |
1598 GetTestJson({GetSnippetWithUrlAndTimesAndSource( | |
1599 source_urls, source_urls[0], creation, expiry, | |
1600 publishers[0], amp_urls[0])})); | |
1601 ASSERT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
1602 // Dismiss the snippet via the mashable source corpus ID. | |
1603 service->DismissSuggestion(MakeArticleID(source_urls[0])); | |
1604 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), IsEmpty()); | |
1605 | |
1606 // The same article from the AOL domain should now be detected as dismissed. | |
1607 LoadFromJSONString(service.get(), | |
1608 GetTestJson({GetSnippetWithUrlAndTimesAndSource( | |
1609 source_urls, source_urls[1], creation, expiry, | |
1610 publishers[1], amp_urls[1])})); | |
1611 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), IsEmpty()); | |
1612 } | |
1613 | |
1614 TEST_F(RemoteSuggestionsProviderTest, StatusChanges) { | |
1615 auto service = MakeSnippetsService(); | |
1616 | |
1617 // Simulate user signed out | |
1618 SetUpFetchResponse(GetTestJson({GetSnippet()})); | |
1619 service->OnStatusChanged(RemoteSuggestionsStatus::ENABLED_AND_SIGNED_IN, | |
1620 RemoteSuggestionsStatus::SIGNED_OUT_AND_DISABLED); | |
1621 | |
1622 base::RunLoop().RunUntilIdle(); | |
1623 EXPECT_THAT(observer().StatusForCategory(articles_category()), | |
1624 Eq(CategoryStatus::SIGNED_OUT)); | |
1625 EXPECT_THAT(RemoteSuggestionsProvider::State::DISABLED, Eq(service->state_)); | |
1626 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), | |
1627 IsEmpty()); // No fetch should be made. | |
1628 | |
1629 // Simulate user sign in. The service should be ready again and load snippets. | |
1630 SetUpFetchResponse(GetTestJson({GetSnippet()})); | |
1631 service->OnStatusChanged(RemoteSuggestionsStatus::SIGNED_OUT_AND_DISABLED, | |
1632 RemoteSuggestionsStatus::ENABLED_AND_SIGNED_IN); | |
1633 EXPECT_THAT(observer().StatusForCategory(articles_category()), | |
1634 Eq(CategoryStatus::AVAILABLE_LOADING)); | |
1635 | |
1636 base::RunLoop().RunUntilIdle(); | |
1637 EXPECT_THAT(observer().StatusForCategory(articles_category()), | |
1638 Eq(CategoryStatus::AVAILABLE)); | |
1639 EXPECT_THAT(RemoteSuggestionsProvider::State::READY, Eq(service->state_)); | |
1640 EXPECT_FALSE(service->GetSnippetsForTesting(articles_category()).empty()); | |
1641 } | |
1642 | |
1643 TEST_F(RemoteSuggestionsProviderTest, ImageReturnedWithTheSameId) { | |
1644 auto service = MakeSnippetsService(); | |
1645 | |
1646 LoadFromJSONString(service.get(), GetTestJson({GetSnippet()})); | |
1647 | |
1648 gfx::Image image; | |
1649 MockFunction<void(const gfx::Image&)> image_fetched; | |
1650 ServeImageCallback cb = | |
1651 base::Bind(&ServeOneByOneImage, &service->GetImageFetcherForTesting()); | |
1652 { | |
1653 InSequence s; | |
1654 EXPECT_CALL(*image_fetcher(), StartOrQueueNetworkRequest(_, _, _)) | |
1655 .WillOnce(WithArgs<0, 2>(Invoke(&cb, &ServeImageCallback::Run))); | |
1656 EXPECT_CALL(image_fetched, Call(_)).WillOnce(SaveArg<0>(&image)); | |
1657 } | |
1658 | |
1659 service->FetchSuggestionImage( | |
1660 MakeArticleID(kSnippetUrl), | |
1661 base::Bind(&MockFunction<void(const gfx::Image&)>::Call, | |
1662 base::Unretained(&image_fetched))); | |
1663 base::RunLoop().RunUntilIdle(); | |
1664 // Check that the image by ServeOneByOneImage is really served. | |
1665 EXPECT_EQ(1, image.Width()); | |
1666 } | |
1667 | |
1668 TEST_F(RemoteSuggestionsProviderTest, EmptyImageReturnedForNonExistentId) { | |
1669 auto service = MakeSnippetsService(); | |
1670 | |
1671 // Create a non-empty image so that we can test the image gets updated. | |
1672 gfx::Image image = gfx::test::CreateImage(1, 1); | |
1673 MockFunction<void(const gfx::Image&)> image_fetched; | |
1674 EXPECT_CALL(image_fetched, Call(_)).WillOnce(SaveArg<0>(&image)); | |
1675 | |
1676 service->FetchSuggestionImage( | |
1677 MakeArticleID(kSnippetUrl2), | |
1678 base::Bind(&MockFunction<void(const gfx::Image&)>::Call, | |
1679 base::Unretained(&image_fetched))); | |
1680 | |
1681 base::RunLoop().RunUntilIdle(); | |
1682 EXPECT_TRUE(image.IsEmpty()); | |
1683 } | |
1684 | |
1685 TEST_F(RemoteSuggestionsProviderTest, | |
1686 FetchingUnknownImageIdShouldNotHitDatabase) { | |
1687 // Testing that the provider is not accessing the database is tricky. | |
1688 // Therefore, we simply put in some data making sure that if the provider asks | |
1689 // the database, it will get a wrong answer. | |
1690 auto service = MakeSnippetsService(); | |
1691 | |
1692 ContentSuggestion::ID unknown_id = MakeArticleID(kSnippetUrl2); | |
1693 database()->SaveImage(unknown_id.id_within_category(), "some image blob"); | |
1694 // Set up the image decoder to always return the 1x1 test image. | |
1695 image_decoder()->SetDecodedImage(gfx::test::CreateImage(1, 1)); | |
1696 | |
1697 // Create a non-empty image so that we can test the image gets updated. | |
1698 gfx::Image image = gfx::test::CreateImage(2, 2); | |
1699 MockFunction<void(const gfx::Image&)> image_fetched; | |
1700 EXPECT_CALL(image_fetched, Call(_)).WillOnce(SaveArg<0>(&image)); | |
1701 | |
1702 service->FetchSuggestionImage( | |
1703 MakeArticleID(kSnippetUrl2), | |
1704 base::Bind(&MockFunction<void(const gfx::Image&)>::Call, | |
1705 base::Unretained(&image_fetched))); | |
1706 | |
1707 base::RunLoop().RunUntilIdle(); | |
1708 EXPECT_TRUE(image.IsEmpty()) << "got image with width: " << image.Width(); | |
1709 } | |
1710 | |
1711 TEST_F(RemoteSuggestionsProviderTest, ClearHistoryRemovesAllSuggestions) { | |
1712 auto service = MakeSnippetsService(); | |
1713 | |
1714 std::string first_snippet = GetSnippetWithUrl("http://url1.com"); | |
1715 std::string second_snippet = GetSnippetWithUrl("http://url2.com"); | |
1716 std::string json_str = GetTestJson({first_snippet, second_snippet}); | |
1717 LoadFromJSONString(service.get(), json_str); | |
1718 ASSERT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(2)); | |
1719 | |
1720 service->DismissSuggestion(MakeArticleID("http://url1.com")); | |
1721 ASSERT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
1722 ASSERT_THAT(service->GetDismissedSnippetsForTesting(articles_category()), | |
1723 SizeIs(1)); | |
1724 | |
1725 base::Time begin = base::Time::FromTimeT(123), | |
1726 end = base::Time::FromTimeT(456); | |
1727 base::Callback<bool(const GURL& url)> filter; | |
1728 service->ClearHistory(begin, end, filter); | |
1729 | |
1730 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), IsEmpty()); | |
1731 EXPECT_THAT(service->GetDismissedSnippetsForTesting(articles_category()), | |
1732 IsEmpty()); | |
1733 } | |
1734 | |
1735 TEST_F(RemoteSuggestionsProviderTest, SuggestionsFetchedOnSignInAndSignOut) { | |
1736 auto service = MakeSnippetsService(); | |
1737 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), IsEmpty()); | |
1738 | |
1739 // |MakeSnippetsService()| creates a service where user is signed in already, | |
1740 // so we start by signing out. | |
1741 SetUpFetchResponse(GetTestJson({GetSnippetN(1)})); | |
1742 service->OnStatusChanged(RemoteSuggestionsStatus::ENABLED_AND_SIGNED_IN, | |
1743 RemoteSuggestionsStatus::ENABLED_AND_SIGNED_OUT); | |
1744 base::RunLoop().RunUntilIdle(); | |
1745 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(1)); | |
1746 | |
1747 // Sign in to check a transition from signed out to signed in. | |
1748 SetUpFetchResponse(GetTestJson({GetSnippetN(1), GetSnippetN(2)})); | |
1749 service->OnStatusChanged(RemoteSuggestionsStatus::ENABLED_AND_SIGNED_OUT, | |
1750 RemoteSuggestionsStatus::ENABLED_AND_SIGNED_IN); | |
1751 base::RunLoop().RunUntilIdle(); | |
1752 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), SizeIs(2)); | |
1753 } | |
1754 | |
1755 TEST_F(RemoteSuggestionsProviderTest, ShouldClearOrphanedImagesOnRestart) { | |
1756 auto service = MakeSnippetsService(); | |
1757 | |
1758 LoadFromJSONString(service.get(), GetTestJson({GetSnippet()})); | |
1759 ServeImageCallback cb = | |
1760 base::Bind(&ServeOneByOneImage, &service->GetImageFetcherForTesting()); | |
1761 | |
1762 EXPECT_CALL(*image_fetcher(), StartOrQueueNetworkRequest(_, _, _)) | |
1763 .WillOnce(WithArgs<0, 2>(Invoke(&cb, &ServeImageCallback::Run))); | |
1764 image_decoder()->SetDecodedImage(gfx::test::CreateImage(1, 1)); | |
1765 | |
1766 gfx::Image image = FetchImage(service.get(), MakeArticleID(kSnippetUrl)); | |
1767 EXPECT_EQ(1, image.Width()); | |
1768 EXPECT_FALSE(image.IsEmpty()); | |
1769 | |
1770 // Send new suggestion which don't include the snippet referencing the image. | |
1771 LoadFromJSONString(service.get(), | |
1772 GetTestJson({GetSnippetWithUrl( | |
1773 "http://something.com/pletely/unrelated")})); | |
1774 // The image should still be available until a restart happens. | |
1775 EXPECT_FALSE(FetchImage(service.get(), MakeArticleID(kSnippetUrl)).IsEmpty()); | |
1776 ResetSnippetsService(&service, /*set_empty_response=*/true); | |
1777 // After the restart, the image should be garbage collected. | |
1778 EXPECT_TRUE(FetchImage(service.get(), MakeArticleID(kSnippetUrl)).IsEmpty()); | |
1779 } | |
1780 | |
1781 TEST_F(RemoteSuggestionsProviderTest, | |
1782 ShouldHandleMoreThanMaxSnippetsInResponse) { | |
1783 auto service = MakeSnippetsService(); | |
1784 | |
1785 std::vector<std::string> suggestions; | |
1786 for (int i = 0; i < service->GetMaxSnippetCountForTesting() + 1; ++i) { | |
1787 suggestions.push_back(GetSnippetWithUrl( | |
1788 base::StringPrintf("http://localhost/snippet-id-%d", i))); | |
1789 } | |
1790 LoadFromJSONString(service.get(), GetTestJson(suggestions)); | |
1791 // TODO(tschumann): We should probably trim out any additional results and | |
1792 // only serve the MaxSnippetCount items. | |
1793 EXPECT_THAT(service->GetSnippetsForTesting(articles_category()), | |
1794 SizeIs(service->GetMaxSnippetCountForTesting() + 1)); | |
1795 } | |
1796 | |
1797 TEST_F(RemoteSuggestionsProviderTest, StoreLastSuccessfullBackgroundFetchTime) { | |
1798 // On initialization of the RemoteSuggestionsProvider a background fetch is | |
1799 // triggered since the snippets DB is empty. Therefore the service must not be | |
1800 // initialized until the test clock is set. | |
1801 auto service = MakeSnippetsServiceWithoutInitialization(); | |
1802 | |
1803 auto simple_test_clock = base::MakeUnique<base::SimpleTestClock>(); | |
1804 base::SimpleTestClock* simple_test_clock_ptr = simple_test_clock.get(); | |
1805 service->SetClockForTesting(std::move(simple_test_clock)); | |
1806 | |
1807 // Test that the preference is correctly initialized with the default value 0. | |
1808 EXPECT_EQ( | |
1809 0, pref_service()->GetInt64(prefs::kLastSuccessfulBackgroundFetchTime)); | |
1810 | |
1811 WaitForSnippetsServiceInitialization(service.get(), | |
1812 /*set_empty_response=*/true); | |
1813 EXPECT_EQ( | |
1814 simple_test_clock_ptr->Now().ToInternalValue(), | |
1815 pref_service()->GetInt64(prefs::kLastSuccessfulBackgroundFetchTime)); | |
1816 | |
1817 // Advance the time and check whether the time was updated correctly after the | |
1818 // background fetch. | |
1819 simple_test_clock_ptr->Advance(TimeDelta::FromHours(1)); | |
1820 service->FetchSnippetsInTheBackground(); | |
1821 base::RunLoop().RunUntilIdle(); | |
1822 EXPECT_EQ( | |
1823 simple_test_clock_ptr->Now().ToInternalValue(), | |
1824 pref_service()->GetInt64(prefs::kLastSuccessfulBackgroundFetchTime)); | |
1825 // TODO(markusheintz): Add a test that simulates a browser restart once the | |
1826 // scheduler refactoring is done (crbug.com/672434). | |
1827 } | |
1828 | |
1829 } // namespace ntp_snippets | |
OLD | NEW |