Add a method to override all the cookies in CookieMonster

This method is required in order to upstream CookieStoreIOS.

CookieStoreIOS can periodically synchronize the iOS system
cookie store (NSHTTPCookieStorage) to CookieMonster.

It is not possible to simply delete all the cookies and re-add
the new version of the cookies, because this can lead to cookie
loss if the application crashes right after deleting all cookies.
For this reason, this CL introduces a new SetAllCookies method
that computes the exact difference between the exiting cookies and
the new cookies, and does only the minimum of operations needed.

Review URL: https://codereview.chromium.org/1000103002

Cr-Commit-Position: refs/heads/master@{#320954}
diff --git a/net/cookies/canonical_cookie.cc b/net/cookies/canonical_cookie.cc
index 227c70f..5932c23 100644
--- a/net/cookies/canonical_cookie.cc
+++ b/net/cookies/canonical_cookie.cc
@@ -102,6 +102,21 @@
   return url_path.substr(0, idx);
 }
 
+// Compares cookies using name, domain and path, so that "equivalent" cookies
+// (per RFC 2965) are equal to each other.
+int PartialCookieOrdering(const net::CanonicalCookie& a,
+                          const net::CanonicalCookie& b) {
+  int diff = a.Name().compare(b.Name());
+  if (diff != 0)
+    return diff;
+
+  diff = a.Domain().compare(b.Domain());
+  if (diff != 0)
+    return diff;
+
+  return a.Path().compare(b.Path());
+}
+
 }  // namespace
 
 CanonicalCookie::CanonicalCookie()
@@ -410,4 +425,39 @@
       static_cast<int64>(creation_date_.ToTimeT()));
 }
 
+bool CanonicalCookie::PartialCompare(const CanonicalCookie& other) const {
+  return PartialCookieOrdering(*this, other) < 0;
+}
+
+bool CanonicalCookie::FullCompare(const CanonicalCookie& other) const {
+  // Do the partial comparison first.
+  int diff = PartialCookieOrdering(*this, other);
+  if (diff != 0)
+    return diff < 0;
+
+  DCHECK(IsEquivalent(other));
+
+  // Compare other fields.
+  diff = Value().compare(other.Value());
+  if (diff != 0)
+    return diff < 0;
+
+  if (CreationDate() != other.CreationDate())
+    return CreationDate() < other.CreationDate();
+
+  if (ExpiryDate() != other.ExpiryDate())
+    return ExpiryDate() < other.ExpiryDate();
+
+  if (LastAccessDate() != other.LastAccessDate())
+    return LastAccessDate() < other.LastAccessDate();
+
+  if (IsSecure() != other.IsSecure())
+    return IsSecure();
+
+  if (IsHttpOnly() != other.IsHttpOnly())
+    return IsHttpOnly();
+
+  return Priority() < other.Priority();
+}
+
 }  // namespace net
diff --git a/net/cookies/canonical_cookie.h b/net/cookies/canonical_cookie.h
index 27051a3..30fb3e76 100644
--- a/net/cookies/canonical_cookie.h
+++ b/net/cookies/canonical_cookie.h
@@ -136,6 +136,18 @@
                                     const base::Time& current,
                                     const base::Time& server_time);
 
+  // Cookie ordering methods.
+
+  // Returns true if the cookie is less than |other|, considering only name,
+  // domain and path. In particular, two equivalent cookies (see IsEquivalent())
+  // are identical for PartialCompare().
+  bool PartialCompare(const CanonicalCookie& other) const;
+
+  // Returns true if the cookie is less than |other|, considering all fields.
+  // FullCompare() is consistent with PartialCompare(): cookies sorted using
+  // FullCompare() are also sorted with respect to PartialCompare().
+  bool FullCompare(const CanonicalCookie& other) const;
+
  private:
   // The source member of a canonical cookie is the origin of the URL that tried
   // to set this cookie, minus the port number if any.  This field is not
diff --git a/net/cookies/canonical_cookie_unittest.cc b/net/cookies/canonical_cookie_unittest.cc
index 78af2e07..fc0d4537 100644
--- a/net/cookies/canonical_cookie_unittest.cc
+++ b/net/cookies/canonical_cookie_unittest.cc
@@ -407,4 +407,67 @@
   EXPECT_FALSE(cookie->IncludeForRequestURL(secure_url, options));
 }
 
