1 // Copyright 2020 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 "chrome/browser/media/feeds/media_feeds_fetcher.h"
6 
7 #include <memory>
8 
9 #include "base/bind.h"
10 #include "base/test/bind.h"
11 #include "base/test/mock_callback.h"
12 #include "base/test/task_environment.h"
13 #include "chrome/browser/media/history/media_history_keyed_service.h"
14 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
15 #include "components/schema_org/common/metadata.mojom.h"
16 #include "components/schema_org/schema_org_entity_names.h"
17 #include "content/public/browser/storage_partition.h"
18 #include "net/cookies/cookie_access_result.h"
19 #include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
20 #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
21 #include "services/network/test/test_url_loader_factory.h"
22 #include "services/network/test/test_utils.h"
23 #include "testing/gtest/include/gtest/gtest.h"
24 #include "url/gurl.h"
25 
26 namespace media_feeds {
27 
28 using testing::_;
29 
30 namespace {
31 
32 const char kTestUrl[] = "https://www.google.com";
33 
34 const char kTestData[] = R"END({
35     "@context": "https://schema.org",
36     "@type": "CompleteDataFeed",
37     "dataFeedElement": [],
38     "provider": {
39       "@type": "Organization",
40       "name": "Media Feeds Developers",
41       "logo": [{
42         "@type": "ImageObject",
43         "width": 1113,
44         "height": 245,
45         "url": "https://wicg.github.io/media-feeds/data/logo_white.png",
46         "additionalProperty": {
47           "@type": "PropertyValue",
48           "name": "contentAttributes",
49           "value": ["forDarkBackground", "hasTitle", "transparentBackground"]
50         }
51       }]
52     }
53 })END";
54 
55 const char kTestDataWithAssociatedOrigins[] = R"END({
56     "@context": "https://schema.org",
57     "@type": "CompleteDataFeed",
58     "dataFeedElement": [],
59     "additionalProperty": {
60       "@type": "PropertyValue",
61       "name": "associatedOrigin",
62       "value": ["https://login.example.org", "https://login.example.com"]
63     },
64     "provider": {
65       "@type": "Organization",
66       "name": "Media Feeds Developers",
67       "logo": [{
68         "@type": "ImageObject",
69         "width": 1113,
70         "height": 245,
71         "url": "https://wicg.github.io/media-feeds/data/logo_white.png",
72         "additionalProperty": {
73           "@type": "PropertyValue",
74           "name": "contentAttributes",
75           "value": ["forDarkBackground", "hasTitle", "transparentBackground"]
76         }
77       }]
78     }
79 })END";
80 
81 const char kTestFeedName[] = "Media Feeds Developers";
82 
83 }  // namespace
84 
85 class MediaFeedsFetcherTest : public ChromeRenderViewHostTestHarness {
86  public:
87   MediaFeedsFetcherTest() = default;
88   ~MediaFeedsFetcherTest() override = default;
89   MediaFeedsFetcherTest(const MediaFeedsFetcherTest& t) = delete;
90   MediaFeedsFetcherTest& operator=(const MediaFeedsFetcherTest&) = delete;
91 
SetUp()92   void SetUp() override {
93     ChromeRenderViewHostTestHarness::SetUp();
94 
95     fetcher_ = std::make_unique<MediaFeedsFetcher>(
96         base::MakeRefCounted<::network::WeakWrapperSharedURLLoaderFactory>(
97             url_loader_factory()));
98   }
99 
100   MediaFeedsFetcher* fetcher() { return fetcher_.get(); }
101   ::network::TestURLLoaderFactory* url_loader_factory() {
102     return &url_loader_factory_;
103   }
104 
105   bool RespondToFetch(
106       const std::string& response_body,
107       net::HttpStatusCode response_code = net::HttpStatusCode::HTTP_OK,
108       int net_error = net::OK,
109       bool was_fetched_via_cache = false) {
110     auto response_head = ::network::CreateURLResponseHead(response_code);
111     response_head->was_fetched_via_cache = was_fetched_via_cache;
112 
113     bool rv = url_loader_factory()->SimulateResponseForPendingRequest(
114         GURL(kTestUrl), ::network::URLLoaderCompletionStatus(net_error),
115         std::move(response_head), response_body);
116     task_environment()->RunUntilIdle();
117     return rv;
118   }
119 
120   void WaitForRequest() {
121     task_environment()->RunUntilIdle();
122 
123     ASSERT_TRUE(GetCurrentRequest().url.is_valid());
124     EXPECT_TRUE(GetCurrentRequest().site_for_cookies.IsEquivalent(
125         net::SiteForCookies::FromUrl(GURL(kTestUrl))));
126     EXPECT_EQ(GetCurrentlyQueriedHeaderValue(net::HttpRequestHeaders::kAccept),
127               "application/ld+json");
128     EXPECT_EQ(GetCurrentRequest().redirect_mode,
129               ::network::mojom::RedirectMode::kError);
130     EXPECT_EQ(net::HttpRequestHeaders::kGetMethod, GetCurrentRequest().method);
131   }
132 
133   bool SetCookie(content::BrowserContext* browser_context,
134                  const GURL& url,
135                  const std::string& value) {
136     bool result = false;
137     base::RunLoop run_loop;
138     mojo::Remote<network::mojom::CookieManager> cookie_manager;
139     content::BrowserContext::GetDefaultStoragePartition(browser_context)
140         ->GetNetworkContext()
141         ->GetCookieManager(cookie_manager.BindNewPipeAndPassReceiver());
142     std::unique_ptr<net::CanonicalCookie> cc(net::CanonicalCookie::Create(
143         url, value, base::Time::Now(), base::nullopt /* server_time */));
144     EXPECT_TRUE(cc.get());
145 
146     cookie_manager->SetCanonicalCookie(
147         *cc.get(), url, net::CookieOptions::MakeAllInclusive(),
148         base::BindOnce(
149             [](bool* result, base::RunLoop* run_loop,
150                net::CookieAccessResult set_cookie_access_result) {
151               *result = set_cookie_access_result.status.IsInclude();
152               run_loop->Quit();
153             },
154             &result, &run_loop));
155     run_loop.Run();
156     return result;
157   }
158 
159  private:
160   std::string GetCurrentlyQueriedHeaderValue(const base::StringPiece& key) {
161     std::string out;
162     GetCurrentRequest().headers.GetHeader(key, &out);
163     return out;
164   }
165 
166   const ::network::ResourceRequest& GetCurrentRequest() {
167     return url_loader_factory()->pending_requests()->front().request;
168   }
169 
170   ::network::TestURLLoaderFactory url_loader_factory_;
171   std::unique_ptr<MediaFeedsFetcher> fetcher_;
172   data_decoder::test::InProcessDataDecoder data_decoder_;
173 };
174 
175 TEST_F(MediaFeedsFetcherTest, SucceedsOnBasicFetch) {
176   GURL site_with_cookies(kTestUrl);
177   ASSERT_TRUE(SetCookie(profile(), site_with_cookies, "testing"));
178 
179   fetcher()->FetchFeed(
180       GURL("https://www.google.com"), false,
181       base::BindLambdaForTesting(
182           [&](media_history::MediaHistoryKeyedService::MediaFeedFetchResult
183                   result) {
184             EXPECT_EQ(result.status, mojom::FetchResult::kSuccess);
185             EXPECT_FALSE(result.was_fetched_from_cache);
186             EXPECT_FALSE(result.gone);
187             EXPECT_EQ(kTestFeedName, result.display_name);
188           }));
189 
190   WaitForRequest();
191   ASSERT_TRUE(RespondToFetch(kTestData));
192 }
193 
TEST_F(MediaFeedsFetcherTest,SucceedsOnBasicFetch_ForceCache)194 TEST_F(MediaFeedsFetcherTest, SucceedsOnBasicFetch_ForceCache) {
195   GURL site_with_cookies(kTestUrl);
196   ASSERT_TRUE(SetCookie(profile(), site_with_cookies, "testing"));
197 
198   fetcher()->FetchFeed(
199       GURL("https://www.google.com"), true,
200       base::BindLambdaForTesting(
201           [&](media_history::MediaHistoryKeyedService::MediaFeedFetchResult
202                   result) {
203             EXPECT_EQ(result.status, mojom::FetchResult::kSuccess);
204             EXPECT_FALSE(result.was_fetched_from_cache);
205             EXPECT_FALSE(result.gone);
206             EXPECT_EQ(kTestFeedName, result.display_name);
207           }));
208 
209   WaitForRequest();
210   ASSERT_TRUE(RespondToFetch(kTestData));
211 }
212 
TEST_F(MediaFeedsFetcherTest,SucceedsFetchFromCache)213 TEST_F(MediaFeedsFetcherTest, SucceedsFetchFromCache) {
214   fetcher()->FetchFeed(
215       GURL("https://www.google.com"), false,
216       base::BindLambdaForTesting(
217           [&](media_history::MediaHistoryKeyedService::MediaFeedFetchResult
218                   result) {
219             EXPECT_EQ(result.status, mojom::FetchResult::kSuccess);
220             EXPECT_TRUE(result.was_fetched_from_cache);
221             EXPECT_FALSE(result.gone);
222             EXPECT_EQ(kTestFeedName, result.display_name);
223           }));
224 
225   WaitForRequest();
226   ASSERT_TRUE(
227       RespondToFetch(kTestData, net::HttpStatusCode::HTTP_OK, net::OK, true));
228 }
229 
TEST_F(MediaFeedsFetcherTest,ReturnsFailedResponseCode)230 TEST_F(MediaFeedsFetcherTest, ReturnsFailedResponseCode) {
231   fetcher()->FetchFeed(
232       GURL("https://www.google.com"), false,
233       base::BindLambdaForTesting(
234           [&](media_history::MediaHistoryKeyedService::MediaFeedFetchResult
235                   result) {
236             EXPECT_EQ(result.status, mojom::FetchResult::kFailedBackendError);
237             EXPECT_FALSE(result.was_fetched_from_cache);
238             EXPECT_FALSE(result.gone);
239           }));
240 
241   WaitForRequest();
242   ASSERT_TRUE(RespondToFetch("", net::HTTP_BAD_REQUEST));
243 }
244 
TEST_F(MediaFeedsFetcherTest,ReturnsGone)245 TEST_F(MediaFeedsFetcherTest, ReturnsGone) {
246   base::MockCallback<MediaFeedsFetcher::MediaFeedCallback> callback;
247 
248   fetcher()->FetchFeed(
249       GURL("https://www.google.com"), false,
250       base::BindLambdaForTesting(
251           [&](media_history::MediaHistoryKeyedService::MediaFeedFetchResult
252                   result) {
253             EXPECT_EQ(result.status, mojom::FetchResult::kNone);
254             EXPECT_FALSE(result.was_fetched_from_cache);
255             EXPECT_TRUE(result.gone);
256           }));
257 
258   WaitForRequest();
259   ASSERT_TRUE(RespondToFetch("", net::HTTP_GONE));
260 }
261 
TEST_F(MediaFeedsFetcherTest,ReturnsNetError)262 TEST_F(MediaFeedsFetcherTest, ReturnsNetError) {
263   fetcher()->FetchFeed(
264       GURL("https://www.google.com"), false,
265       base::BindLambdaForTesting(
266           [&](media_history::MediaHistoryKeyedService::MediaFeedFetchResult
267                   result) {
268             EXPECT_EQ(result.status, mojom::FetchResult::kFailedBackendError);
269             EXPECT_FALSE(result.was_fetched_from_cache);
270             EXPECT_FALSE(result.gone);
271           }));
272 
273   WaitForRequest();
274   ASSERT_TRUE(RespondToFetch("", net::HTTP_OK, net::ERR_UNEXPECTED));
275 }
276 
TEST_F(MediaFeedsFetcherTest,ReturnsErrFileNotFoundForEmptyFeedData)277 TEST_F(MediaFeedsFetcherTest, ReturnsErrFileNotFoundForEmptyFeedData) {
278   fetcher()->FetchFeed(
279       GURL("https://www.google.com"), false,
280       base::BindLambdaForTesting(
281           [&](media_history::MediaHistoryKeyedService::MediaFeedFetchResult
282                   result) {
283             EXPECT_EQ(result.status, mojom::FetchResult::kFailedNetworkError);
284             EXPECT_FALSE(result.was_fetched_from_cache);
285             EXPECT_FALSE(result.gone);
286           }));
287 
288   WaitForRequest();
289   ASSERT_TRUE(RespondToFetch(""));
290 }
291 
TEST_F(MediaFeedsFetcherTest,ReturnsErrFailedForBadEntityData)292 TEST_F(MediaFeedsFetcherTest, ReturnsErrFailedForBadEntityData) {
293   fetcher()->FetchFeed(
294       GURL("https://www.google.com"), false,
295       base::BindLambdaForTesting(
296           [&](media_history::MediaHistoryKeyedService::MediaFeedFetchResult
297                   result) {
298             EXPECT_EQ(result.status, mojom::FetchResult::kFailedBackendError);
299             EXPECT_FALSE(result.was_fetched_from_cache);
300             EXPECT_FALSE(result.gone);
301           }));
302 
303   WaitForRequest();
304   ASSERT_TRUE(RespondToFetch(
305       "{\"@type\":\"CompleteDataFeed\"\"name\":\"Bad json missing a comma\"}"));
306 }
307 
TEST_F(MediaFeedsFetcherTest,Success_AssociatedOrigins_BothWork)308 TEST_F(MediaFeedsFetcherTest, Success_AssociatedOrigins_BothWork) {
309   GURL site_with_cookies(kTestUrl);
310   ASSERT_TRUE(SetCookie(profile(), site_with_cookies, "testing"));
311 
312   fetcher()->FetchFeed(
313       GURL("https://www.google.com"), false,
314       base::BindLambdaForTesting(
315           [&](media_history::MediaHistoryKeyedService::MediaFeedFetchResult
316                   result) {
317             EXPECT_EQ(result.status, mojom::FetchResult::kSuccess);
318             EXPECT_FALSE(result.was_fetched_from_cache);
319             EXPECT_FALSE(result.gone);
320             EXPECT_EQ(kTestFeedName, result.display_name);
321           }));
322 
323   WaitForRequest();
324   ASSERT_TRUE(RespondToFetch(kTestDataWithAssociatedOrigins));
325 }
326 
327 }  // namespace media_feeds
328