1 // Copyright 2019 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 <lib/fidl/cpp/binding.h>
6 
7 #include <map>
8 #include <vector>
9 
10 #include "base/bind.h"
11 #include "base/macros.h"
12 #include "base/optional.h"
13 #include "base/run_loop.h"
14 #include "base/test/bind_test_util.h"
15 #include "base/test/task_environment.h"
16 #include "fuchsia/base/fit_adapter.h"
17 #include "fuchsia/base/result_receiver.h"
18 #include "fuchsia/engine/browser/cookie_manager_impl.h"
19 #include "mojo/public/cpp/bindings/remote.h"
20 #include "services/network/network_service.h"
21 #include "services/network/public/mojom/cookie_manager.mojom.h"
22 #include "services/network/public/mojom/network_context.mojom.h"
23 #include "testing/gtest/include/gtest/gtest.h"
24 
25 namespace {
26 
27 const char kTestCookieUrl[] = "https://www.testing.com/";
28 const char kTestOtherUrl[] = "https://www.other.com/";
29 const char kCookieName1[] = "Cookie";
30 const char kCookieName2[] = "Monster";
31 const char kCookieValue1[] = "Eats";
32 const char kCookieValue2[] = "Cookies";
33 const char kCookieValue3[] = "Nyom nyom nyom";
34 
35 // Creates a CanonicalCookie with |name| and |value|, for kTestCookieUrlHost.
CreateCookie(base::StringPiece name,base::StringPiece value)36 std::unique_ptr<net::CanonicalCookie> CreateCookie(base::StringPiece name,
37                                                    base::StringPiece value) {
38   return net::CanonicalCookie::CreateSanitizedCookie(
39       GURL(kTestCookieUrl), name.as_string(), value.as_string(), /*domain=*/"",
40       /*path=*/"", /*creation_time=*/base::Time(),
41       /*expiration_time=*/base::Time(), /*last_access_time=*/base::Time(),
42       /*secure=*/true,
43       /*httponly*/ false, net::CookieSameSite::NO_RESTRICTION,
44       net::COOKIE_PRIORITY_MEDIUM);
45 }
46 
47 class CookieManagerImplTest : public testing::Test {
48  public:
CookieManagerImplTest()49   CookieManagerImplTest()
50       : task_environment_(
51             base::test::TaskEnvironment::MainThreadType::IO),
52         network_service_(network::NetworkService::CreateForTesting()),
53         cookie_manager_(
54             base::BindRepeating(&CookieManagerImplTest::GetNetworkContext,
55                                 base::Unretained(this))) {}
56   ~CookieManagerImplTest() override = default;
57 
58  protected:
GetNetworkContext()59   network::mojom::NetworkContext* GetNetworkContext() {
60     if (!network_context_.is_bound()) {
61       network_service_->CreateNetworkContext(
62           network_context_.BindNewPipeAndPassReceiver(),
63           network::mojom::NetworkContextParams::New());
64       network_context_.reset_on_disconnect();
65     }
66     return network_context_.get();
67   }
68 
69   // Adds the specified cookie under kTestCookieUrlHost.
CreateAndSetCookieAsync(base::StringPiece name,base::StringPiece value)70   void CreateAndSetCookieAsync(base::StringPiece name,
71                                base::StringPiece value) {
72     EnsureMojoCookieManager();
73 
74     net::CookieOptions options;
75     mojo_cookie_manager_->SetCanonicalCookie(
76         *CreateCookie(name, value), "https", options,
77         base::BindOnce([](net::CanonicalCookie::CookieInclusionStatus status) {
78           EXPECT_TRUE(status.IsInclude());
79         }));
80   }
81 
82   // Removes the specified cookie from under kTestCookieUrlHost.
DeleteCookieAsync(base::StringPiece name,base::StringPiece value)83   void DeleteCookieAsync(base::StringPiece name, base::StringPiece value) {
84     EnsureMojoCookieManager();
85 
86     mojo_cookie_manager_->DeleteCanonicalCookie(
87         *CreateCookie(name, value),
88         base::BindOnce([](bool success) { EXPECT_TRUE(success); }));
89   }
90 
91   // Synchronously fetches all cookies via the |cookie_manager_|.
92   // Returns a base::nullopt if the iterator closes before a GetNext() returns.
GetAllCookies()93   base::Optional<std::vector<fuchsia::web::Cookie>> GetAllCookies() {
94     base::RunLoop get_cookies_loop;
95     fuchsia::web::CookiesIteratorPtr cookies_iterator;
96     cookies_iterator.set_error_handler([&](zx_status_t status) {
97       EXPECT_EQ(ZX_ERR_PEER_CLOSED, status);
98       get_cookies_loop.Quit();
99     });
100     cookie_manager_.GetCookieList(nullptr, nullptr,
101                                   cookies_iterator.NewRequest());
102     base::Optional<std::vector<fuchsia::web::Cookie>> cookies;
103     std::function<void(std::vector<fuchsia::web::Cookie>)> get_next_callback =
104         [&](std::vector<fuchsia::web::Cookie> new_cookies) {
105           if (!cookies.has_value()) {
106             cookies.emplace(std::move(new_cookies));
107           } else {
108             cookies->insert(cookies->end(),
109                             std::make_move_iterator(new_cookies.begin()),
110                             std::make_move_iterator(new_cookies.end()));
111           }
112           cookies_iterator->GetNext(get_next_callback);
113         };
114     cookies_iterator->GetNext(get_next_callback);
115     get_cookies_loop.Run();
116     return cookies;
117   }
118 
EnsureMojoCookieManager()119   void EnsureMojoCookieManager() {
120     if (mojo_cookie_manager_.is_bound())
121       return;
122     network_context_->GetCookieManager(
123         mojo_cookie_manager_.BindNewPipeAndPassReceiver());
124   }
125 
126   base::test::TaskEnvironment task_environment_;
127 
128   std::unique_ptr<network::NetworkService> network_service_;
129   mojo::Remote<network::mojom::NetworkContext> network_context_;
130   mojo::Remote<network::mojom::CookieManager> mojo_cookie_manager_;
131 
132   CookieManagerImpl cookie_manager_;
133 
134  private:
135   DISALLOW_COPY_AND_ASSIGN(CookieManagerImplTest);
136 };
137 
138 // Calls GetNext() on the supplied |iterator| and lets the caller express
139 // expectations on the results.
140 class GetNextCookiesIteratorResult {
141  public:
GetNextCookiesIteratorResult(fuchsia::web::CookiesIterator * iterator)142   explicit GetNextCookiesIteratorResult(fuchsia::web::CookiesIterator* iterator)
143       : result_(loop_.QuitClosure()) {
144     iterator->GetNext(
145         cr_fuchsia::CallbackToFitFunction(result_.GetReceiveCallback()));
146   }
147 
148   ~GetNextCookiesIteratorResult() = default;
149 
ExpectSingleCookie(base::StringPiece name,base::Optional<base::StringPiece> value)150   void ExpectSingleCookie(base::StringPiece name,
151                           base::Optional<base::StringPiece> value) {
152     ExpectCookieUpdates({{name, value}});
153   }
154 
ExpectDeleteSingleCookie(base::StringPiece name)155   void ExpectDeleteSingleCookie(base::StringPiece name) {
156     ExpectCookieUpdates({{name, base::nullopt}});
157   }
158 
159   // Specifies the cookie name/value pairs expected in the GetNext() results.
160   // Deletions expectations are specified by using base::nullopt as the value.
ExpectCookieUpdates(std::map<base::StringPiece,base::Optional<base::StringPiece>> expected)161   void ExpectCookieUpdates(
162       std::map<base::StringPiece, base::Optional<base::StringPiece>> expected) {
163     loop_.Run();
164     ASSERT_TRUE(result_.has_value());
165     ASSERT_EQ(result_->size(), expected.size());
166     std::map<base::StringPiece, base::StringPiece> result_updates;
167     for (auto& cookie_update : *result_) {
168       ASSERT_TRUE(cookie_update.has_id());
169       ASSERT_TRUE(cookie_update.id().has_name());
170       auto it = expected.find(cookie_update.id().name());
171       ASSERT_TRUE(it != expected.end());
172       ASSERT_EQ(cookie_update.has_value(), it->second.has_value());
173       if (it->second.has_value())
174         EXPECT_EQ(*it->second, cookie_update.value());
175       expected.erase(it);
176     }
177     EXPECT_TRUE(expected.empty());
178   }
179 
ExpectReceivedNoUpdates()180   void ExpectReceivedNoUpdates() {
181     // If we ran |loop_| then this would hang, so just ensure any pending work
182     // has been processed.
183     base::RunLoop().RunUntilIdle();
184     EXPECT_FALSE(result_.has_value());
185   }
186 
187  protected:
188   base::RunLoop loop_;
189   cr_fuchsia::ResultReceiver<std::vector<fuchsia::web::Cookie>> result_;
190 
191   DISALLOW_COPY_AND_ASSIGN(GetNextCookiesIteratorResult);
192 };
193 
194 }  // namespace
195 
TEST_F(CookieManagerImplTest,GetAndObserveAddModifyDelete)196 TEST_F(CookieManagerImplTest, GetAndObserveAddModifyDelete) {
197   // Add global, URL-filtered and URL+name-filtered observers.
198   fuchsia::web::CookiesIteratorPtr global_changes;
199   global_changes.set_error_handler([](zx_status_t) { ADD_FAILURE(); });
200   cookie_manager_.ObserveCookieChanges(nullptr, nullptr,
201                                        global_changes.NewRequest());
202 
203   fuchsia::web::CookiesIteratorPtr url_changes;
204   url_changes.set_error_handler([](zx_status_t) { ADD_FAILURE(); });
205   cookie_manager_.ObserveCookieChanges(kTestCookieUrl, nullptr,
206                                        url_changes.NewRequest());
207 
208   fuchsia::web::CookiesIteratorPtr name_changes;
209   name_changes.set_error_handler([](zx_status_t) { ADD_FAILURE(); });
210   cookie_manager_.ObserveCookieChanges(kTestCookieUrl, kCookieName1,
211                                        name_changes.NewRequest());
212 
213   // Register interest in updates for another URL, so we can verify none are
214   // received.
215   fuchsia::web::CookiesIteratorPtr other_changes;
216   name_changes.set_error_handler([](zx_status_t) { ADD_FAILURE(); });
217   cookie_manager_.ObserveCookieChanges(kTestOtherUrl, nullptr,
218                                        other_changes.NewRequest());
219   GetNextCookiesIteratorResult other_updates(other_changes.get());
220 
221   // Ensure that all ObserveCookieChanges() were processed before modifying
222   // cookies.
223   EXPECT_EQ(GetAllCookies()->size(), 0u);
224 
225   // Set cookie kCookieName1, which should trigger notifications to all
226   // observers.
227   {
228     GetNextCookiesIteratorResult global_update(global_changes.get());
229     GetNextCookiesIteratorResult url_update(url_changes.get());
230     GetNextCookiesIteratorResult name_update(name_changes.get());
231 
232     CreateAndSetCookieAsync(kCookieName1, kCookieValue1);
233 
234     global_update.ExpectSingleCookie(kCookieName1, kCookieValue1);
235     url_update.ExpectSingleCookie(kCookieName1, kCookieValue1);
236     name_update.ExpectSingleCookie(kCookieName1, kCookieValue1);
237   }
238 
239   // Expect an add notification for kCookieName2, except on the with-name
240   // observer. If the with-name observer does get notified then the remove &
241   // re-add check below will observe kCookieName2 rather than kCookieName1, and
242   // fail.
243   {
244     GetNextCookiesIteratorResult global_update(global_changes.get());
245     GetNextCookiesIteratorResult url_update(url_changes.get());
246 
247     CreateAndSetCookieAsync(kCookieName2, kCookieValue2);
248 
249     global_update.ExpectSingleCookie(kCookieName2, kCookieValue2);
250     url_update.ExpectSingleCookie(kCookieName2, kCookieValue2);
251   }
252 
253   // Set kCookieName1 to a new value, which will trigger deletion notifications,
254   // followed by an addition with the new value.
255   {
256     GetNextCookiesIteratorResult global_update(global_changes.get());
257     GetNextCookiesIteratorResult url_update(url_changes.get());
258     GetNextCookiesIteratorResult name_update(name_changes.get());
259 
260     // Updating the cookie will generate a deletion, following by an insertion.
261     // CookiesIterator will batch updates into a single response, so we may get
262     // two separate updates, or a single update, depending on timing. Eliminate
263     // the non-determinism by ensuring that the GetNext() calls have been
264     // received before updating the cookie.
265     base::RunLoop().RunUntilIdle();
266 
267     CreateAndSetCookieAsync(kCookieName1, kCookieValue3);
268 
269     global_update.ExpectDeleteSingleCookie(kCookieName1);
270     url_update.ExpectDeleteSingleCookie(kCookieName1);
271     name_update.ExpectDeleteSingleCookie(kCookieName1);
272   }
273   {
274     GetNextCookiesIteratorResult global_update(global_changes.get());
275     GetNextCookiesIteratorResult url_update(url_changes.get());
276     GetNextCookiesIteratorResult name_update(name_changes.get());
277 
278     global_update.ExpectSingleCookie(kCookieName1, kCookieValue3);
279     url_update.ExpectSingleCookie(kCookieName1, kCookieValue3);
280     name_update.ExpectSingleCookie(kCookieName1, kCookieValue3);
281   }
282 
283   // Set kCookieName2 empty, which will notify only the global and URL
284   // observers. If the name observer is mis-notified then the next step, below,
285   // will fail.
286   {
287     GetNextCookiesIteratorResult global_update(global_changes.get());
288     GetNextCookiesIteratorResult url_update(url_changes.get());
289 
290     DeleteCookieAsync(kCookieName2, kCookieValue2);
291 
292     global_update.ExpectDeleteSingleCookie(kCookieName2);
293     url_update.ExpectDeleteSingleCookie(kCookieName2);
294   }
295 
296   // Set kCookieName1 empty, which will notify all the observers that it was
297   // removed.
298   {
299     GetNextCookiesIteratorResult global_update(global_changes.get());
300     GetNextCookiesIteratorResult url_update(url_changes.get());
301     GetNextCookiesIteratorResult name_update(name_changes.get());
302 
303     DeleteCookieAsync(kCookieName1, kCookieValue3);
304 
305     global_update.ExpectDeleteSingleCookie(kCookieName1);
306     url_update.ExpectDeleteSingleCookie(kCookieName1);
307     name_update.ExpectDeleteSingleCookie(kCookieName1);
308   }
309 
310   // Verify that no updates were received for the "other" URL (since we did not
311   // set any cookies for it). It is possible that this could pass due to the
312   // CookiesIterator not having been scheduled, but that is very unlikely.
313   other_updates.ExpectReceivedNoUpdates();
314 }
315 
TEST_F(CookieManagerImplTest,UpdateBatching)316 TEST_F(CookieManagerImplTest, UpdateBatching) {
317   fuchsia::web::CookiesIteratorPtr global_changes;
318   global_changes.set_error_handler([](zx_status_t) { ADD_FAILURE(); });
319   cookie_manager_.ObserveCookieChanges(nullptr, nullptr,
320                                        global_changes.NewRequest());
321 
322   // Ensure that all ObserveCookieChanges() were processed before modifying
323   // cookies.
324   EXPECT_EQ(GetAllCookies()->size(), 0u);
325 
326   {
327     // Verify that some insertions are batched into a single GetNext() result.
328     CreateAndSetCookieAsync(kCookieName1, kCookieValue1);
329     CreateAndSetCookieAsync(kCookieName2, kCookieValue2);
330     CreateAndSetCookieAsync(kCookieName1, kCookieValue3);
331 
332     // Flush the Cookie Manager so that all cookie changes are processed.
333     mojo_cookie_manager_.FlushForTesting();
334 
335     // Run all pending tasks so that CookiesIteratorImpl receives all cookie
336     // changes through network::mojom::CookieChangeListener::OnCookieChange().
337     // This is important because fuchsia::web::CookiesIterator::GetNext() only
338     // returns cookie updates that have already been received by the iterator
339     // implementation.
340     base::RunLoop().RunUntilIdle();
341 
342     // Request cookie updates through fuchsia::web::CookiesIterator::GetNext().
343     // Multiple updates to the same cookie should be coalesced.
344     GetNextCookiesIteratorResult global_updates(global_changes.get());
345     global_updates.ExpectCookieUpdates(
346         {{kCookieName1, kCookieValue3}, {kCookieName2, kCookieValue2}});
347   }
348 
349   {
350     // Verify that some deletions are batched into a single GetNext() result.
351     DeleteCookieAsync(kCookieName2, kCookieValue2);
352     DeleteCookieAsync(kCookieName1, kCookieValue3);
353     mojo_cookie_manager_.FlushForTesting();
354 
355     GetNextCookiesIteratorResult global_updates(global_changes.get());
356     global_updates.ExpectCookieUpdates(
357         {{kCookieName1, base::nullopt}, {kCookieName2, base::nullopt}});
358   }
359 }
360 
TEST_F(CookieManagerImplTest,ReconnectToNetworkContext)361 TEST_F(CookieManagerImplTest, ReconnectToNetworkContext) {
362   // Attach a cookie observer, which we expect should become disconnected with
363   // an appropriate error if the NetworkService goes away.
364   base::RunLoop mojo_disconnect_loop;
365   cookie_manager_.set_on_mojo_disconnected_for_test(
366       mojo_disconnect_loop.QuitClosure());
367 
368   // Verify that GetAllCookies() returns a valid list of cookies (as opposed to
369   // not returning a list at all) initially.
370   EXPECT_TRUE(GetAllCookies().has_value());
371 
372   // Tear-down and re-create the NetworkService and |network_context_|, causing
373   // the CookieManager's connection to it to be dropped.
374   network_service_.reset();
375   network_context_.reset();
376   network_service_ = network::NetworkService::CreateForTesting();
377 
378   // Wait for the |cookie_manager_| to observe the NetworkContext disconnect,
379   // so that GetAllCookies() can re-connect.
380   mojo_disconnect_loop.Run();
381 
382   // If the CookieManager fails to re-connect then GetAllCookies() will receive
383   // no data (as opposed to receiving an empty list of cookies).
384   EXPECT_TRUE(GetAllCookies().has_value());
385 }
386