+TEST(CanonicalCookieTest, PartialCompare) {
+  GURL url("http://www.example.com");
+  base::Time creation_time = base::Time::Now();
+  CookieOptions options;
+  scoped_ptr<CanonicalCookie> cookie(
+      CanonicalCookie::Create(url, "a=b", creation_time, options));
+  scoped_ptr<CanonicalCookie> cookie_different_path(
+      CanonicalCookie::Create(url, "a=b; path=/foo", creation_time, options));
+  scoped_ptr<CanonicalCookie> cookie_different_value(
+      CanonicalCookie::Create(url, "a=c", creation_time, options));
+
+  // Cookie is equivalent to itself.
+  EXPECT_FALSE(cookie->PartialCompare(*cookie));
+
+  // Changing the path affects the ordering.
+  EXPECT_TRUE(cookie->PartialCompare(*cookie_different_path));
+  EXPECT_FALSE(cookie_different_path->PartialCompare(*cookie));
+
+  // Changing the value does not affect the ordering.
+  EXPECT_FALSE(cookie->PartialCompare(*cookie_different_value));
+  EXPECT_FALSE(cookie_different_value->PartialCompare(*cookie));
+
+  // Cookies identical for PartialCompare() are equivalent.
+  EXPECT_TRUE(cookie->IsEquivalent(*cookie_different_value));
+  EXPECT_TRUE(cookie->IsEquivalent(*cookie));
+}
+
+TEST(CanonicalCookieTest, FullCompare) {
+  GURL url("http://www.example.com");
+  base::Time creation_time = base::Time::Now();
+  CookieOptions options;
+  scoped_ptr<CanonicalCookie> cookie(
+      CanonicalCookie::Create(url, "a=b", creation_time, options));
+  scoped_ptr<CanonicalCookie> cookie_different_path(
+      CanonicalCookie::Create(url, "a=b; path=/foo", creation_time, options));
+  scoped_ptr<CanonicalCookie> cookie_different_value(
+      CanonicalCookie::Create(url, "a=c", creation_time, options));
+
+  // Cookie is equivalent to itself.
+  EXPECT_FALSE(cookie->FullCompare(*cookie));
+
+  // Changing the path affects the ordering.
+  EXPECT_TRUE(cookie->FullCompare(*cookie_different_path));
+  EXPECT_FALSE(cookie_different_path->FullCompare(*cookie));
+
+  // Changing the value affects the ordering.
+  EXPECT_TRUE(cookie->FullCompare(*cookie_different_value));
+  EXPECT_FALSE(cookie_different_value->FullCompare(*cookie));
+
+  // FullCompare() implies PartialCompare().
+  auto check_consistency =
+      [](const CanonicalCookie& a, const CanonicalCookie& b) {
+        if (a.FullCompare(b))
+          EXPECT_FALSE(b.PartialCompare(a));
+        else if (b.FullCompare(a))
+          EXPECT_FALSE(a.PartialCompare(b));
+      };
+
+  check_consistency(*cookie, *cookie_different_path);
+  check_consistency(*cookie, *cookie_different_value);
+  check_consistency(*cookie_different_path, *cookie_different_value);
+}
+
 }  // namespace net
diff --git a/net/cookies/cookie_monster.cc b/net/cookies/cookie_monster.cc
index 6947756a..aa39b60 100644
--- a/net/cookies/cookie_monster.cc
+++ b/net/cookies/cookie_monster.cc
@@ -163,6 +163,20 @@
   return it1->second->CreationDate() < it2->second->CreationDate();
 }
 
+// Compare cookies using name, domain and path, so that "equivalent" cookies
+// (per RFC 2965) are equal to each other.
+bool PartialDiffCookieSorter(const net::CanonicalCookie& a,
+                             const net::CanonicalCookie& b) {
+  return a.PartialCompare(b);
+}
+
+// This is a stricter ordering than PartialDiffCookieOrdering, where all fields
+// are used.
+bool FullDiffCookieSorter(const net::CanonicalCookie& a,
+                          const net::CanonicalCookie& b) {
+  return a.FullCompare(b);
+}
+
 // Our strategy to find duplicates is:
 // (1) Build a map from (cookiename, cookiepath) to
 //     {list of cookies with this signature, sorted by creation time}.
