remote_suggestions_fetcher_impl_unittest.cc 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147
  1. // Copyright 2017 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. #include "components/ntp_snippets/remote/remote_suggestions_fetcher_impl.h"
  5. #include <map>
  6. #include <utility>
  7. #include <vector>
  8. #include "base/bind.h"
  9. #include "base/containers/circular_deque.h"
  10. #include "base/json/json_reader.h"
  11. #include "base/strings/stringprintf.h"
  12. #include "base/strings/utf_string_conversions.h"
  13. #include "base/test/metrics/histogram_tester.h"
  14. #include "base/test/scoped_feature_list.h"
  15. #include "base/test/task_environment.h"
  16. #include "base/threading/thread_task_runner_handle.h"
  17. #include "base/time/default_clock.h"
  18. #include "base/time/time.h"
  19. #include "base/values.h"
  20. #include "build/build_config.h"
  21. #include "components/ntp_snippets/category.h"
  22. #include "components/ntp_snippets/features.h"
  23. #include "components/ntp_snippets/ntp_snippets_constants.h"
  24. #include "components/ntp_snippets/remote/remote_suggestion.h"
  25. #include "components/ntp_snippets/remote/request_params.h"
  26. #include "components/ntp_snippets/remote/test_utils.h"
  27. #include "components/ntp_snippets/user_classifier.h"
  28. #include "components/prefs/testing_pref_service.h"
  29. #include "components/signin/public/identity_manager/identity_test_environment.h"
  30. #include "components/variations/entropy_provider.h"
  31. #include "components/variations/scoped_variations_ids_provider.h"
  32. #include "net/http/http_util.h"
  33. #include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
  34. #include "services/network/public/cpp/shared_url_loader_factory.h"
  35. #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
  36. #include "services/network/public/mojom/url_response_head.mojom.h"
  37. #include "services/network/test/test_url_loader_factory.h"
  38. #include "testing/gmock/include/gmock/gmock.h"
  39. #include "testing/gtest/include/gtest/gtest.h"
  40. #include "third_party/abseil-cpp/absl/types/optional.h"
  41. namespace ntp_snippets {
  42. namespace {
  43. using testing::_;
  44. using testing::AllOf;
  45. using testing::ElementsAre;
  46. using testing::Eq;
  47. using testing::Field;
  48. using testing::IsEmpty;
  49. using testing::Not;
  50. using testing::NotNull;
  51. using testing::Property;
  52. using testing::StartsWith;
  53. const char kAPIKey[] = "fakeAPIkey";
  54. const char kTestEmail[] = "foo@bar.com";
  55. const char kFetchSuggestionsEndpoint[] =
  56. "https://chromefeedcontentsuggestions-pa.googleapis.com/v2/suggestions/"
  57. "fetch";
  58. // Artificial time delay for JSON parsing.
  59. const int64_t kTestJsonParsingLatencyMs = 20;
  60. ACTION_P(MoveArgument1PointeeTo, ptr) {
  61. *ptr = std::move(*arg1);
  62. }
  63. MATCHER(IsNullCategoriesList, "is a null list of categories") {
  64. RemoteSuggestionsFetcher::OptionalFetchedCategories& fetched_categories =
  65. *arg;
  66. return !fetched_categories.has_value();
  67. }
  68. MATCHER(IsEmptyCategoriesList, "is an empty list of categories") {
  69. RemoteSuggestionsFetcher::OptionalFetchedCategories& fetched_categories =
  70. *arg;
  71. return fetched_categories && fetched_categories->empty();
  72. }
  73. MATCHER(IsEmptyArticleList, "is an empty list of articles") {
  74. RemoteSuggestionsFetcher::OptionalFetchedCategories& fetched_categories =
  75. *arg;
  76. return fetched_categories && fetched_categories->size() == 1 &&
  77. fetched_categories->begin()->suggestions.empty();
  78. }
  79. MATCHER_P(IsSingleArticle, url, "is a list with the single article %(url)s") {
  80. RemoteSuggestionsFetcher::OptionalFetchedCategories& fetched_categories =
  81. *arg;
  82. if (!fetched_categories) {
  83. *result_listener << "got empty categories.";
  84. return false;
  85. }
  86. if (fetched_categories->size() != 1) {
  87. *result_listener << "expected single category.";
  88. return false;
  89. }
  90. auto category = fetched_categories->begin();
  91. if (category->suggestions.size() != 1) {
  92. *result_listener << "expected single snippet, got: "
  93. << category->suggestions.size();
  94. return false;
  95. }
  96. if (category->suggestions[0]->url().spec() != url) {
  97. *result_listener << "unexpected url, got: "
  98. << category->suggestions[0]->url().spec();
  99. return false;
  100. }
  101. return true;
  102. }
  103. MATCHER(IsCategoryInfoForArticles, "") {
  104. if (arg.additional_action() != ContentSuggestionsAdditionalAction::FETCH) {
  105. *result_listener << "missing expected FETCH action";
  106. return false;
  107. }
  108. if (!arg.show_if_empty()) {
  109. *result_listener << "missing expected show_if_empty";
  110. return false;
  111. }
  112. return true;
  113. }
  114. MATCHER_P(FirstCategoryHasInfo, info_matcher, "") {
  115. if (!arg->has_value() || arg->value().size() == 0) {
  116. *result_listener << "No category found.";
  117. }
  118. return testing::ExplainMatchResult(info_matcher, arg->value().front().info,
  119. result_listener);
  120. }
  121. class MockSnippetsAvailableCallback {
  122. public:
  123. // Workaround for gMock's lack of support for movable arguments.
  124. void WrappedRun(
  125. Status status,
  126. RemoteSuggestionsFetcher::OptionalFetchedCategories fetched_categories) {
  127. Run(status, &fetched_categories);
  128. }
  129. MOCK_METHOD2(Run,
  130. void(Status status,
  131. RemoteSuggestionsFetcher::OptionalFetchedCategories*
  132. fetched_categories));
  133. };
  134. void ParseJson(const std::string& json,
  135. SuccessCallback success_callback,
  136. ErrorCallback error_callback) {
  137. auto parsed_json = base::JSONReader::ReadAndReturnValueWithError(json);
  138. if (parsed_json.has_value()) {
  139. std::move(success_callback).Run(std::move(*parsed_json));
  140. } else {
  141. std::move(error_callback).Run(std::move(parsed_json.error().message));
  142. }
  143. }
  144. void ParseJsonDelayed(const std::string& json,
  145. SuccessCallback success_callback,
  146. ErrorCallback error_callback) {
  147. base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
  148. FROM_HERE,
  149. base::BindOnce(&ParseJson, json, std::move(success_callback),
  150. std::move(error_callback)),
  151. base::Milliseconds(kTestJsonParsingLatencyMs));
  152. }
  153. } // namespace
  154. class RemoteSuggestionsFetcherImplTest : public testing::Test {
  155. public:
  156. RemoteSuggestionsFetcherImplTest()
  157. : default_variation_params_(
  158. {{"send_top_languages", "true"},
  159. {"send_user_class", "true"},
  160. {"append_request_priority_as_query_parameter", "true"}}) {
  161. scoped_feature_list_.InitAndEnableFeatureWithParameters(
  162. ntp_snippets::kArticleSuggestionsFeature, default_variation_params_);
  163. UserClassifier::RegisterProfilePrefs(utils_.pref_service()->registry());
  164. user_classifier_ = std::make_unique<UserClassifier>(
  165. utils_.pref_service(), base::DefaultClock::GetInstance());
  166. ResetFetcher();
  167. }
  168. RemoteSuggestionsFetcherImplTest(const RemoteSuggestionsFetcherImplTest&) =
  169. delete;
  170. RemoteSuggestionsFetcherImplTest& operator=(
  171. const RemoteSuggestionsFetcherImplTest&) = delete;
  172. ~RemoteSuggestionsFetcherImplTest() override {}
  173. void ResetFetcher() { ResetFetcherWithAPIKey(kAPIKey); }
  174. void ResetFetcherWithAPIKey(const std::string& api_key) {
  175. scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory =
  176. base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
  177. &test_url_loader_factory_);
  178. fetcher_ = std::make_unique<RemoteSuggestionsFetcherImpl>(
  179. identity_test_env_.identity_manager(),
  180. std::move(test_shared_loader_factory), utils_.pref_service(), nullptr,
  181. base::BindRepeating(&ParseJsonDelayed), GetFetchEndpoint(), api_key,
  182. user_classifier_.get());
  183. fetcher_->SetClockForTesting(task_environment_.GetMockClock());
  184. }
  185. void SignIn() {
  186. identity_test_env_.MakePrimaryAccountAvailable(kTestEmail,
  187. signin::ConsentLevel::kSync);
  188. }
  189. RemoteSuggestionsFetcher::SnippetsAvailableCallback
  190. ToSnippetsAvailableCallback(MockSnippetsAvailableCallback* callback) {
  191. return base::BindOnce(&MockSnippetsAvailableCallback::WrappedRun,
  192. base::Unretained(callback));
  193. }
  194. RemoteSuggestionsFetcherImpl& fetcher() { return *fetcher_; }
  195. MockSnippetsAvailableCallback& mock_callback() { return mock_callback_; }
  196. void FastForwardUntilNoTasksRemain() {
  197. task_environment_.FastForwardUntilNoTasksRemain();
  198. }
  199. base::HistogramTester& histogram_tester() { return histogram_tester_; }
  200. RequestParams test_params() {
  201. RequestParams result;
  202. result.count_to_fetch = 1;
  203. result.interactive_request = true;
  204. return result;
  205. }
  206. void SetVariationParam(std::string param_name, std::string value) {
  207. std::map<std::string, std::string> params = default_variation_params_;
  208. params[param_name] = value;
  209. scoped_feature_list_.Reset();
  210. scoped_feature_list_.InitAndEnableFeatureWithParameters(
  211. ntp_snippets::kArticleSuggestionsFeature, params);
  212. }
  213. void SetFakeResponse(const GURL& request_url,
  214. const std::string& response_data,
  215. net::HttpStatusCode response_code,
  216. net::Error error) {
  217. auto head = network::mojom::URLResponseHead::New();
  218. std::string headers(base::StringPrintf(
  219. "HTTP/1.1 %d %s\nContent-type: application/json\n\n",
  220. static_cast<int>(response_code), GetHttpReasonPhrase(response_code)));
  221. head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
  222. net::HttpUtil::AssembleRawHeaders(headers));
  223. head->mime_type = "application/json";
  224. network::URLLoaderCompletionStatus status(error);
  225. status.decoded_body_length = response_data.size();
  226. test_url_loader_factory_.AddResponse(request_url, std::move(head),
  227. response_data, status);
  228. }
  229. protected:
  230. base::test::TaskEnvironment task_environment_{
  231. base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  232. std::map<std::string, std::string> default_variation_params_;
  233. signin::IdentityTestEnvironment identity_test_env_;
  234. network::TestURLLoaderFactory test_url_loader_factory_;
  235. private:
  236. test::RemoteSuggestionsTestUtils utils_;
  237. base::test::ScopedFeatureList scoped_feature_list_;
  238. variations::ScopedVariationsIdsProvider scoped_variations_ids_provider_{
  239. variations::VariationsIdsProvider::Mode::kUseSignedInState};
  240. std::unique_ptr<RemoteSuggestionsFetcherImpl> fetcher_;
  241. std::unique_ptr<UserClassifier> user_classifier_;
  242. MockSnippetsAvailableCallback mock_callback_;
  243. GURL test_url_;
  244. base::HistogramTester histogram_tester_;
  245. };
  246. TEST_F(RemoteSuggestionsFetcherImplTest, ShouldNotFetchOnCreation) {
  247. // The lack of registered baked in responses would cause any fetch to fail.
  248. FastForwardUntilNoTasksRemain();
  249. EXPECT_THAT(histogram_tester().GetAllSamples(
  250. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  251. IsEmpty());
  252. EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
  253. IsEmpty());
  254. EXPECT_THAT(fetcher().GetLastStatusForDebugging(), IsEmpty());
  255. }
  256. TEST_F(RemoteSuggestionsFetcherImplTest, ShouldFetchSuccessfully) {
  257. const std::string kJsonStr =
  258. "{\"categories\" : [{"
  259. " \"id\": 1,"
  260. " \"localizedTitle\": \"Articles for You\","
  261. " \"suggestions\" : [{"
  262. " \"ids\" : [\"http://localhost/foobar\"],"
  263. " \"title\" : \"Foo Barred from Baz\","
  264. " \"snippet\" : \"...\","
  265. " \"fullPageUrl\" : \"http://localhost/foobar\","
  266. " \"creationTime\" : \"2016-06-30T11:01:37.000Z\","
  267. " \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
  268. " \"attribution\" : \"Foo News\","
  269. " \"imageUrl\" : \"http://localhost/foobar.jpg\","
  270. " \"ampUrl\" : \"http://localhost/amp\","
  271. " \"faviconUrl\" : \"http://localhost/favicon.ico\" "
  272. " }]"
  273. "}]}";
  274. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  275. "?key=fakeAPIkey&priority=user_action"),
  276. /*response_data=*/kJsonStr, net::HTTP_OK, net::OK);
  277. EXPECT_CALL(mock_callback(),
  278. Run(Property(&Status::IsSuccess, true),
  279. /*fetched_categories=*/AllOf(
  280. IsSingleArticle("http://localhost/foobar"),
  281. FirstCategoryHasInfo(IsCategoryInfoForArticles()))));
  282. fetcher().FetchSnippets(test_params(),
  283. ToSnippetsAvailableCallback(&mock_callback()));
  284. FastForwardUntilNoTasksRemain();
  285. EXPECT_THAT(fetcher().GetLastStatusForDebugging(), Eq("OK"));
  286. EXPECT_THAT(fetcher().GetLastJsonForDebugging(), Eq(kJsonStr));
  287. EXPECT_THAT(histogram_tester().GetAllSamples(
  288. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  289. ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
  290. EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
  291. ElementsAre(base::Bucket(/*min=*/kTestJsonParsingLatencyMs,
  292. /*count=*/1)));
  293. }
  294. TEST_F(RemoteSuggestionsFetcherImplTest, ShouldExposeRequestPriorityInUrl) {
  295. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  296. "?key=fakeAPIkey&priority=background_prefetch"),
  297. /*response_data=*/"{\"categories\" : []}", net::HTTP_OK,
  298. net::OK);
  299. EXPECT_CALL(mock_callback(), Run(Property(&Status::IsSuccess, true),
  300. /*fetched_categories=*/_));
  301. RequestParams params = test_params();
  302. params.interactive_request = false;
  303. fetcher().FetchSnippets(params,
  304. ToSnippetsAvailableCallback(&mock_callback()));
  305. // Wait for the fake response.
  306. FastForwardUntilNoTasksRemain();
  307. EXPECT_THAT(fetcher().GetLastStatusForDebugging(), Eq("OK"));
  308. }
  309. TEST_F(RemoteSuggestionsFetcherImplTest,
  310. ShouldNotExposeRequestPriorityInUrlWhenDisabled) {
  311. SetVariationParam("append_request_priority_as_query_parameter", "false");
  312. SetFakeResponse(
  313. GURL(std::string(kFetchSuggestionsEndpoint) + "?key=fakeAPIkey"),
  314. /*response_data=*/"{\"categories\" : []}", net::HTTP_OK, net::OK);
  315. EXPECT_CALL(mock_callback(), Run(Property(&Status::IsSuccess, true),
  316. /*fetched_categories=*/_));
  317. RequestParams params = test_params();
  318. params.interactive_request = false;
  319. fetcher().FetchSnippets(params,
  320. ToSnippetsAvailableCallback(&mock_callback()));
  321. // Wait for the fake response.
  322. FastForwardUntilNoTasksRemain();
  323. EXPECT_THAT(fetcher().GetLastStatusForDebugging(), Eq("OK"));
  324. }
  325. TEST_F(RemoteSuggestionsFetcherImplTest, ShouldFetchSuccessfullyWhenSignedIn) {
  326. SignIn();
  327. const std::string kJsonStr =
  328. "{\"categories\" : [{"
  329. " \"id\": 1,"
  330. " \"localizedTitle\": \"Articles for You\","
  331. " \"suggestions\" : [{"
  332. " \"ids\" : [\"http://localhost/foobar\"],"
  333. " \"title\" : \"Foo Barred from Baz\","
  334. " \"snippet\" : \"...\","
  335. " \"fullPageUrl\" : \"http://localhost/foobar\","
  336. " \"creationTime\" : \"2016-06-30T11:01:37.000Z\","
  337. " \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
  338. " \"attribution\" : \"Foo News\","
  339. " \"imageUrl\" : \"http://localhost/foobar.jpg\","
  340. " \"ampUrl\" : \"http://localhost/amp\","
  341. " \"faviconUrl\" : \"http://localhost/favicon.ico\" "
  342. " }]"
  343. "}]}";
  344. SetFakeResponse(
  345. GURL(std::string(kFetchSuggestionsEndpoint) + "?priority=user_action"),
  346. /*response_data=*/kJsonStr, net::HTTP_OK, net::OK);
  347. EXPECT_CALL(mock_callback(),
  348. Run(Property(&Status::IsSuccess, true),
  349. /*fetched_categories=*/AllOf(
  350. IsSingleArticle("http://localhost/foobar"),
  351. FirstCategoryHasInfo(IsCategoryInfoForArticles()))));
  352. fetcher().FetchSnippets(test_params(),
  353. ToSnippetsAvailableCallback(&mock_callback()));
  354. identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
  355. "access_token", base::Time::Max());
  356. // Wait for the fake response.
  357. FastForwardUntilNoTasksRemain();
  358. EXPECT_THAT(fetcher().GetLastStatusForDebugging(), Eq("OK"));
  359. EXPECT_THAT(fetcher().GetLastJsonForDebugging(), Eq(kJsonStr));
  360. EXPECT_THAT(histogram_tester().GetAllSamples(
  361. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  362. ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
  363. EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
  364. ElementsAre(base::Bucket(/*min=*/kTestJsonParsingLatencyMs,
  365. /*count=*/1)));
  366. }
  367. TEST_F(RemoteSuggestionsFetcherImplTest,
  368. ShouldExposeRequestPriorityInUrlWhenSignedIn) {
  369. SignIn();
  370. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  371. "?priority=background_prefetch"),
  372. /*response_data=*/"{\"categories\" : []}", net::HTTP_OK,
  373. net::OK);
  374. EXPECT_CALL(mock_callback(), Run(Property(&Status::IsSuccess, true),
  375. /*fetched_categories=*/_));
  376. RequestParams params = test_params();
  377. params.interactive_request = false;
  378. fetcher().FetchSnippets(params,
  379. ToSnippetsAvailableCallback(&mock_callback()));
  380. identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
  381. "access_token", base::Time::Max());
  382. // Wait for the fake response.
  383. FastForwardUntilNoTasksRemain();
  384. EXPECT_THAT(fetcher().GetLastStatusForDebugging(), Eq("OK"));
  385. }
  386. TEST_F(RemoteSuggestionsFetcherImplTest,
  387. ShouldNotExposeRequestPriorityInUrlWhenDisabledWhenSignedIn) {
  388. SetVariationParam("append_request_priority_as_query_parameter", "false");
  389. SignIn();
  390. SetFakeResponse(GURL(kFetchSuggestionsEndpoint),
  391. /*response_data=*/"{\"categories\" : []}", net::HTTP_OK,
  392. net::OK);
  393. EXPECT_CALL(mock_callback(), Run(Property(&Status::IsSuccess, true),
  394. /*fetched_categories=*/_));
  395. RequestParams params = test_params();
  396. params.interactive_request = false;
  397. fetcher().FetchSnippets(params,
  398. ToSnippetsAvailableCallback(&mock_callback()));
  399. identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
  400. "access_token", base::Time::Max());
  401. // Wait for the fake response.
  402. FastForwardUntilNoTasksRemain();
  403. EXPECT_THAT(fetcher().GetLastStatusForDebugging(), Eq("OK"));
  404. }
  405. TEST_F(RemoteSuggestionsFetcherImplTest,
  406. ShouldRetryWhenOAuthCancelledWhenSignedIn) {
  407. SignIn();
  408. const std::string kJsonStr =
  409. "{\"categories\" : [{"
  410. " \"id\": 1,"
  411. " \"localizedTitle\": \"Articles for You\","
  412. " \"suggestions\" : [{"
  413. " \"ids\" : [\"http://localhost/foobar\"],"
  414. " \"title\" : \"Foo Barred from Baz\","
  415. " \"snippet\" : \"...\","
  416. " \"fullPageUrl\" : \"http://localhost/foobar\","
  417. " \"creationTime\" : \"2016-06-30T11:01:37.000Z\","
  418. " \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
  419. " \"attribution\" : \"Foo News\","
  420. " \"imageUrl\" : \"http://localhost/foobar.jpg\","
  421. " \"ampUrl\" : \"http://localhost/amp\","
  422. " \"faviconUrl\" : \"http://localhost/favicon.ico\" "
  423. " }]"
  424. "}]}";
  425. SetFakeResponse(
  426. GURL(std::string(kFetchSuggestionsEndpoint) + "?priority=user_action"),
  427. /*response_data=*/kJsonStr, net::HTTP_OK, net::OK);
  428. EXPECT_CALL(mock_callback(),
  429. Run(Property(&Status::IsSuccess, true),
  430. /*fetched_categories=*/AllOf(
  431. IsSingleArticle("http://localhost/foobar"),
  432. FirstCategoryHasInfo(IsCategoryInfoForArticles()))));
  433. fetcher().FetchSnippets(test_params(),
  434. ToSnippetsAvailableCallback(&mock_callback()));
  435. // Cancel the first access token request that's made.
  436. identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
  437. GoogleServiceAuthError(GoogleServiceAuthError::State::REQUEST_CANCELED));
  438. // RemoteSuggestionsFetcher should retry fetching an access token if the first
  439. // attempt is cancelled. Respond with a valid access token on the retry.
  440. identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
  441. "access_token", base::Time::Max());
  442. // Wait for the fake response.
  443. FastForwardUntilNoTasksRemain();
  444. EXPECT_THAT(fetcher().GetLastStatusForDebugging(), Eq("OK"));
  445. EXPECT_THAT(fetcher().GetLastJsonForDebugging(), Eq(kJsonStr));
  446. EXPECT_THAT(histogram_tester().GetAllSamples(
  447. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  448. ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
  449. EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
  450. ElementsAre(base::Bucket(/*min=*/kTestJsonParsingLatencyMs,
  451. /*count=*/1)));
  452. }
  453. TEST_F(RemoteSuggestionsFetcherImplTest, EmptyCategoryIsOK) {
  454. const std::string kJsonStr =
  455. "{\"categories\" : [{"
  456. " \"id\": 1,"
  457. " \"localizedTitle\": \"Articles for You\""
  458. "}]}";
  459. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  460. "?key=fakeAPIkey&priority=user_action"),
  461. /*response_data=*/kJsonStr, net::HTTP_OK, net::OK);
  462. EXPECT_CALL(mock_callback(),
  463. Run(Property(&Status::IsSuccess, true),
  464. /*fetched_categories=*/IsEmptyArticleList()));
  465. fetcher().FetchSnippets(test_params(),
  466. ToSnippetsAvailableCallback(&mock_callback()));
  467. FastForwardUntilNoTasksRemain();
  468. EXPECT_THAT(fetcher().GetLastStatusForDebugging(), Eq("OK"));
  469. EXPECT_THAT(fetcher().GetLastJsonForDebugging(), Eq(kJsonStr));
  470. EXPECT_THAT(histogram_tester().GetAllSamples(
  471. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  472. ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
  473. EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
  474. ElementsAre(base::Bucket(/*min=*/kTestJsonParsingLatencyMs,
  475. /*count=*/1)));
  476. }
  477. TEST_F(RemoteSuggestionsFetcherImplTest, ServerCategories) {
  478. const std::string kJsonStr =
  479. "{\"categories\" : [{"
  480. " \"id\": 1,"
  481. " \"localizedTitle\": \"Articles for You\","
  482. " \"suggestions\" : [{"
  483. " \"ids\" : [\"http://localhost/foobar\"],"
  484. " \"title\" : \"Foo Barred from Baz\","
  485. " \"snippet\" : \"...\","
  486. " \"fullPageUrl\" : \"http://localhost/foobar\","
  487. " \"creationTime\" : \"2016-06-30T11:01:37.000Z\","
  488. " \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
  489. " \"attribution\" : \"Foo News\","
  490. " \"imageUrl\" : \"http://localhost/foobar.jpg\","
  491. " \"ampUrl\" : \"http://localhost/amp\","
  492. " \"faviconUrl\" : \"http://localhost/favicon.ico\" "
  493. " }]"
  494. "}, {"
  495. " \"id\": 2,"
  496. " \"localizedTitle\": \"Articles for Me\","
  497. " \"allowFetchingMoreResults\": true,"
  498. " \"suggestions\" : [{"
  499. " \"ids\" : [\"http://localhost/foo2\"],"
  500. " \"title\" : \"Foo Barred from Baz\","
  501. " \"snippet\" : \"...\","
  502. " \"fullPageUrl\" : \"http://localhost/foo2\","
  503. " \"creationTime\" : \"2016-06-30T11:01:37.000Z\","
  504. " \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
  505. " \"attribution\" : \"Foo News\","
  506. " \"imageUrl\" : \"http://localhost/foo2.jpg\","
  507. " \"ampUrl\" : \"http://localhost/amp\","
  508. " \"faviconUrl\" : \"http://localhost/favicon.ico\" "
  509. " }]"
  510. "}]}";
  511. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  512. "?key=fakeAPIkey&priority=user_action"),
  513. /*response_data=*/kJsonStr, net::HTTP_OK, net::OK);
  514. RemoteSuggestionsFetcher::OptionalFetchedCategories fetched_categories;
  515. EXPECT_CALL(mock_callback(),
  516. Run(Property(&Status::IsSuccess, true), /*fetched_categories=*/_))
  517. .WillOnce(MoveArgument1PointeeTo(&fetched_categories));
  518. fetcher().FetchSnippets(test_params(),
  519. ToSnippetsAvailableCallback(&mock_callback()));
  520. FastForwardUntilNoTasksRemain();
  521. ASSERT_TRUE(fetched_categories);
  522. ASSERT_THAT(fetched_categories->size(), Eq(2u));
  523. for (const auto& category : *fetched_categories) {
  524. const auto& articles = category.suggestions;
  525. if (category.category.IsKnownCategory(KnownCategories::ARTICLES)) {
  526. ASSERT_THAT(articles.size(), Eq(1u));
  527. EXPECT_THAT(articles[0]->url().spec(), Eq("http://localhost/foobar"));
  528. EXPECT_THAT(category.info, IsCategoryInfoForArticles());
  529. } else if (category.category == Category::FromRemoteCategory(2)) {
  530. ASSERT_THAT(articles.size(), Eq(1u));
  531. EXPECT_THAT(articles[0]->url().spec(), Eq("http://localhost/foo2"));
  532. EXPECT_THAT(category.info.additional_action(),
  533. Eq(ContentSuggestionsAdditionalAction::FETCH));
  534. EXPECT_THAT(category.info.show_if_empty(), Eq(false));
  535. } else {
  536. FAIL() << "unknown category ID " << category.category.id();
  537. }
  538. }
  539. EXPECT_THAT(fetcher().GetLastStatusForDebugging(), Eq("OK"));
  540. EXPECT_THAT(fetcher().GetLastJsonForDebugging(), Eq(kJsonStr));
  541. EXPECT_THAT(histogram_tester().GetAllSamples(
  542. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  543. ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
  544. EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
  545. ElementsAre(base::Bucket(/*min=*/kTestJsonParsingLatencyMs,
  546. /*count=*/1)));
  547. }
  548. TEST_F(RemoteSuggestionsFetcherImplTest,
  549. SupportMissingAllowFetchingMoreResultsOption) {
  550. // This tests makes sure we handle the missing option although it's required
  551. // by the interface. It's just that the Service doesn't follow that
  552. // requirement (yet). TODO(tschumann): remove this test once not needed
  553. // anymore.
  554. const std::string kJsonStr =
  555. "{\"categories\" : [{"
  556. " \"id\": 2,"
  557. " \"localizedTitle\": \"Articles for Me\","
  558. " \"suggestions\" : [{"
  559. " \"ids\" : [\"http://localhost/foo2\"],"
  560. " \"title\" : \"Foo Barred from Baz\","
  561. " \"snippet\" : \"...\","
  562. " \"fullPageUrl\" : \"http://localhost/foo2\","
  563. " \"creationTime\" : \"2016-06-30T11:01:37.000Z\","
  564. " \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
  565. " \"attribution\" : \"Foo News\","
  566. " \"imageUrl\" : \"http://localhost/foo2.jpg\","
  567. " \"ampUrl\" : \"http://localhost/amp\","
  568. " \"faviconUrl\" : \"http://localhost/favicon.ico\" "
  569. " }]"
  570. "}]}";
  571. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  572. "?key=fakeAPIkey&priority=user_action"),
  573. /*response_data=*/kJsonStr, net::HTTP_OK, net::OK);
  574. RemoteSuggestionsFetcher::OptionalFetchedCategories fetched_categories;
  575. EXPECT_CALL(mock_callback(),
  576. Run(Property(&Status::IsSuccess, true), /*fetched_categories=*/_))
  577. .WillOnce(MoveArgument1PointeeTo(&fetched_categories));
  578. fetcher().FetchSnippets(test_params(),
  579. ToSnippetsAvailableCallback(&mock_callback()));
  580. FastForwardUntilNoTasksRemain();
  581. ASSERT_TRUE(fetched_categories);
  582. ASSERT_THAT(fetched_categories->size(), Eq(1u));
  583. EXPECT_THAT(fetched_categories->front().info.additional_action(),
  584. Eq(ContentSuggestionsAdditionalAction::NONE));
  585. EXPECT_THAT(fetched_categories->front().info.title(), Eq(u"Articles for Me"));
  586. }
  587. TEST_F(RemoteSuggestionsFetcherImplTest, ExclusiveCategoryOnly) {
  588. const std::string kJsonStr =
  589. "{\"categories\" : [{"
  590. " \"id\": 1,"
  591. " \"localizedTitle\": \"Articles for You\","
  592. " \"suggestions\" : [{"
  593. " \"ids\" : [\"http://localhost/foobar\"],"
  594. " \"title\" : \"Foo Barred from Baz\","
  595. " \"snippet\" : \"...\","
  596. " \"fullPageUrl\" : \"http://localhost/foobar\","
  597. " \"creationTime\" : \"2016-06-30T11:01:37.000Z\","
  598. " \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
  599. " \"attribution\" : \"Foo News\","
  600. " \"imageUrl\" : \"http://localhost/foobar.jpg\","
  601. " \"ampUrl\" : \"http://localhost/amp\","
  602. " \"faviconUrl\" : \"http://localhost/favicon.ico\" "
  603. " }]"
  604. "}, {"
  605. " \"id\": 2,"
  606. " \"localizedTitle\": \"Articles for Me\","
  607. " \"suggestions\" : [{"
  608. " \"ids\" : [\"http://localhost/foo2\"],"
  609. " \"title\" : \"Foo Barred from Baz\","
  610. " \"snippet\" : \"...\","
  611. " \"fullPageUrl\" : \"http://localhost/foo2\","
  612. " \"creationTime\" : \"2016-06-30T11:01:37.000Z\","
  613. " \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
  614. " \"attribution\" : \"Foo News\","
  615. " \"imageUrl\" : \"http://localhost/foo2.jpg\","
  616. " \"ampUrl\" : \"http://localhost/amp\","
  617. " \"faviconUrl\" : \"http://localhost/favicon.ico\" "
  618. " }]"
  619. "}, {"
  620. " \"id\": 3,"
  621. " \"localizedTitle\": \"Articles for Anybody\","
  622. " \"suggestions\" : [{"
  623. " \"ids\" : [\"http://localhost/foo3\"],"
  624. " \"title\" : \"Foo Barred from Baz\","
  625. " \"snippet\" : \"...\","
  626. " \"fullPageUrl\" : \"http://localhost/foo3\","
  627. " \"creationTime\" : \"2016-06-30T11:01:37.000Z\","
  628. " \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
  629. " \"attribution\" : \"Foo News\","
  630. " \"imageUrl\" : \"http://localhost/foo3.jpg\","
  631. " \"ampUrl\" : \"http://localhost/amp\","
  632. " \"faviconUrl\" : \"http://localhost/favicon.ico\" "
  633. " }]"
  634. "}]}";
  635. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  636. "?key=fakeAPIkey&priority=user_action"),
  637. /*response_data=*/kJsonStr, net::HTTP_OK, net::OK);
  638. RemoteSuggestionsFetcher::OptionalFetchedCategories fetched_categories;
  639. EXPECT_CALL(mock_callback(),
  640. Run(Property(&Status::IsSuccess, true), /*fetched_categories=*/_))
  641. .WillOnce(MoveArgument1PointeeTo(&fetched_categories));
  642. RequestParams params = test_params();
  643. params.exclusive_category =
  644. absl::optional<Category>(Category::FromRemoteCategory(2));
  645. fetcher().FetchSnippets(params,
  646. ToSnippetsAvailableCallback(&mock_callback()));
  647. FastForwardUntilNoTasksRemain();
  648. ASSERT_TRUE(fetched_categories);
  649. ASSERT_THAT(fetched_categories->size(), Eq(1u));
  650. const auto& category = (*fetched_categories)[0];
  651. EXPECT_THAT(category.category.id(), Eq(Category::FromRemoteCategory(2).id()));
  652. ASSERT_THAT(category.suggestions.size(), Eq(1u));
  653. EXPECT_THAT(category.suggestions[0]->url().spec(),
  654. Eq("http://localhost/foo2"));
  655. }
  656. TEST_F(RemoteSuggestionsFetcherImplTest, ShouldNotFetchWithoutApiKey) {
  657. ResetFetcherWithAPIKey(std::string());
  658. EXPECT_CALL(mock_callback(),
  659. Run(Field(&Status::code, StatusCode::PERMANENT_ERROR),
  660. /*fetched_categories=*/IsNullCategoriesList()))
  661. .Times(1);
  662. fetcher().FetchSnippets(test_params(),
  663. ToSnippetsAvailableCallback(&mock_callback()));
  664. FastForwardUntilNoTasksRemain();
  665. EXPECT_THAT(fetcher().GetLastStatusForDebugging(),
  666. Eq("No API key available."));
  667. EXPECT_THAT(histogram_tester().GetAllSamples(
  668. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  669. IsEmpty());
  670. EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
  671. IsEmpty());
  672. }
  673. TEST_F(RemoteSuggestionsFetcherImplTest, ShouldFetchSuccessfullyEmptyList) {
  674. const std::string kJsonStr = "{\"categories\": []}";
  675. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  676. "?key=fakeAPIkey&priority=user_action"),
  677. /*response_data=*/kJsonStr, net::HTTP_OK, net::OK);
  678. EXPECT_CALL(mock_callback(),
  679. Run(Property(&Status::IsSuccess, true),
  680. /*fetched_categories=*/IsEmptyCategoriesList()));
  681. fetcher().FetchSnippets(test_params(),
  682. ToSnippetsAvailableCallback(&mock_callback()));
  683. FastForwardUntilNoTasksRemain();
  684. EXPECT_THAT(fetcher().GetLastStatusForDebugging(), Eq("OK"));
  685. EXPECT_THAT(fetcher().GetLastJsonForDebugging(), Eq(kJsonStr));
  686. EXPECT_THAT(
  687. histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
  688. ElementsAre(base::Bucket(/*min=*/0, /*count=*/1)));
  689. EXPECT_THAT(histogram_tester().GetAllSamples(
  690. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  691. ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
  692. }
  693. TEST_F(RemoteSuggestionsFetcherImplTest, RetryOnInteractiveRequests) {
  694. RequestParams params = test_params();
  695. params.interactive_request = true;
  696. EXPECT_THAT(
  697. internal::JsonRequest::Get5xxRetryCount(params.interactive_request),
  698. Eq(2));
  699. }
  700. TEST_F(RemoteSuggestionsFetcherImplTest,
  701. RetriesConfigurableOnNonInteractiveRequests) {
  702. struct ExpectationForVariationParam {
  703. std::string param_value;
  704. int expected_value;
  705. std::string description;
  706. };
  707. const std::vector<ExpectationForVariationParam> retry_config_expectation = {
  708. {"", 0, "Do not retry by default"},
  709. {"0", 0, "Do not retry on param value 0"},
  710. {"-1", 0, "Do not retry on negative param values."},
  711. {"4", 4, "Retry as set in param value."}};
  712. RequestParams params = test_params();
  713. params.interactive_request = false;
  714. for (const auto& retry_config : retry_config_expectation) {
  715. // DelegateCallingTestURLFetcherFactory fetcher_factory;
  716. SetVariationParam("background_5xx_retries_count", retry_config.param_value);
  717. EXPECT_THAT(internal::JsonRequest::Get5xxRetryCount(false),
  718. Eq(retry_config.expected_value))
  719. << retry_config.description;
  720. }
  721. }
  722. TEST_F(RemoteSuggestionsFetcherImplTest,
  723. HttpUnauthorizedIsTreatedAsDistinctTemporaryError) {
  724. SignIn();
  725. SetFakeResponse(
  726. GURL(std::string(kFetchSuggestionsEndpoint) + "?priority=user_action"),
  727. /*response_data=*/std::string(), net::HTTP_UNAUTHORIZED, net::OK);
  728. EXPECT_CALL(mock_callback(),
  729. Run(Field(&Status::code, StatusCode::TEMPORARY_ERROR),
  730. /*fetched_categories=*/IsNullCategoriesList()))
  731. .Times(1);
  732. fetcher().FetchSnippets(test_params(),
  733. ToSnippetsAvailableCallback(&mock_callback()));
  734. identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
  735. "access_token", base::Time::Max());
  736. FastForwardUntilNoTasksRemain();
  737. EXPECT_THAT(fetcher().GetLastStatusForDebugging(),
  738. Eq("Access token invalid 401"));
  739. }
  740. TEST_F(RemoteSuggestionsFetcherImplTest, ShouldReportUrlStatusError) {
  741. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  742. "?key=fakeAPIkey&priority=user_action"),
  743. /*response_data=*/std::string(), net::HTTP_NOT_FOUND,
  744. net::ERR_FAILED);
  745. EXPECT_CALL(mock_callback(),
  746. Run(Field(&Status::code, StatusCode::TEMPORARY_ERROR),
  747. /*fetched_categories=*/IsNullCategoriesList()))
  748. .Times(1);
  749. fetcher().FetchSnippets(test_params(),
  750. ToSnippetsAvailableCallback(&mock_callback()));
  751. FastForwardUntilNoTasksRemain();
  752. EXPECT_THAT(fetcher().GetLastStatusForDebugging(),
  753. Eq("URLRequestStatus error -2"));
  754. EXPECT_THAT(fetcher().GetLastJsonForDebugging(), IsEmpty());
  755. EXPECT_THAT(
  756. histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
  757. ElementsAre(base::Bucket(/*min=*/2, /*count=*/1)));
  758. EXPECT_THAT(histogram_tester().GetAllSamples(
  759. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  760. ElementsAre(base::Bucket(/*min=*/-2, /*count=*/1)));
  761. EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
  762. Not(IsEmpty()));
  763. }
  764. TEST_F(RemoteSuggestionsFetcherImplTest, ShouldReportHttpError) {
  765. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  766. "?key=fakeAPIkey&priority=user_action"),
  767. /*response_data=*/std::string(), net::HTTP_NOT_FOUND,
  768. net::OK);
  769. EXPECT_CALL(mock_callback(),
  770. Run(Field(&Status::code, StatusCode::TEMPORARY_ERROR),
  771. /*fetched_categories=*/IsNullCategoriesList()))
  772. .Times(1);
  773. fetcher().FetchSnippets(test_params(),
  774. ToSnippetsAvailableCallback(&mock_callback()));
  775. FastForwardUntilNoTasksRemain();
  776. EXPECT_THAT(fetcher().GetLastJsonForDebugging(), IsEmpty());
  777. EXPECT_THAT(
  778. histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
  779. ElementsAre(base::Bucket(/*min=*/3, /*count=*/1)));
  780. EXPECT_THAT(histogram_tester().GetAllSamples(
  781. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  782. ElementsAre(base::Bucket(/*min=*/404, /*count=*/1)));
  783. EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
  784. Not(IsEmpty()));
  785. }
  786. TEST_F(RemoteSuggestionsFetcherImplTest, ShouldReportJsonError) {
  787. const std::string kInvalidJsonStr = "{ \"recos\": []";
  788. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  789. "?key=fakeAPIkey&priority=user_action"),
  790. /*response_data=*/kInvalidJsonStr, net::HTTP_OK, net::OK);
  791. EXPECT_CALL(mock_callback(),
  792. Run(Field(&Status::code, StatusCode::TEMPORARY_ERROR),
  793. /*fetched_categories=*/IsNullCategoriesList()))
  794. .Times(1);
  795. fetcher().FetchSnippets(test_params(),
  796. ToSnippetsAvailableCallback(&mock_callback()));
  797. FastForwardUntilNoTasksRemain();
  798. EXPECT_THAT(fetcher().GetLastStatusForDebugging(),
  799. StartsWith("Received invalid JSON (error "));
  800. EXPECT_THAT(fetcher().GetLastJsonForDebugging(), Eq(kInvalidJsonStr));
  801. EXPECT_THAT(
  802. histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
  803. ElementsAre(base::Bucket(/*min=*/4, /*count=*/1)));
  804. EXPECT_THAT(histogram_tester().GetAllSamples(
  805. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  806. ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
  807. EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
  808. ElementsAre(base::Bucket(/*min=*/kTestJsonParsingLatencyMs,
  809. /*count=*/1)));
  810. }
  811. TEST_F(RemoteSuggestionsFetcherImplTest,
  812. ShouldReportJsonErrorForEmptyResponse) {
  813. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  814. "?key=fakeAPIkey&priority=user_action"),
  815. /*response_data=*/std::string(), net::HTTP_OK, net::OK);
  816. EXPECT_CALL(mock_callback(),
  817. Run(Field(&Status::code, StatusCode::TEMPORARY_ERROR),
  818. /*fetched_categories=*/IsNullCategoriesList()))
  819. .Times(1);
  820. fetcher().FetchSnippets(test_params(),
  821. ToSnippetsAvailableCallback(&mock_callback()));
  822. FastForwardUntilNoTasksRemain();
  823. EXPECT_THAT(fetcher().GetLastJsonForDebugging(), std::string());
  824. EXPECT_THAT(
  825. histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
  826. ElementsAre(base::Bucket(/*min=*/4, /*count=*/1)));
  827. EXPECT_THAT(histogram_tester().GetAllSamples(
  828. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  829. ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
  830. }
  831. TEST_F(RemoteSuggestionsFetcherImplTest, ShouldReportInvalidListError) {
  832. const std::string kJsonStr =
  833. "{\"recos\": [{ \"contentInfo\": { \"foo\" : \"bar\" }}]}";
  834. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  835. "?key=fakeAPIkey&priority=user_action"),
  836. /*response_data=*/kJsonStr, net::HTTP_OK, net::OK);
  837. EXPECT_CALL(mock_callback(),
  838. Run(Field(&Status::code, StatusCode::TEMPORARY_ERROR),
  839. /*fetched_categories=*/IsNullCategoriesList()))
  840. .Times(1);
  841. fetcher().FetchSnippets(test_params(),
  842. ToSnippetsAvailableCallback(&mock_callback()));
  843. FastForwardUntilNoTasksRemain();
  844. EXPECT_THAT(fetcher().GetLastJsonForDebugging(), Eq(kJsonStr));
  845. EXPECT_THAT(fetcher().GetLastStatusForDebugging(),
  846. StartsWith("Invalid / empty list"));
  847. EXPECT_THAT(
  848. histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
  849. ElementsAre(base::Bucket(/*min=*/5, /*count=*/1)));
  850. EXPECT_THAT(histogram_tester().GetAllSamples(
  851. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  852. ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
  853. EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
  854. Not(IsEmpty()));
  855. }
  856. TEST_F(RemoteSuggestionsFetcherImplTest,
  857. ShouldReportInvalidListErrorForIncompleteSuggestionButValidJson) {
  858. // This is valid json, but it does not represent a valid suggestion
  859. // (fullPageUrl is missing).
  860. const std::string kValidJsonStr =
  861. "{\"categories\" : [{"
  862. " \"id\": 1,"
  863. " \"localizedTitle\": \"Articles for You\","
  864. " \"suggestions\" : [{"
  865. " \"ids\" : [\"http://localhost/foobar\"],"
  866. " \"title\" : \"Foo Barred from Baz\","
  867. " \"snippet\" : \"...\","
  868. " \"INVALID_fullPageUrl\" : \"http://localhost/foobar\","
  869. " \"creationTime\" : \"2016-06-30T11:01:37.000Z\","
  870. " \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
  871. " \"attribution\" : \"Foo News\","
  872. " \"imageUrl\" : \"http://localhost/foobar.jpg\","
  873. " \"ampUrl\" : \"http://localhost/amp\","
  874. " \"faviconUrl\" : \"http://localhost/favicon.ico\" "
  875. " }]"
  876. "}]}";
  877. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  878. "?key=fakeAPIkey&priority=user_action"),
  879. /*response_data=*/kValidJsonStr, net::HTTP_OK, net::OK);
  880. EXPECT_CALL(mock_callback(),
  881. Run(Field(&Status::code, StatusCode::TEMPORARY_ERROR),
  882. /*fetched_categories=*/IsNullCategoriesList()))
  883. .Times(1);
  884. fetcher().FetchSnippets(test_params(),
  885. ToSnippetsAvailableCallback(&mock_callback()));
  886. FastForwardUntilNoTasksRemain();
  887. EXPECT_THAT(fetcher().GetLastStatusForDebugging(),
  888. StartsWith("Invalid / empty list"));
  889. EXPECT_THAT(
  890. histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
  891. ElementsAre(base::Bucket(/*min=*/5, /*count=*/1)));
  892. EXPECT_THAT(histogram_tester().GetAllSamples(
  893. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  894. ElementsAre(base::Bucket(/*min=*/200, /*count=*/1)));
  895. EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
  896. Not(IsEmpty()));
  897. }
  898. TEST_F(RemoteSuggestionsFetcherImplTest,
  899. ShouldReportInvalidListErrorForInvalidTimestampButValidJson) {
  900. // This is valid json, but it does not represent a valid suggestion
  901. // (creationTime is invalid).
  902. const std::string kValidJsonStr =
  903. "{\"categories\" : [{"
  904. " \"id\": 1,"
  905. " \"localizedTitle\": \"Articles for You\","
  906. " \"suggestions\" : [{"
  907. " \"ids\" : [\"http://localhost/foobar\"],"
  908. " \"title\" : \"Foo Barred from Baz\","
  909. " \"snippet\" : \"...\","
  910. " \"fullPageUrl\" : \"http://localhost/foobar\","
  911. " \"creationTime\" : \"INVALID_2016-06-30T11:01:37.000Z\","
  912. " \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
  913. " \"attribution\" : \"Foo News\","
  914. " \"imageUrl\" : \"http://localhost/foobar.jpg\","
  915. " \"ampUrl\" : \"http://localhost/amp\","
  916. " \"faviconUrl\" : \"http://localhost/favicon.ico\" "
  917. " }]"
  918. "}]}";
  919. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  920. "?key=fakeAPIkey&priority=user_action"),
  921. /*response_data=*/kValidJsonStr, net::HTTP_OK, net::OK);
  922. EXPECT_CALL(mock_callback(),
  923. Run(Field(&Status::code, StatusCode::TEMPORARY_ERROR),
  924. /*fetched_categories=*/IsNullCategoriesList()))
  925. .Times(1);
  926. fetcher().FetchSnippets(test_params(),
  927. ToSnippetsAvailableCallback(&mock_callback()));
  928. FastForwardUntilNoTasksRemain();
  929. EXPECT_THAT(fetcher().GetLastStatusForDebugging(),
  930. StartsWith("Invalid / empty list"));
  931. }
  932. TEST_F(RemoteSuggestionsFetcherImplTest,
  933. ShouldReportInvalidListErrorForInvalidUrlButValidJson) {
  934. // This is valid json, but it does not represent a valid suggestion
  935. // (URL is invalid).
  936. const std::string kValidJsonStr =
  937. "{\"categories\" : [{"
  938. " \"id\": 1,"
  939. " \"localizedTitle\": \"Articles for You\","
  940. " \"suggestions\" : [{"
  941. " \"ids\" : [\"NOT A URL\"],"
  942. " \"title\" : \"Foo Barred from Baz\","
  943. " \"snippet\" : \"...\","
  944. " \"fullPageUrl\" : \"NOT A URL\","
  945. " \"creationTime\" : \"2016-06-30T11:01:37.000Z\","
  946. " \"expirationTime\" : \"2016-07-01T11:01:37.000Z\","
  947. " \"attribution\" : \"Foo News\","
  948. " \"imageUrl\" : \"http://localhost/foobar.jpg\","
  949. " \"ampUrl\" : \"http://localhost/amp\","
  950. " \"faviconUrl\" : \"http://localhost/favicon.ico\" "
  951. " }]"
  952. "}]}";
  953. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  954. "?key=fakeAPIkey&priority=user_action"),
  955. /*response_data=*/kValidJsonStr, net::HTTP_OK, net::OK);
  956. EXPECT_CALL(mock_callback(),
  957. Run(Field(&Status::code, StatusCode::TEMPORARY_ERROR),
  958. /*fetched_categories=*/IsNullCategoriesList()))
  959. .Times(1);
  960. fetcher().FetchSnippets(test_params(),
  961. ToSnippetsAvailableCallback(&mock_callback()));
  962. FastForwardUntilNoTasksRemain();
  963. EXPECT_THAT(fetcher().GetLastStatusForDebugging(),
  964. StartsWith("Invalid / empty list"));
  965. }
  966. TEST_F(RemoteSuggestionsFetcherImplTest,
  967. ShouldReportRequestFailureAsTemporaryError) {
  968. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  969. "?key=fakeAPIkey&priority=user_action"),
  970. /*response_data=*/std::string(), net::HTTP_NOT_FOUND,
  971. net::ERR_FAILED);
  972. EXPECT_CALL(mock_callback(),
  973. Run(Field(&Status::code, StatusCode::TEMPORARY_ERROR),
  974. /*fetched_categories=*/IsNullCategoriesList()))
  975. .Times(1);
  976. fetcher().FetchSnippets(test_params(),
  977. ToSnippetsAvailableCallback(&mock_callback()));
  978. FastForwardUntilNoTasksRemain();
  979. }
  980. // This test actually verifies that the test setup itself is sane, to prevent
  981. // hard-to-reproduce test failures.
  982. TEST_F(RemoteSuggestionsFetcherImplTest,
  983. ShouldReportHttpErrorForMissingBakedResponse) {
  984. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  985. "?key=fakeAPIkey&priority=user_action"),
  986. /*response_data=*/std::string(), net::HTTP_NOT_FOUND,
  987. net::ERR_FAILED);
  988. EXPECT_CALL(mock_callback(),
  989. Run(Field(&Status::code, StatusCode::TEMPORARY_ERROR),
  990. /*fetched_categories=*/IsNullCategoriesList()))
  991. .Times(1);
  992. fetcher().FetchSnippets(test_params(),
  993. ToSnippetsAvailableCallback(&mock_callback()));
  994. FastForwardUntilNoTasksRemain();
  995. }
  996. TEST_F(RemoteSuggestionsFetcherImplTest, ShouldProcessConcurrentFetches) {
  997. const std::string kJsonStr = "{ \"categories\": [] }";
  998. SetFakeResponse(GURL(std::string(kFetchSuggestionsEndpoint) +
  999. "?key=fakeAPIkey&priority=user_action"),
  1000. /*response_data=*/kJsonStr, net::HTTP_OK, net::OK);
  1001. EXPECT_CALL(mock_callback(),
  1002. Run(Property(&Status::IsSuccess, true),
  1003. /*fetched_categories=*/IsEmptyCategoriesList()))
  1004. .Times(5);
  1005. fetcher().FetchSnippets(test_params(),
  1006. ToSnippetsAvailableCallback(&mock_callback()));
  1007. // More calls to FetchSnippets() do not interrupt the previous.
  1008. // Callback is expected to be called once each time.
  1009. fetcher().FetchSnippets(test_params(),
  1010. ToSnippetsAvailableCallback(&mock_callback()));
  1011. fetcher().FetchSnippets(test_params(),
  1012. ToSnippetsAvailableCallback(&mock_callback()));
  1013. fetcher().FetchSnippets(test_params(),
  1014. ToSnippetsAvailableCallback(&mock_callback()));
  1015. fetcher().FetchSnippets(test_params(),
  1016. ToSnippetsAvailableCallback(&mock_callback()));
  1017. FastForwardUntilNoTasksRemain();
  1018. EXPECT_THAT(
  1019. histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchResult"),
  1020. ElementsAre(base::Bucket(/*min=*/0, /*count=*/5)));
  1021. EXPECT_THAT(histogram_tester().GetAllSamples(
  1022. "NewTabPage.Snippets.FetchHttpResponseOrErrorCode"),
  1023. ElementsAre(base::Bucket(/*min=*/200, /*count=*/5)));
  1024. EXPECT_THAT(histogram_tester().GetAllSamples("NewTabPage.Snippets.FetchTime"),
  1025. ElementsAre(base::Bucket(/*min=*/kTestJsonParsingLatencyMs,
  1026. /*count=*/5)));
  1027. }
  1028. ::std::ostream& operator<<(
  1029. ::std::ostream& os,
  1030. const RemoteSuggestionsFetcher::OptionalFetchedCategories&
  1031. fetched_categories) {
  1032. if (fetched_categories) {
  1033. // Matchers above aren't any more precise than this, so this is sufficient
  1034. // for test-failure diagnostics.
  1035. return os << "list with " << fetched_categories->size() << " elements";
  1036. }
  1037. return os << "null";
  1038. }
  1039. } // namespace ntp_snippets