| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/ntp_snippets/remote/json_request.h" |
| |
| #include <set> |
| #include <utility> |
| |
| #include "base/json/json_reader.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_mock_time_task_runner.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/tick_clock.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "components/ntp_snippets/features.h" |
| #include "components/ntp_snippets/ntp_snippets_constants.h" |
| #include "components/ntp_snippets/remote/request_params.h" |
| #include "components/prefs/testing_pref_service.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" |
| #include "services/network/test/test_url_loader_factory.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| // TODO(crbug.com/961023): Fix memory leaks in tests and re-enable on LSAN. |
| #ifdef LEAK_SANITIZER |
| #define MAYBE_BuildRequestAuthenticated DISABLED_BuildRequestAuthenticated |
| #else |
| #define MAYBE_BuildRequestAuthenticated BuildRequestAuthenticated |
| #endif |
| |
| namespace ntp_snippets { |
| |
| namespace internal { |
| |
| namespace { |
| |
| using testing::_; |
| using testing::Eq; |
| using testing::Not; |
| using testing::NotNull; |
| using testing::StrEq; |
| |
| MATCHER_P(EqualsJSON, json, "equals JSON") { |
| base::Optional<base::Value> expected = base::JSONReader::Read(json); |
| if (!expected) { |
| *result_listener << "INTERNAL ERROR: couldn't parse expected JSON"; |
| return false; |
| } |
| |
| base::JSONReader::ValueWithError actual = |
| base::JSONReader::ReadAndReturnValueWithError(arg); |
| if (!actual.value) { |
| *result_listener << "input:" << actual.error_line << ":" |
| << actual.error_column << ": " |
| << "parse error: " << actual.error_message; |
| return false; |
| } |
| return *expected == *actual.value; |
| } |
| |
| } // namespace |
| |
| class JsonRequestTest : public testing::Test { |
| public: |
| JsonRequestTest() |
| : pref_service_(std::make_unique<TestingPrefServiceSimple>()), |
| mock_task_runner_(new base::TestMockTimeTaskRunner()), |
| mock_runner_handle_( |
| std::make_unique<base::ThreadTaskRunnerHandle>(mock_task_runner_)), |
| test_shared_loader_factory_( |
| base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>( |
| &test_url_loader_factory_)) { |
| scoped_feature_list_.InitAndEnableFeatureWithParameters( |
| kArticleSuggestionsFeature, |
| {{"send_top_languages", "true"}, {"send_user_class", "true"}}); |
| language::UrlLanguageHistogram::RegisterProfilePrefs( |
| pref_service_->registry()); |
| } |
| |
| std::unique_ptr<language::UrlLanguageHistogram> MakeLanguageHistogram( |
| const std::set<std::string>& codes) { |
| std::unique_ptr<language::UrlLanguageHistogram> language_histogram = |
| std::make_unique<language::UrlLanguageHistogram>(pref_service_.get()); |
| // There must be at least 10 visits before the top languages are defined. |
| for (int i = 0; i < 10; i++) { |
| for (const std::string& code : codes) { |
| language_histogram->OnPageVisited(code); |
| } |
| } |
| return language_histogram; |
| } |
| |
| JsonRequest::Builder CreateMinimalBuilder() { |
| JsonRequest::Builder builder; |
| builder.SetUrl(GURL("http://valid-url.test")) |
| .SetClock(mock_task_runner_->GetMockClock()) |
| .SetUrlLoaderFactory(test_shared_loader_factory_); |
| return builder; |
| } |
| |
| std::unique_ptr<base::test::ScopedFeatureList> ForceOptionalImagesSupport( |
| bool supported) { |
| auto feature_list = std::make_unique<base::test::ScopedFeatureList>(); |
| if (supported) { |
| feature_list->InitWithFeatures({kOptionalImagesEnabledFeature}, {}); |
| } else { |
| feature_list->InitWithFeatures({}, {kOptionalImagesEnabledFeature}); |
| } |
| return feature_list; |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| std::unique_ptr<TestingPrefServiceSimple> pref_service_; |
| scoped_refptr<base::TestMockTimeTaskRunner> mock_task_runner_; |
| std::unique_ptr<base::ThreadTaskRunnerHandle> mock_runner_handle_; |
| network::TestURLLoaderFactory test_url_loader_factory_; |
| scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(JsonRequestTest); |
| }; |
| |
| TEST_F(JsonRequestTest, MAYBE_BuildRequestAuthenticated) { |
| JsonRequest::Builder builder = CreateMinimalBuilder(); |
| RequestParams params; |
| params.excluded_ids = {"1234567890"}; |
| params.count_to_fetch = 25; |
| params.interactive_request = false; |
| builder.SetParams(params) |
| .SetUrl(GURL("http://valid-url.test")) |
| .SetAuthentication("headerstuff") |
| .SetUserClassForTesting("ACTIVE_NTP_USER") |
| .Build(); |
| |
| EXPECT_THAT(builder.PreviewRequestHeadersForTesting(), |
| StrEq("Content-Type: application/json; charset=UTF-8\r\n" |
| "Authorization: headerstuff\r\n" |
| "\r\n")); |
| EXPECT_THAT(builder.PreviewRequestBodyForTesting(), |
| EqualsJSON("{" |
| " \"priority\": \"BACKGROUND_PREFETCH\"," |
| " \"excludedSuggestionIds\": [" |
| " \"1234567890\"" |
| " ]," |
| " \"userActivenessClass\": \"ACTIVE_NTP_USER\"" |
| "}")); |
| } |
| |
| TEST_F(JsonRequestTest, BuildRequestUnauthenticated) { |
| JsonRequest::Builder builder; |
| RequestParams params; |
| params.interactive_request = true; |
| params.count_to_fetch = 10; |
| builder.SetParams(params).SetUserClassForTesting("ACTIVE_NTP_USER"); |
| |
| EXPECT_THAT(builder.PreviewRequestHeadersForTesting(), |
| StrEq("Content-Type: application/json; charset=UTF-8\r\n" |
| "\r\n")); |
| EXPECT_THAT(builder.PreviewRequestBodyForTesting(), |
| EqualsJSON("{" |
| " \"priority\": \"USER_ACTION\"," |
| " \"excludedSuggestionIds\": []," |
| " \"userActivenessClass\": \"ACTIVE_NTP_USER\"" |
| "}")); |
| } |
| |
| TEST_F(JsonRequestTest, BuildRequestDisplayCapabilityDisabledByFeature) { |
| auto optional_images_feature_list = ForceOptionalImagesSupport(false); |
| |
| JsonRequest::Builder builder; |
| builder.SetOptionalImagesCapability(true); |
| |
| EXPECT_THAT(builder.PreviewRequestHeadersForTesting(), |
| StrEq("Content-Type: application/json; charset=UTF-8\r\n" |
| "\r\n")); |
| |
| // The JSON should not contain any mention of displayCapability. |
| EXPECT_THAT(builder.PreviewRequestBodyForTesting(), |
| EqualsJSON("{" |
| " \"excludedSuggestionIds\": []," |
| " \"priority\": \"BACKGROUND_PREFETCH\"" |
| "}")); |
| } |
| |
| TEST_F(JsonRequestTest, BuildRequestDisplayCapabilityUnspecified) { |
| auto optional_images_feature_list = ForceOptionalImagesSupport(true); |
| |
| JsonRequest::Builder builder; |
| builder.SetOptionalImagesCapability(false); |
| |
| EXPECT_THAT(builder.PreviewRequestHeadersForTesting(), |
| StrEq("Content-Type: application/json; charset=UTF-8\r\n" |
| "\r\n")); |
| EXPECT_THAT(builder.PreviewRequestBodyForTesting(), |
| EqualsJSON("{" |
| " \"excludedSuggestionIds\": []," |
| " \"priority\": \"BACKGROUND_PREFETCH\"" |
| "}")); |
| } |
| |
| TEST_F(JsonRequestTest, BuildRequestOptionalImages) { |
| auto optional_images_feature_list = ForceOptionalImagesSupport(true); |
| |
| JsonRequest::Builder builder; |
| builder.SetOptionalImagesCapability(true); |
| |
| EXPECT_THAT(builder.PreviewRequestHeadersForTesting(), |
| StrEq("Content-Type: application/json; charset=UTF-8\r\n" |
| "\r\n")); |
| EXPECT_THAT( |
| builder.PreviewRequestBodyForTesting(), |
| EqualsJSON("{" |
| " \"displayCapability\": \"CAPABILITY_OPTIONAL_IMAGES\"," |
| " \"excludedSuggestionIds\": []," |
| " \"priority\": \"BACKGROUND_PREFETCH\"" |
| "}")); |
| } |
| |
| TEST_F(JsonRequestTest, ShouldNotTruncateExcludedIdsList) { |
| JsonRequest::Builder builder; |
| RequestParams params; |
| params.interactive_request = false; |
| for (int i = 0; i < 200; ++i) { |
| params.excluded_ids.insert(base::StringPrintf("%03d", i)); |
| } |
| builder.SetParams(params).SetUserClassForTesting("ACTIVE_NTP_USER"); |
| |
| EXPECT_THAT(builder.PreviewRequestBodyForTesting(), |
| EqualsJSON("{" |
| " \"priority\": \"BACKGROUND_PREFETCH\"," |
| " \"excludedSuggestionIds\": [" |
| " \"000\", \"001\", \"002\", \"003\", \"004\"," |
| " \"005\", \"006\", \"007\", \"008\", \"009\"," |
| " \"010\", \"011\", \"012\", \"013\", \"014\"," |
| " \"015\", \"016\", \"017\", \"018\", \"019\"," |
| " \"020\", \"021\", \"022\", \"023\", \"024\"," |
| " \"025\", \"026\", \"027\", \"028\", \"029\"," |
| " \"030\", \"031\", \"032\", \"033\", \"034\"," |
| " \"035\", \"036\", \"037\", \"038\", \"039\"," |
| " \"040\", \"041\", \"042\", \"043\", \"044\"," |
| " \"045\", \"046\", \"047\", \"048\", \"049\"," |
| " \"050\", \"051\", \"052\", \"053\", \"054\"," |
| " \"055\", \"056\", \"057\", \"058\", \"059\"," |
| " \"060\", \"061\", \"062\", \"063\", \"064\"," |
| " \"065\", \"066\", \"067\", \"068\", \"069\"," |
| " \"070\", \"071\", \"072\", \"073\", \"074\"," |
| " \"075\", \"076\", \"077\", \"078\", \"079\"," |
| " \"080\", \"081\", \"082\", \"083\", \"084\"," |
| " \"085\", \"086\", \"087\", \"088\", \"089\"," |
| " \"090\", \"091\", \"092\", \"093\", \"094\"," |
| " \"095\", \"096\", \"097\", \"098\", \"099\"," |
| " \"100\", \"101\", \"102\", \"103\", \"104\"," |
| " \"105\", \"106\", \"107\", \"108\", \"109\"," |
| " \"110\", \"111\", \"112\", \"113\", \"114\"," |
| " \"115\", \"116\", \"117\", \"118\", \"119\"," |
| " \"120\", \"121\", \"122\", \"123\", \"124\"," |
| " \"125\", \"126\", \"127\", \"128\", \"129\"," |
| " \"130\", \"131\", \"132\", \"133\", \"134\"," |
| " \"135\", \"136\", \"137\", \"138\", \"139\"," |
| " \"140\", \"141\", \"142\", \"143\", \"144\"," |
| " \"145\", \"146\", \"147\", \"148\", \"149\"," |
| " \"150\", \"151\", \"152\", \"153\", \"154\"," |
| " \"155\", \"156\", \"157\", \"158\", \"159\"," |
| " \"160\", \"161\", \"162\", \"163\", \"164\"," |
| " \"165\", \"166\", \"167\", \"168\", \"169\"," |
| " \"170\", \"171\", \"172\", \"173\", \"174\"," |
| " \"175\", \"176\", \"177\", \"178\", \"179\"," |
| " \"180\", \"181\", \"182\", \"183\", \"184\"," |
| " \"185\", \"186\", \"187\", \"188\", \"189\"," |
| " \"190\", \"191\", \"192\", \"193\", \"194\"," |
| " \"195\", \"196\", \"197\", \"198\", \"199\"" |
| " ]," |
| " \"userActivenessClass\": \"ACTIVE_NTP_USER\"" |
| "}")); |
| } |
| |
| TEST_F(JsonRequestTest, BuildRequestNoUserClass) { |
| JsonRequest::Builder builder; |
| RequestParams params; |
| params.interactive_request = false; |
| builder.SetParams(params); |
| |
| EXPECT_THAT(builder.PreviewRequestBodyForTesting(), |
| EqualsJSON("{" |
| " \"priority\": \"BACKGROUND_PREFETCH\"," |
| " \"excludedSuggestionIds\": []" |
| "}")); |
| } |
| |
| TEST_F(JsonRequestTest, BuildRequestWithTwoLanguages) { |
| JsonRequest::Builder builder; |
| std::unique_ptr<language::UrlLanguageHistogram> language_histogram = |
| MakeLanguageHistogram({"de", "en"}); |
| RequestParams params; |
| params.interactive_request = true; |
| params.language_code = "en"; |
| builder.SetParams(params).SetLanguageHistogram(language_histogram.get()); |
| |
| EXPECT_THAT(builder.PreviewRequestBodyForTesting(), |
| EqualsJSON("{" |
| " \"priority\": \"USER_ACTION\"," |
| " \"uiLanguage\": \"en\"," |
| " \"excludedSuggestionIds\": []," |
| " \"topLanguages\": [" |
| " {" |
| " \"language\" : \"en\"," |
| " \"frequency\" : 0.5" |
| " }," |
| " {" |
| " \"language\" : \"de\"," |
| " \"frequency\" : 0.5" |
| " }" |
| " ]" |
| "}")); |
| } |
| |
| TEST_F(JsonRequestTest, BuildRequestWithUILanguageOnly) { |
| JsonRequest::Builder builder; |
| std::unique_ptr<language::UrlLanguageHistogram> language_histogram = |
| MakeLanguageHistogram({"en"}); |
| RequestParams params; |
| params.interactive_request = true; |
| params.language_code = "en"; |
| builder.SetParams(params).SetLanguageHistogram(language_histogram.get()); |
| |
| EXPECT_THAT(builder.PreviewRequestBodyForTesting(), |
| EqualsJSON("{" |
| " \"priority\": \"USER_ACTION\"," |
| " \"uiLanguage\": \"en\"," |
| " \"excludedSuggestionIds\": []," |
| " \"topLanguages\": [{" |
| " \"language\" : \"en\"," |
| " \"frequency\" : 1.0" |
| " }]" |
| "}")); |
| } |
| |
| TEST_F(JsonRequestTest, |
| ShouldPropagateCountToFetchWhenExclusiveCategoryPresent) { |
| JsonRequest::Builder builder; |
| RequestParams params; |
| params.interactive_request = true; |
| params.language_code = "en"; |
| params.exclusive_category = |
| Category::FromKnownCategory(KnownCategories::ARTICLES); |
| params.count_to_fetch = 25; |
| builder.SetParams(params); |
| |
| EXPECT_THAT(builder.PreviewRequestBodyForTesting(), EqualsJSON(R"( |
| { |
| "priority": "USER_ACTION", |
| "uiLanguage": "en", |
| "excludedSuggestionIds": [], |
| "categoryParameters": [{ |
| "id": 1, |
| "numSuggestions": 25 |
| }] |
| } |
| )")); |
| } |
| |
| // TODO(vitaliii): Propagate count to fetch in this case as well and delete this |
| // test. Currently the server does not support this. |
| TEST_F(JsonRequestTest, |
| ShouldNotPropagateCountToFetchWhenExclusiveCategoryNotPresent) { |
| JsonRequest::Builder builder; |
| RequestParams params; |
| params.interactive_request = true; |
| params.language_code = "en"; |
| params.count_to_fetch = 10; |
| builder.SetParams(params); |
| |
| EXPECT_THAT(builder.PreviewRequestBodyForTesting(), EqualsJSON(R"( |
| { |
| "priority": "USER_ACTION", |
| "uiLanguage": "en", |
| "excludedSuggestionIds": [] |
| } |
| )")); |
| } |
| |
| } // namespace internal |
| |
| } // namespace ntp_snippets |