@@ -764,6 +778,49 @@
   }
 }
 
+// Task class for SetAllCookies call.
+class CookieMonster::SetAllCookiesTask : public CookieMonsterTask {
+ public:
+  SetAllCookiesTask(CookieMonster* cookie_monster,
+                    const CookieList& list,
+                    const SetCookiesCallback& callback)
+      : CookieMonsterTask(cookie_monster), list_(list), callback_(callback) {}
+
+  // CookieMonsterTask:
+  void Run() override;
+
+ protected:
+  ~SetAllCookiesTask() override {}
+
+ private:
+  CookieList list_;
+  SetCookiesCallback callback_;
+
+  DISALLOW_COPY_AND_ASSIGN(SetAllCookiesTask);
+};
+
+void CookieMonster::SetAllCookiesTask::Run() {
+  CookieList positive_diff;
+  CookieList negative_diff;
+  CookieList old_cookies = this->cookie_monster()->GetAllCookies();
+  this->cookie_monster()->ComputeCookieDiff(&old_cookies, &list_,
+                                            &positive_diff, &negative_diff);
+
+  for (CookieList::const_iterator it = negative_diff.begin();
+       it != negative_diff.end(); ++it) {
+    this->cookie_monster()->DeleteCanonicalCookie(*it);
+  }
+
+  bool result = true;
+  if (positive_diff.size() > 0)
+    result = this->cookie_monster()->SetCanonicalCookies(list_);
+
+  if (!callback_.is_null()) {
+    this->InvokeCallback(base::Bind(&SetCookiesCallback::Run,
+                                    base::Unretained(&callback_), result));
+  }
+}
+
 // Task class for GetCookiesWithOptions call.
 class CookieMonster::GetCookiesWithOptionsTask : public CookieMonsterTask {
  public:
@@ -985,6 +1042,13 @@
   DoCookieTask(task);
 }
 
+void CookieMonster::SetAllCookiesAsync(const CookieList& list,
+                                       const SetCookiesCallback& callback) {
+  scoped_refptr<SetAllCookiesTask> task =
+      new SetAllCookiesTask(this, list, callback);
+  DoCookieTask(task);
+}
+
 void CookieMonster::SetCookieWithOptionsAsync(
     const GURL& url,
     const std::string& cookie_line,
@@ -1843,6 +1907,21 @@
   return true;
 }
 
+bool CookieMonster::SetCanonicalCookies(const CookieList& list) {
+  base::AutoLock autolock(lock_);
+
+  net::CookieOptions options;
+  options.set_include_httponly();
+
+  for (CookieList::const_iterator it = list.begin(); it != list.end(); ++it) {
+    scoped_ptr<CanonicalCookie> canonical_cookie(new CanonicalCookie(*it));
+    if (!SetCanonicalCookie(&canonical_cookie, it->CreationDate(), options))
+      return false;
+  }
+
+  return true;
+}
+
 void CookieMonster::InternalUpdateCookieAccessTime(CanonicalCookie* cc,
                                                    const Time& current) {
   lock_.AssertAcquired();
@@ -2226,6 +2305,39 @@
                                    last_time_seen_.ToInternalValue() + 1));
 }
 
