Repository: aio-libs/aiohttp

Issue:
Subsequent removal and addition of a cookie may leave the cookie removed
## Long story short

I encountered a flaky test failure when testing login-logout-login and discovered that a login right after a logout may not work, in other words, the authentication cookie may not be set.

This boiled down to the following error in `CookieJar`: for event loops with poor time resolution (in my case `uvloop`), subsequent removal and addition of a cookie may not add the cookie.

Here is a test case:
```python
import time
from http.cookies import SimpleCookie

from aiohttp.cookiejar import CookieJar
from uvloop import new_event_loop


def test_uvloop_cookies():
    loop = new_event_loop()

    for _ in range(1000):
        jar = CookieJar(unsafe=True, loop=loop)

        # Remove`test` cookie.
        jar.update_cookies(SimpleCookie('test=""; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/'))
        # Set `test` cookie to `bar`.
        jar.update_cookies(SimpleCookie('test="bar"'))

        # Assert that there a cookie.
        assert list(jar)
        time.sleep(0.1)
        assert list(jar)
```

Although it took me quite a bit of time to discover what's going on, it seems there is an extremely simple fix: replace strict comparison (`<`) with non-strict one (`<=`) [here](https://github.com/aio-libs/aiohttp/blob/eee65a4eed846394082c5e931fc75f9c1e7f30cd/aiohttp/cookiejar.py#L77).
I'll provide a PR soon if maintainers agree.

## Expected behaviour

Test passes

## Actual behaviour

Test fails

## Steps to reproduce

Run the test

## Your environment

uvloop=0.8.0, aiohttp=2.1.0



Discussion:
Would you make a PR (unittest required)?
Yes, I'll submit by tomorrow hopefully.

Resolving patch:
diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py
--- a/aiohttp/cookiejar.py
+++ b/aiohttp/cookiejar.py
@@ -74,7 +74,7 @@ def _do_expiration(self):
         cookies = self._cookies
         expirations = self._expirations
         for (domain, name), when in expirations.items():
-            if when < now:
+            if when <= now:
                 cookies[domain].pop(name, None)
                 to_del.append((domain, name))
                 self._host_only_cookies.discard((domain, name))


Test patch:
diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py
--- a/tests/test_cookiejar.py
+++ b/tests/test_cookiejar.py
@@ -1,5 +1,6 @@
 import asyncio
 import datetime
+import itertools
 import os
 import tempfile
 import unittest
@@ -577,3 +578,21 @@ def test_invalid_values(self):
 
         cookie = cookies_sent["invalid-expires-cookie"]
         self.assertEqual(cookie["expires"], "")
+
+    def test_cookie_not_expired_when_added_after_removal(self):
+        """Test case for https://github.com/aio-libs/aiohttp/issues/2084"""
+        timestamps = [533588.993, 533588.993, 533588.993,
+                      533588.993, 533589.093, 533589.093]
+
+        loop = mock.Mock()
+        loop.time.side_effect = itertools.chain(
+            timestamps, itertools.cycle([timestamps[-1]]))
+
+        jar = CookieJar(unsafe=True, loop=loop)
+        # Remove `foo` cookie.
+        jar.update_cookies(SimpleCookie('foo=""; Max-Age=0'))
+        # Set `foo` cookie to `bar`.
+        jar.update_cookies(SimpleCookie('foo="bar"'))
+
+        # Assert that there is a cookie.
+        assert len(jar) == 1