Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(985)

Side by Side Diff: components/ntp_snippets/remote/remote_suggestions_provider_unittest.cc

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

Powered by Google App Engine
This is Rietveld 408576698