+void CookieMonster::ComputeCookieDiff(CookieList* old_cookies,
+                                      CookieList* new_cookies,
+                                      CookieList* cookies_to_add,
+                                      CookieList* cookies_to_delete) {
+  DCHECK(old_cookies);
+  DCHECK(new_cookies);
+  DCHECK(cookies_to_add);
+  DCHECK(cookies_to_delete);
+  DCHECK(cookies_to_add->empty());
+  DCHECK(cookies_to_delete->empty());
+
+  // Sort both lists.
+  // A set ordered by FullDiffCookieSorter is also ordered by
+  // PartialDiffCookieSorter.
+  std::sort(old_cookies->begin(), old_cookies->end(), FullDiffCookieSorter);
+  std::sort(new_cookies->begin(), new_cookies->end(), FullDiffCookieSorter);
+
+  // Select any old cookie for deletion if no new cookie has the same name,
+  // domain, and path.
+  std::set_difference(
+      old_cookies->begin(), old_cookies->end(), new_cookies->begin(),
+      new_cookies->end(),
+      std::inserter(*cookies_to_delete, cookies_to_delete->begin()),
+      PartialDiffCookieSorter);
+
+  // Select any new cookie for addition (or update) if no old cookie is exactly
+  // equivalent.
+  std::set_difference(new_cookies->begin(), new_cookies->end(),
+                      old_cookies->begin(), old_cookies->end(),
+                      std::inserter(*cookies_to_add, cookies_to_add->begin()),
+                      FullDiffCookieSorter);
+}
+
 scoped_ptr<CookieStore::CookieChangedSubscription>
 CookieMonster::AddCallbackForCookie(const GURL& gurl,
                                     const std::string& name,
diff --git a/net/cookies/cookie_monster.h b/net/cookies/cookie_monster.h
index 79f1bbf..5633baf 100644
--- a/net/cookies/cookie_monster.h
+++ b/net/cookies/cookie_monster.h
@@ -234,6 +234,10 @@
   // to the thread you actually want to be notified on.
   void FlushStore(const base::Closure& callback);
 
+  // Replaces all the cookies by |list|. This method does not flush the backend.
+  void SetAllCookiesAsync(const CookieList& list,
+                          const SetCookiesCallback& callback);
+
   // CookieStore implementation.
 
   // Sets the cookies specified by |cookie_list| returned from |url|
@@ -322,6 +326,7 @@
   class GetAllCookiesForURLWithOptionsTask;
   class GetAllCookiesTask;
   class GetCookiesWithOptionsTask;
+  class SetAllCookiesTask;
   class SetCookieWithDetailsTask;
   class SetCookieWithOptionsTask;
   class DeleteSessionCookiesTask;
@@ -350,6 +355,9 @@
   // For FindCookiesForKey.
   FRIEND_TEST_ALL_PREFIXES(CookieMonsterTest, ShortLivedSessionCookies);
 
+  // For ComputeCookieDiff.
+  FRIEND_TEST_ALL_PREFIXES(CookieMonsterTest, ComputeCookieDiff);
+
   // Internal reasons for deletion, used to populate informative histograms
   // and to provide a public cause for onCookieChange notifications.
   //
@@ -544,6 +552,9 @@
                           const base::Time& creation_time,
                           const CookieOptions& options);
 
+  // Helper function calling SetCanonicalCookie() for all cookies in |list|.
+  bool SetCanonicalCookies(const CookieList& list);
+
   void InternalUpdateCookieAccessTime(CanonicalCookie* cc,
                                       const base::Time& current_time);
 
@@ -608,6 +619,16 @@
   void DoCookieTaskForURL(const scoped_refptr<CookieMonsterTask>& task_item,
                           const GURL& url);
 
+  // Computes the difference between |old_cookies| and |new_cookies|, and writes
+  // the result in |cookies_to_add| and |cookies_to_delete|.
+  // This function has the side effect of changing the order of |old_cookies|
+  // and |new_cookies|. |cookies_to_add| and |cookies_to_delete| must be empty,
+  // and none of the arguments can be null.
+  void ComputeCookieDiff(CookieList* old_cookies,
+                         CookieList* new_cookies,
+                         CookieList* cookies_to_add,
+                         CookieList* cookies_to_delete);
+
   // Run all cookie changed callbacks that are monitoring |cookie|.
   // |removed| is true if the cookie was deleted.
   void RunCallbacks(const CanonicalCookie& cookie, bool removed);
diff --git a/net/cookies/cookie_monster_unittest.cc b/net/cookies/cookie_monster_unittest.cc
index 7ca99a3..60caa67 100644
--- a/net/cookies/cookie_monster_unittest.cc
+++ b/net/cookies/cookie_monster_unittest.cc
@@ -166,6 +166,17 @@
     return callback.result();
   }
 
+  bool SetAllCookies(CookieMonster* cm, const CookieList& list) {
+    DCHECK(cm);
+    ResultSavingCookieCallback<bool> callback;
+    cm->SetAllCookiesAsync(list,
+                           base::Bind(&ResultSavingCookieCallback<bool>::Run,
+                                      base::Unretained(&callback)));
+    RunFor(kTimeout);
+    EXPECT_TRUE(callback.did_run());
+    return callback.result();
+  }
+
   int DeleteAll(CookieMonster* cm) {
     DCHECK(cm);
     ResultSavingCookieCallback<int> callback;
@@ -542,6 +553,24 @@
     }
     return cm;
   }
+
+  bool IsCookieInList(const CanonicalCookie& cookie, const CookieList& list) {
+    for (CookieList::const_iterator it = list.begin(); it != list.end(); ++it) {
+      if (it->Source() == cookie.Source() && it->Name() == cookie.Name() &&
+          it->Value() == cookie.Value() && it->Domain() == cookie.Domain() &&
+          it->Path() == cookie.Path() &&
+          it->CreationDate() == cookie.CreationDate() &&
+          it->ExpiryDate() == cookie.ExpiryDate() &&
+          it->LastAccessDate() == cookie.LastAccessDate() &&
+          it->IsSecure() == cookie.IsSecure() &&
+          it->IsHttpOnly() == cookie.IsHttpOnly() &&
+          it->Priority() == cookie.Priority()) {
+        return true;
+      }
+    }
+
+    return false;
+  }
 };
 
 // TODO(erikwright): Replace the other callbacks and synchronous helper methods
@@ -625,6 +654,9 @@
   cookie_monster->SetCookieWithOptionsAsync(url, cookie_line, CookieOptions(),
                                             callback->AsCallback());
 }
+ACTION_P3(SetAllCookiesAction, cookie_monster, list, callback) {
+  cookie_monster->SetAllCookiesAsync(list, callback->AsCallback());
+}
 ACTION_P4(DeleteAllCreatedBetweenAction,
           cookie_monster,
           delete_begin,
@@ -824,6 +856,30 @@
   CompleteLoadingAndWait();
 }
 
+TEST_F(DeferredCookieTaskTest, DeferredSetAllCookies) {
+  MockSetCookiesCallback set_cookies_callback;
+  CookieList list;
+  list.push_back(CanonicalCookie(url_google_, "A", "B", "google.izzle", "/",
+                                 base::Time::Now(), base::Time(), base::Time(),
+                                 false, true, false, COOKIE_PRIORITY_DEFAULT));
+  list.push_back(CanonicalCookie(url_google_, "C", "D", "google.izzle", "/",
+                                 base::Time::Now(), base::Time(), base::Time(),
+                                 false, true, false, COOKIE_PRIORITY_DEFAULT));
+
+  BeginWith(
+      SetAllCookiesAction(&cookie_monster(), list, &set_cookies_callback));
+
+  WaitForLoadCall();
+
+  EXPECT_CALL(set_cookies_callback, Invoke(true))
+      .WillOnce(
+          SetAllCookiesAction(&cookie_monster(), list, &set_cookies_callback));
+  EXPECT_CALL(set_cookies_callback, Invoke(true))
+      .WillOnce(QuitCurrentMessageLoop());
+
+  CompleteLoadingAndWait();
+}
+
 TEST_F(DeferredCookieTaskTest, DeferredDeleteCookie) {
   MockClosure delete_cookie_callback;
 
@@ -2106,6 +2162,155 @@
   ASSERT_EQ(3, counter->callback_count());
 }
 
+TEST_F(CookieMonsterTest, SetAllCookies) {
+  scoped_refptr<FlushablePersistentStore> store(new FlushablePersistentStore());
+  scoped_refptr<CookieMonster> cm(new CookieMonster(store.get(), NULL));
+  cm->SetPersistSessionCookies(true);
+
+  EXPECT_TRUE(SetCookie(cm.get(), url_google_, "U=V; path=/"));
+  EXPECT_TRUE(SetCookie(cm.get(), url_google_, "W=X; path=/foo"));
+  EXPECT_TRUE(SetCookie(cm.get(), url_google_, "Y=Z; path=/"));
+
+  CookieList list;
+  list.push_back(CanonicalCookie(url_google_, "A", "B", url_google_.host(), "/",
+                                 base::Time::Now(), base::Time(), base::Time(),
+                                 false, false, false, COOKIE_PRIORITY_DEFAULT));
+  list.push_back(CanonicalCookie(url_google_, "W", "X", url_google_.host(),
+                                 "/bar", base::Time::Now(), base::Time(),
+                                 base::Time(), false, false, false,
+                                 COOKIE_PRIORITY_DEFAULT));
+  list.push_back(CanonicalCookie(url_google_, "Y", "Z", url_google_.host(), "/",
+                                 base::Time::Now(), base::Time(), base::Time(),
+                                 false, false, false, COOKIE_PRIORITY_DEFAULT));
+
+  // SetAllCookies must not flush.
+  ASSERT_EQ(0, store->flush_count());
+  EXPECT_TRUE(SetAllCookies(cm.get(), list));
+  EXPECT_EQ(0, store->flush_count());
+
+  CookieList cookies = GetAllCookies(cm.get());
+  size_t expected_size = 3;  // "A", "W" and "Y". "U" is gone.
+  EXPECT_EQ(expected_size, cookies.size());
+  CookieList::iterator it = cookies.begin();
+
+  ASSERT_TRUE(it != cookies.end());
+  EXPECT_EQ("W", it->Name());
+  EXPECT_EQ("X", it->Value());
+  EXPECT_EQ("/bar", it->Path());  // The path has been updated.
+
+  ASSERT_TRUE(++it != cookies.end());
+  EXPECT_EQ("A", it->Name());
+  EXPECT_EQ("B", it->Value());
+
+  ASSERT_TRUE(++it != cookies.end());
+  EXPECT_EQ("Y", it->Name());
+  EXPECT_EQ("Z", it->Value());
+}
+
+TEST_F(CookieMonsterTest, ComputeCookieDiff) {
+  scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL));
+
+  base::Time now = base::Time::Now();
+  base::Time creation_time = now - base::TimeDelta::FromSeconds(1);
+
+  CanonicalCookie cookie1(url_google_, "A", "B", url_google_.host(), "/",
+                          creation_time, base::Time(), base::Time(), false,
+                          false, false, COOKIE_PRIORITY_DEFAULT);
+  CanonicalCookie cookie2(url_google_, "C", "D", url_google_.host(), "/",
+                          creation_time, base::Time(), base::Time(), false,
+                          false, false, COOKIE_PRIORITY_DEFAULT);
+  CanonicalCookie cookie3(url_google_, "E", "F", url_google_.host(), "/",
+                          creation_time, base::Time(), base::Time(), false,
+                          false, false, COOKIE_PRIORITY_DEFAULT);
+  CanonicalCookie cookie4(url_google_, "G", "H", url_google_.host(), "/",
+                          creation_time, base::Time(), base::Time(), false,
+                          false, false, COOKIE_PRIORITY_DEFAULT);
+  CanonicalCookie cookie4_with_new_value(
+      url_google_, "G", "iamnew", url_google_.host(), "/", creation_time,
+      base::Time(), base::Time(), false, false, false, COOKIE_PRIORITY_DEFAULT);
+  CanonicalCookie cookie5(url_google_, "I", "J", url_google_.host(), "/",
+                          creation_time, base::Time(), base::Time(), false,
+                          false, false, COOKIE_PRIORITY_DEFAULT);
+  CanonicalCookie cookie5_with_new_creation_time(
+      url_google_, "I", "J", url_google_.host(), "/", now, base::Time(),
+      base::Time(), false, false, false, COOKIE_PRIORITY_DEFAULT);
+  CanonicalCookie cookie6(url_google_, "K", "L", url_google_.host(), "/foo",
+                          creation_time, base::Time(), base::Time(), false,
+                          false, false, COOKIE_PRIORITY_DEFAULT);
+  CanonicalCookie cookie6_with_new_path(
+      url_google_, "K", "L", url_google_.host(), "/bar", creation_time,
+      base::Time(), base::Time(), false, false, false, COOKIE_PRIORITY_DEFAULT);
+  CanonicalCookie cookie7(url_google_, "M", "N", url_google_.host(), "/foo",
+                          creation_time, base::Time(), base::Time(), false,
+                          false, false, COOKIE_PRIORITY_DEFAULT);
+  CanonicalCookie cookie7_with_new_path(
+      url_google_, "M", "N", url_google_.host(), "/bar", creation_time,
+      base::Time(), base::Time(), false, false, false, COOKIE_PRIORITY_DEFAULT);
+
+  CookieList old_cookies;
+  old_cookies.push_back(cookie1);
+  old_cookies.push_back(cookie2);
+  old_cookies.push_back(cookie4);
+  old_cookies.push_back(cookie5);
+  old_cookies.push_back(cookie6);
+  old_cookies.push_back(cookie7);
+
+  CookieList new_cookies;
+  new_cookies.push_back(cookie1);
+  new_cookies.push_back(cookie3);
+  new_cookies.push_back(cookie4_with_new_value);
+  new_cookies.push_back(cookie5_with_new_creation_time);
+  new_cookies.push_back(cookie6_with_new_path);
+  new_cookies.push_back(cookie7);
+  new_cookies.push_back(cookie7_with_new_path);
+
+  CookieList cookies_to_add;
+  CookieList cookies_to_delete;
+
+  cm->ComputeCookieDiff(&old_cookies, &new_cookies, &cookies_to_add,
+                        &cookies_to_delete);
+
+  // |cookie1| has not changed.
+  EXPECT_FALSE(IsCookieInList(cookie1, cookies_to_add));
+  EXPECT_FALSE(IsCookieInList(cookie1, cookies_to_delete));
+
+  // |cookie2| has been deleted.
+  EXPECT_FALSE(IsCookieInList(cookie2, cookies_to_add));
+  EXPECT_TRUE(IsCookieInList(cookie2, cookies_to_delete));
+
+  // |cookie3| has been added.
+  EXPECT_TRUE(IsCookieInList(cookie3, cookies_to_add));
+  EXPECT_FALSE(IsCookieInList(cookie3, cookies_to_delete));
+
+  // |cookie4| has a new value: new cookie overrides the old one (which does not
+  // need to be explicitly removed).
+  EXPECT_FALSE(IsCookieInList(cookie4, cookies_to_add));
+  EXPECT_FALSE(IsCookieInList(cookie4, cookies_to_delete));
+  EXPECT_TRUE(IsCookieInList(cookie4_with_new_value, cookies_to_add));
+  EXPECT_FALSE(IsCookieInList(cookie4_with_new_value, cookies_to_delete));
+
+  // |cookie5| has a new creation time: new cookie overrides the old one (which
+  // does not need to be explicitly removed).
+  EXPECT_FALSE(IsCookieInList(cookie5, cookies_to_add));
+  EXPECT_FALSE(IsCookieInList(cookie5, cookies_to_delete));
+  EXPECT_TRUE(IsCookieInList(cookie5_with_new_creation_time, cookies_to_add));
+  EXPECT_FALSE(
+      IsCookieInList(cookie5_with_new_creation_time, cookies_to_delete));
+
+  // |cookie6| has a new path: the new cookie does not overrides the old one,
+  // which needs to be explicitly removed.
+  EXPECT_FALSE(IsCookieInList(cookie6, cookies_to_add));
+  EXPECT_TRUE(IsCookieInList(cookie6, cookies_to_delete));
+  EXPECT_TRUE(IsCookieInList(cookie6_with_new_path, cookies_to_add));
+  EXPECT_FALSE(IsCookieInList(cookie6_with_new_path, cookies_to_delete));
+
+  // |cookie7| is kept and |cookie7_with_new_path| is added as a new cookie.
+  EXPECT_FALSE(IsCookieInList(cookie7, cookies_to_add));
+  EXPECT_FALSE(IsCookieInList(cookie7, cookies_to_delete));
+  EXPECT_TRUE(IsCookieInList(cookie7_with_new_path, cookies_to_add));
+  EXPECT_FALSE(IsCookieInList(cookie7_with_new_path, cookies_to_delete));
+}
+
 TEST_F(CookieMonsterTest, HistogramCheck) {
   scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL));
   // Should match call in InitializeHistograms, but doesn't really matter