test_cookielib.py revision 3e7de59bd2ac22f677f3a053ade22f44785f15f4
1# -*- coding: utf-8 -*-
2"""Tests for cookielib.py."""
3
4import re, os, time
5from unittest import TestCase
6
7from test import test_support
8
9class DateTimeTests(TestCase):
10
11    def test_time2isoz(self):
12        from cookielib import time2isoz
13
14        base = 1019227000
15        day = 24*3600
16        self.assertEquals(time2isoz(base), "2002-04-19 14:36:40Z")
17        self.assertEquals(time2isoz(base+day), "2002-04-20 14:36:40Z")
18        self.assertEquals(time2isoz(base+2*day), "2002-04-21 14:36:40Z")
19        self.assertEquals(time2isoz(base+3*day), "2002-04-22 14:36:40Z")
20
21        az = time2isoz()
22        bz = time2isoz(500000)
23        for text in (az, bz):
24            self.assert_(re.search(r"^\d{4}-\d\d-\d\d \d\d:\d\d:\d\dZ$", text),
25                         "bad time2isoz format: %s %s" % (az, bz))
26
27    def test_http2time(self):
28        from cookielib import http2time
29
30        def parse_date(text):
31            return time.gmtime(http2time(text))[:6]
32
33        self.assertEquals(parse_date("01 Jan 2001"), (2001, 1, 1, 0, 0, 0.0))
34
35        # this test will break around year 2070
36        self.assertEquals(parse_date("03-Feb-20"), (2020, 2, 3, 0, 0, 0.0))
37
38        # this test will break around year 2048
39        self.assertEquals(parse_date("03-Feb-98"), (1998, 2, 3, 0, 0, 0.0))
40
41    def test_http2time_formats(self):
42        from cookielib import http2time, time2isoz
43
44        # test http2time for supported dates.  Test cases with 2 digit year
45        # will probably break in year 2044.
46        tests = [
47         'Thu, 03 Feb 1994 00:00:00 GMT',  # proposed new HTTP format
48         'Thursday, 03-Feb-94 00:00:00 GMT',  # old rfc850 HTTP format
49         'Thursday, 03-Feb-1994 00:00:00 GMT',  # broken rfc850 HTTP format
50
51         '03 Feb 1994 00:00:00 GMT',  # HTTP format (no weekday)
52         '03-Feb-94 00:00:00 GMT',  # old rfc850 (no weekday)
53         '03-Feb-1994 00:00:00 GMT',  # broken rfc850 (no weekday)
54         '03-Feb-1994 00:00 GMT',  # broken rfc850 (no weekday, no seconds)
55         '03-Feb-1994 00:00',  # broken rfc850 (no weekday, no seconds, no tz)
56
57         '03-Feb-94',  # old rfc850 HTTP format (no weekday, no time)
58         '03-Feb-1994',  # broken rfc850 HTTP format (no weekday, no time)
59         '03 Feb 1994',  # proposed new HTTP format (no weekday, no time)
60
61         # A few tests with extra space at various places
62         '  03   Feb   1994  0:00  ',
63         '  03-Feb-1994  ',
64        ]
65
66        test_t = 760233600  # assume broken POSIX counting of seconds
67        result = time2isoz(test_t)
68        expected = "1994-02-03 00:00:00Z"
69        self.assertEquals(result, expected,
70                          "%s  =>  '%s' (%s)" % (test_t, result, expected))
71
72        for s in tests:
73            t = http2time(s)
74            t2 = http2time(s.lower())
75            t3 = http2time(s.upper())
76
77            self.assert_(t == t2 == t3 == test_t,
78                         "'%s'  =>  %s, %s, %s (%s)" % (s, t, t2, t3, test_t))
79
80    def test_http2time_garbage(self):
81        from cookielib import http2time
82
83        for test in [
84            '',
85            'Garbage',
86            'Mandag 16. September 1996',
87            '01-00-1980',
88            '01-13-1980',
89            '00-01-1980',
90            '32-01-1980',
91            '01-01-1980 25:00:00',
92            '01-01-1980 00:61:00',
93            '01-01-1980 00:00:62',
94            ]:
95            self.assert_(http2time(test) is None,
96                         "http2time(%s) is not None\n"
97                         "http2time(test) %s" % (test, http2time(test))
98                         )
99
100
101class HeaderTests(TestCase):
102    def test_parse_ns_headers(self):
103        from cookielib import parse_ns_headers
104
105        # quotes should be stripped
106        expected = [[('foo', 'bar'), ('expires', 2209069412L), ('version', '0')]]
107        for hdr in [
108            'foo=bar; expires=01 Jan 2040 22:23:32 GMT',
109            'foo=bar; expires="01 Jan 2040 22:23:32 GMT"',
110            ]:
111            self.assertEquals(parse_ns_headers([hdr]), expected)
112
113    def test_parse_ns_headers_special_names(self):
114        # names such as 'expires' are not special in first name=value pair
115        # of Set-Cookie: header
116        from cookielib import parse_ns_headers
117
118        # Cookie with name 'expires'
119        hdr = 'expires=01 Jan 2040 22:23:32 GMT'
120        expected = [[("expires", "01 Jan 2040 22:23:32 GMT"), ("version", "0")]]
121        self.assertEquals(parse_ns_headers([hdr]), expected)
122
123    def test_join_header_words(self):
124        from cookielib import join_header_words
125
126        joined = join_header_words([[("foo", None), ("bar", "baz")]])
127        self.assertEquals(joined, "foo; bar=baz")
128
129        self.assertEquals(join_header_words([[]]), "")
130
131    def test_split_header_words(self):
132        from cookielib import split_header_words
133
134        tests = [
135            ("foo", [[("foo", None)]]),
136            ("foo=bar", [[("foo", "bar")]]),
137            ("   foo   ", [[("foo", None)]]),
138            ("   foo=   ", [[("foo", "")]]),
139            ("   foo=", [[("foo", "")]]),
140            ("   foo=   ; ", [[("foo", "")]]),
141            ("   foo=   ; bar= baz ", [[("foo", ""), ("bar", "baz")]]),
142            ("foo=bar bar=baz", [[("foo", "bar"), ("bar", "baz")]]),
143            # doesn't really matter if this next fails, but it works ATM
144            ("foo= bar=baz", [[("foo", "bar=baz")]]),
145            ("foo=bar;bar=baz", [[("foo", "bar"), ("bar", "baz")]]),
146            ('foo bar baz', [[("foo", None), ("bar", None), ("baz", None)]]),
147            ("a, b, c", [[("a", None)], [("b", None)], [("c", None)]]),
148            (r'foo; bar=baz, spam=, foo="\,\;\"", bar= ',
149             [[("foo", None), ("bar", "baz")],
150              [("spam", "")], [("foo", ',;"')], [("bar", "")]]),
151            ]
152
153        for arg, expect in tests:
154            try:
155                result = split_header_words([arg])
156            except:
157                import traceback, StringIO
158                f = StringIO.StringIO()
159                traceback.print_exc(None, f)
160                result = "(error -- traceback follows)\n\n%s" % f.getvalue()
161            self.assertEquals(result,  expect, """
162When parsing: '%s'
163Expected:     '%s'
164Got:          '%s'
165""" % (arg, expect, result))
166
167    def test_roundtrip(self):
168        from cookielib import split_header_words, join_header_words
169
170        tests = [
171            ("foo", "foo"),
172            ("foo=bar", "foo=bar"),
173            ("   foo   ", "foo"),
174            ("foo=", 'foo=""'),
175            ("foo=bar bar=baz", "foo=bar; bar=baz"),
176            ("foo=bar;bar=baz", "foo=bar; bar=baz"),
177            ('foo bar baz', "foo; bar; baz"),
178            (r'foo="\"" bar="\\"', r'foo="\""; bar="\\"'),
179            ('foo,,,bar', 'foo, bar'),
180            ('foo=bar,bar=baz', 'foo=bar, bar=baz'),
181
182            ('text/html; charset=iso-8859-1',
183             'text/html; charset="iso-8859-1"'),
184
185            ('foo="bar"; port="80,81"; discard, bar=baz',
186             'foo=bar; port="80,81"; discard, bar=baz'),
187
188            (r'Basic realm="\"foo\\\\bar\""',
189             r'Basic; realm="\"foo\\\\bar\""')
190            ]
191
192        for arg, expect in tests:
193            input = split_header_words([arg])
194            res = join_header_words(input)
195            self.assertEquals(res, expect, """
196When parsing: '%s'
197Expected:     '%s'
198Got:          '%s'
199Input was:    '%s'
200""" % (arg, expect, res, input))
201
202
203class FakeResponse:
204    def __init__(self, headers=[], url=None):
205        """
206        headers: list of RFC822-style 'Key: value' strings
207        """
208        import mimetools, StringIO
209        f = StringIO.StringIO("\n".join(headers))
210        self._headers = mimetools.Message(f)
211        self._url = url
212    def info(self): return self._headers
213
214def interact_2965(cookiejar, url, *set_cookie_hdrs):
215    return _interact(cookiejar, url, set_cookie_hdrs, "Set-Cookie2")
216
217def interact_netscape(cookiejar, url, *set_cookie_hdrs):
218    return _interact(cookiejar, url, set_cookie_hdrs, "Set-Cookie")
219
220def _interact(cookiejar, url, set_cookie_hdrs, hdr_name):
221    """Perform a single request / response cycle, returning Cookie: header."""
222    from urllib2 import Request
223    req = Request(url)
224    cookiejar.add_cookie_header(req)
225    cookie_hdr = req.get_header("Cookie", "")
226    headers = []
227    for hdr in set_cookie_hdrs:
228        headers.append("%s: %s" % (hdr_name, hdr))
229    res = FakeResponse(headers, url)
230    cookiejar.extract_cookies(res, req)
231    return cookie_hdr
232
233
234class FileCookieJarTests(TestCase):
235    def test_lwp_valueless_cookie(self):
236        # cookies with no value should be saved and loaded consistently
237        from cookielib import LWPCookieJar
238        filename = test_support.TESTFN
239        c = LWPCookieJar()
240        interact_netscape(c, "http://www.acme.com/", 'boo')
241        self.assertEqual(c._cookies["www.acme.com"]["/"]["boo"].value, None)
242        try:
243            c.save(filename, ignore_discard=True)
244            c = LWPCookieJar()
245            c.load(filename, ignore_discard=True)
246        finally:
247            try: os.unlink(filename)
248            except OSError: pass
249        self.assertEqual(c._cookies["www.acme.com"]["/"]["boo"].value, None)
250
251    def test_bad_magic(self):
252        from cookielib import LWPCookieJar, MozillaCookieJar, LoadError
253        # IOErrors (eg. file doesn't exist) are allowed to propagate
254        filename = test_support.TESTFN
255        for cookiejar_class in LWPCookieJar, MozillaCookieJar:
256            c = cookiejar_class()
257            try:
258                c.load(filename="for this test to work, a file with this "
259                                "filename should not exist")
260            except IOError, exc:
261                # exactly IOError, not LoadError
262                self.assertEqual(exc.__class__, IOError)
263            else:
264                self.fail("expected IOError for invalid filename")
265        # Invalid contents of cookies file (eg. bad magic string)
266        # causes a LoadError.
267        try:
268            f = open(filename, "w")
269            f.write("oops\n")
270            for cookiejar_class in LWPCookieJar, MozillaCookieJar:
271                c = cookiejar_class()
272                self.assertRaises(LoadError, c.load, filename)
273        finally:
274            try: os.unlink(filename)
275            except OSError: pass
276
277class CookieTests(TestCase):
278    # XXX
279    # Get rid of string comparisons where not actually testing str / repr.
280    # .clear() etc.
281    # IP addresses like 50 (single number, no dot) and domain-matching
282    #  functions (and is_HDN)?  See draft RFC 2965 errata.
283    # Strictness switches
284    # is_third_party()
285    # unverifiability / third-party blocking
286    # Netscape cookies work the same as RFC 2965 with regard to port.
287    # Set-Cookie with negative max age.
288    # If turn RFC 2965 handling off, Set-Cookie2 cookies should not clobber
289    #  Set-Cookie cookies.
290    # Cookie2 should be sent if *any* cookies are not V1 (ie. V0 OR V2 etc.).
291    # Cookies (V1 and V0) with no expiry date should be set to be discarded.
292    # RFC 2965 Quoting:
293    #  Should accept unquoted cookie-attribute values?  check errata draft.
294    #   Which are required on the way in and out?
295    #  Should always return quoted cookie-attribute values?
296    # Proper testing of when RFC 2965 clobbers Netscape (waiting for errata).
297    # Path-match on return (same for V0 and V1).
298    # RFC 2965 acceptance and returning rules
299    #  Set-Cookie2 without version attribute is rejected.
300
301    # Netscape peculiarities list from Ronald Tschalar.
302    # The first two still need tests, the rest are covered.
303## - Quoting: only quotes around the expires value are recognized as such
304##   (and yes, some folks quote the expires value); quotes around any other
305##   value are treated as part of the value.
306## - White space: white space around names and values is ignored
307## - Default path: if no path parameter is given, the path defaults to the
308##   path in the request-uri up to, but not including, the last '/'. Note
309##   that this is entirely different from what the spec says.
310## - Commas and other delimiters: Netscape just parses until the next ';'.
311##   This means it will allow commas etc inside values (and yes, both
312##   commas and equals are commonly appear in the cookie value). This also
313##   means that if you fold multiple Set-Cookie header fields into one,
314##   comma-separated list, it'll be a headache to parse (at least my head
315##   starts hurting everytime I think of that code).
316## - Expires: You'll get all sorts of date formats in the expires,
317##   including emtpy expires attributes ("expires="). Be as flexible as you
318##   can, and certainly don't expect the weekday to be there; if you can't
319##   parse it, just ignore it and pretend it's a session cookie.
320## - Domain-matching: Netscape uses the 2-dot rule for _all_ domains, not
321##   just the 7 special TLD's listed in their spec. And folks rely on
322##   that...
323
324    def test_domain_return_ok(self):
325        # test optimization: .domain_return_ok() should filter out most
326        # domains in the CookieJar before we try to access them (because that
327        # may require disk access -- in particular, with MSIECookieJar)
328        # This is only a rough check for performance reasons, so it's not too
329        # critical as long as it's sufficiently liberal.
330        import cookielib, urllib2
331        pol = cookielib.DefaultCookiePolicy()
332        for url, domain, ok in [
333            ("http://foo.bar.com/", "blah.com", False),
334            ("http://foo.bar.com/", "rhubarb.blah.com", False),
335            ("http://foo.bar.com/", "rhubarb.foo.bar.com", False),
336            ("http://foo.bar.com/", ".foo.bar.com", True),
337            ("http://foo.bar.com/", "foo.bar.com", True),
338            ("http://foo.bar.com/", ".bar.com", True),
339            ("http://foo.bar.com/", "com", True),
340            ("http://foo.com/", "rhubarb.foo.com", False),
341            ("http://foo.com/", ".foo.com", True),
342            ("http://foo.com/", "foo.com", True),
343            ("http://foo.com/", "com", True),
344            ("http://foo/", "rhubarb.foo", False),
345            ("http://foo/", ".foo", True),
346            ("http://foo/", "foo", True),
347            ("http://foo/", "foo.local", True),
348            ("http://foo/", ".local", True),
349            ]:
350            request = urllib2.Request(url)
351            r = pol.domain_return_ok(domain, request)
352            if ok: self.assert_(r)
353            else: self.assert_(not r)
354
355    def test_missing_value(self):
356        from cookielib import MozillaCookieJar, lwp_cookie_str
357
358        # missing = sign in Cookie: header is regarded by Mozilla as a missing
359        # name, and by cookielib as a missing value
360        filename = test_support.TESTFN
361        c = MozillaCookieJar(filename)
362        interact_netscape(c, "http://www.acme.com/", 'eggs')
363        interact_netscape(c, "http://www.acme.com/", '"spam"; path=/foo/')
364        cookie = c._cookies["www.acme.com"]["/"]["eggs"]
365        self.assert_(cookie.value is None)
366        self.assertEquals(cookie.name, "eggs")
367        cookie = c._cookies["www.acme.com"]['/foo/']['"spam"']
368        self.assert_(cookie.value is None)
369        self.assertEquals(cookie.name, '"spam"')
370        self.assertEquals(lwp_cookie_str(cookie), (
371            r'"spam"; path="/foo/"; domain="www.acme.com"; '
372            'path_spec; discard; version=0'))
373        old_str = repr(c)
374        c.save(ignore_expires=True, ignore_discard=True)
375        try:
376            c = MozillaCookieJar(filename)
377            c.revert(ignore_expires=True, ignore_discard=True)
378        finally:
379            os.unlink(c.filename)
380        # cookies unchanged apart from lost info re. whether path was specified
381        self.assertEquals(
382            repr(c),
383            re.sub("path_specified=%s" % True, "path_specified=%s" % False,
384                   old_str)
385            )
386        self.assertEquals(interact_netscape(c, "http://www.acme.com/foo/"),
387                          '"spam"; eggs')
388
389    def test_ns_parser(self):
390        from cookielib import CookieJar, DEFAULT_HTTP_PORT
391
392        c = CookieJar()
393        interact_netscape(c, "http://www.acme.com/",
394                          'spam=eggs; DoMain=.acme.com; port; blArgh="feep"')
395        interact_netscape(c, "http://www.acme.com/", 'ni=ni; port=80,8080')
396        interact_netscape(c, "http://www.acme.com:80/", 'nini=ni')
397        interact_netscape(c, "http://www.acme.com:80/", 'foo=bar; expires=')
398        interact_netscape(c, "http://www.acme.com:80/", 'spam=eggs; '
399                          'expires="Foo Bar 25 33:22:11 3022"')
400
401        cookie = c._cookies[".acme.com"]["/"]["spam"]
402        self.assertEquals(cookie.domain, ".acme.com")
403        self.assert_(cookie.domain_specified)
404        self.assertEquals(cookie.port, DEFAULT_HTTP_PORT)
405        self.assert_(not cookie.port_specified)
406        # case is preserved
407        self.assert_(cookie.has_nonstandard_attr("blArgh") and
408                     not cookie.has_nonstandard_attr("blargh"))
409
410        cookie = c._cookies["www.acme.com"]["/"]["ni"]
411        self.assertEquals(cookie.domain, "www.acme.com")
412        self.assert_(not cookie.domain_specified)
413        self.assertEquals(cookie.port, "80,8080")
414        self.assert_(cookie.port_specified)
415
416        cookie = c._cookies["www.acme.com"]["/"]["nini"]
417        self.assert_(cookie.port is None)
418        self.assert_(not cookie.port_specified)
419
420        # invalid expires should not cause cookie to be dropped
421        foo = c._cookies["www.acme.com"]["/"]["foo"]
422        spam = c._cookies["www.acme.com"]["/"]["foo"]
423        self.assert_(foo.expires is None)
424        self.assert_(spam.expires is None)
425
426    def test_ns_parser_special_names(self):
427        # names such as 'expires' are not special in first name=value pair
428        # of Set-Cookie: header
429        from cookielib import CookieJar
430
431        c = CookieJar()
432        interact_netscape(c, "http://www.acme.com/", 'expires=eggs')
433        interact_netscape(c, "http://www.acme.com/", 'version=eggs; spam=eggs')
434
435        cookies = c._cookies["www.acme.com"]["/"]
436        self.assert_('expires' in cookies)
437        self.assert_('version' in cookies)
438
439    def test_expires(self):
440        from cookielib import time2netscape, CookieJar
441
442        # if expires is in future, keep cookie...
443        c = CookieJar()
444        future = time2netscape(time.time()+3600)
445        interact_netscape(c, "http://www.acme.com/", 'spam="bar"; expires=%s' %
446                          future)
447        self.assertEquals(len(c), 1)
448        now = time2netscape(time.time()-1)
449        # ... and if in past or present, discard it
450        interact_netscape(c, "http://www.acme.com/", 'foo="eggs"; expires=%s' %
451                          now)
452        h = interact_netscape(c, "http://www.acme.com/")
453        self.assertEquals(len(c), 1)
454        self.assert_('spam="bar"' in h and "foo" not in h)
455
456        # max-age takes precedence over expires, and zero max-age is request to
457        # delete both new cookie and any old matching cookie
458        interact_netscape(c, "http://www.acme.com/", 'eggs="bar"; expires=%s' %
459                          future)
460        interact_netscape(c, "http://www.acme.com/", 'bar="bar"; expires=%s' %
461                          future)
462        self.assertEquals(len(c), 3)
463        interact_netscape(c, "http://www.acme.com/", 'eggs="bar"; '
464                          'expires=%s; max-age=0' % future)
465        interact_netscape(c, "http://www.acme.com/", 'bar="bar"; '
466                          'max-age=0; expires=%s' % future)
467        h = interact_netscape(c, "http://www.acme.com/")
468        self.assertEquals(len(c), 1)
469
470        # test expiry at end of session for cookies with no expires attribute
471        interact_netscape(c, "http://www.rhubarb.net/", 'whum="fizz"')
472        self.assertEquals(len(c), 2)
473        c.clear_session_cookies()
474        self.assertEquals(len(c), 1)
475        self.assert_('spam="bar"' in h)
476
477        # XXX RFC 2965 expiry rules (some apply to V0 too)
478
479    def test_default_path(self):
480        from cookielib import CookieJar, DefaultCookiePolicy
481
482        # RFC 2965
483        pol = DefaultCookiePolicy(rfc2965=True)
484
485        c = CookieJar(pol)
486        interact_2965(c, "http://www.acme.com/", 'spam="bar"; Version="1"')
487        self.assert_("/" in c._cookies["www.acme.com"])
488
489        c = CookieJar(pol)
490        interact_2965(c, "http://www.acme.com/blah", 'eggs="bar"; Version="1"')
491        self.assert_("/" in c._cookies["www.acme.com"])
492
493        c = CookieJar(pol)
494        interact_2965(c, "http://www.acme.com/blah/rhubarb",
495                      'eggs="bar"; Version="1"')
496        self.assert_("/blah/" in c._cookies["www.acme.com"])
497
498        c = CookieJar(pol)
499        interact_2965(c, "http://www.acme.com/blah/rhubarb/",
500                      'eggs="bar"; Version="1"')
501        self.assert_("/blah/rhubarb/" in c._cookies["www.acme.com"])
502
503        # Netscape
504
505        c = CookieJar()
506        interact_netscape(c, "http://www.acme.com/", 'spam="bar"')
507        self.assert_("/" in c._cookies["www.acme.com"])
508
509        c = CookieJar()
510        interact_netscape(c, "http://www.acme.com/blah", 'eggs="bar"')
511        self.assert_("/" in c._cookies["www.acme.com"])
512
513        c = CookieJar()
514        interact_netscape(c, "http://www.acme.com/blah/rhubarb", 'eggs="bar"')
515        self.assert_("/blah" in c._cookies["www.acme.com"])
516
517        c = CookieJar()
518        interact_netscape(c, "http://www.acme.com/blah/rhubarb/", 'eggs="bar"')
519        self.assert_("/blah/rhubarb" in c._cookies["www.acme.com"])
520
521    def test_escape_path(self):
522        from cookielib import escape_path
523        cases = [
524            # quoted safe
525            ("/foo%2f/bar", "/foo%2F/bar"),
526            ("/foo%2F/bar", "/foo%2F/bar"),
527            # quoted %
528            ("/foo%%/bar", "/foo%%/bar"),
529            # quoted unsafe
530            ("/fo%19o/bar", "/fo%19o/bar"),
531            ("/fo%7do/bar", "/fo%7Do/bar"),
532            # unquoted safe
533            ("/foo/bar&", "/foo/bar&"),
534            ("/foo//bar", "/foo//bar"),
535            ("\176/foo/bar", "\176/foo/bar"),
536            # unquoted unsafe
537            ("/foo\031/bar", "/foo%19/bar"),
538            ("/\175foo/bar", "/%7Dfoo/bar"),
539            # unicode
540            (u"/foo/bar\uabcd", "/foo/bar%EA%AF%8D"),  # UTF-8 encoded
541            ]
542        for arg, result in cases:
543            self.assertEquals(escape_path(arg), result)
544
545    def test_request_path(self):
546        from urllib2 import Request
547        from cookielib import request_path
548        # with parameters
549        req = Request("http://www.example.com/rheum/rhaponicum;"
550                      "foo=bar;sing=song?apples=pears&spam=eggs#ni")
551        self.assertEquals(request_path(req), "/rheum/rhaponicum;"
552                     "foo=bar;sing=song?apples=pears&spam=eggs#ni")
553        # without parameters
554        req = Request("http://www.example.com/rheum/rhaponicum?"
555                      "apples=pears&spam=eggs#ni")
556        self.assertEquals(request_path(req), "/rheum/rhaponicum?"
557                     "apples=pears&spam=eggs#ni")
558        # missing final slash
559        req = Request("http://www.example.com")
560        self.assertEquals(request_path(req), "/")
561
562    def test_request_port(self):
563        from urllib2 import Request
564        from cookielib import request_port, DEFAULT_HTTP_PORT
565        req = Request("http://www.acme.com:1234/",
566                      headers={"Host": "www.acme.com:4321"})
567        self.assertEquals(request_port(req), "1234")
568        req = Request("http://www.acme.com/",
569                      headers={"Host": "www.acme.com:4321"})
570        self.assertEquals(request_port(req), DEFAULT_HTTP_PORT)
571
572    def test_request_host(self):
573        from urllib2 import Request
574        from cookielib import request_host
575        # this request is illegal (RFC2616, 14.2.3)
576        req = Request("http://1.1.1.1/",
577                      headers={"Host": "www.acme.com:80"})
578        # libwww-perl wants this response, but that seems wrong (RFC 2616,
579        # section 5.2, point 1., and RFC 2965 section 1, paragraph 3)
580        #self.assertEquals(request_host(req), "www.acme.com")
581        self.assertEquals(request_host(req), "1.1.1.1")
582        req = Request("http://www.acme.com/",
583                      headers={"Host": "irrelevant.com"})
584        self.assertEquals(request_host(req), "www.acme.com")
585        # not actually sure this one is valid Request object, so maybe should
586        # remove test for no host in url in request_host function?
587        req = Request("/resource.html",
588                      headers={"Host": "www.acme.com"})
589        self.assertEquals(request_host(req), "www.acme.com")
590        # port shouldn't be in request-host
591        req = Request("http://www.acme.com:2345/resource.html",
592                      headers={"Host": "www.acme.com:5432"})
593        self.assertEquals(request_host(req), "www.acme.com")
594
595    def test_is_HDN(self):
596        from cookielib import is_HDN
597        self.assert_(is_HDN("foo.bar.com"))
598        self.assert_(is_HDN("1foo2.3bar4.5com"))
599        self.assert_(not is_HDN("192.168.1.1"))
600        self.assert_(not is_HDN(""))
601        self.assert_(not is_HDN("."))
602        self.assert_(not is_HDN(".foo.bar.com"))
603        self.assert_(not is_HDN("..foo"))
604        self.assert_(not is_HDN("foo."))
605
606    def test_reach(self):
607        from cookielib import reach
608        self.assertEquals(reach("www.acme.com"), ".acme.com")
609        self.assertEquals(reach("acme.com"), "acme.com")
610        self.assertEquals(reach("acme.local"), ".local")
611        self.assertEquals(reach(".local"), ".local")
612        self.assertEquals(reach(".com"), ".com")
613        self.assertEquals(reach("."), ".")
614        self.assertEquals(reach(""), "")
615        self.assertEquals(reach("192.168.0.1"), "192.168.0.1")
616
617    def test_domain_match(self):
618        from cookielib import domain_match, user_domain_match
619        self.assert_(domain_match("192.168.1.1", "192.168.1.1"))
620        self.assert_(not domain_match("192.168.1.1", ".168.1.1"))
621        self.assert_(domain_match("x.y.com", "x.Y.com"))
622        self.assert_(domain_match("x.y.com", ".Y.com"))
623        self.assert_(not domain_match("x.y.com", "Y.com"))
624        self.assert_(domain_match("a.b.c.com", ".c.com"))
625        self.assert_(not domain_match(".c.com", "a.b.c.com"))
626        self.assert_(domain_match("example.local", ".local"))
627        self.assert_(not domain_match("blah.blah", ""))
628        self.assert_(not domain_match("", ".rhubarb.rhubarb"))
629        self.assert_(domain_match("", ""))
630
631        self.assert_(user_domain_match("acme.com", "acme.com"))
632        self.assert_(not user_domain_match("acme.com", ".acme.com"))
633        self.assert_(user_domain_match("rhubarb.acme.com", ".acme.com"))
634        self.assert_(user_domain_match("www.rhubarb.acme.com", ".acme.com"))
635        self.assert_(user_domain_match("x.y.com", "x.Y.com"))
636        self.assert_(user_domain_match("x.y.com", ".Y.com"))
637        self.assert_(not user_domain_match("x.y.com", "Y.com"))
638        self.assert_(user_domain_match("y.com", "Y.com"))
639        self.assert_(not user_domain_match(".y.com", "Y.com"))
640        self.assert_(user_domain_match(".y.com", ".Y.com"))
641        self.assert_(user_domain_match("x.y.com", ".com"))
642        self.assert_(not user_domain_match("x.y.com", "com"))
643        self.assert_(not user_domain_match("x.y.com", "m"))
644        self.assert_(not user_domain_match("x.y.com", ".m"))
645        self.assert_(not user_domain_match("x.y.com", ""))
646        self.assert_(not user_domain_match("x.y.com", "."))
647        self.assert_(user_domain_match("192.168.1.1", "192.168.1.1"))
648        # not both HDNs, so must string-compare equal to match
649        self.assert_(not user_domain_match("192.168.1.1", ".168.1.1"))
650        self.assert_(not user_domain_match("192.168.1.1", "."))
651        # empty string is a special case
652        self.assert_(not user_domain_match("192.168.1.1", ""))
653
654    def test_wrong_domain(self):
655        # Cookies whose effective request-host name does not domain-match the
656        # domain are rejected.
657
658        # XXX far from complete
659        from cookielib import CookieJar
660        c = CookieJar()
661        interact_2965(c, "http://www.nasty.com/",
662                      'foo=bar; domain=friendly.org; Version="1"')
663        self.assertEquals(len(c), 0)
664
665    def test_two_component_domain_ns(self):
666        # Netscape: .www.bar.com, www.bar.com, .bar.com, bar.com, no domain
667        # should all get accepted, as should .acme.com, acme.com and no domain
668        # for 2-component domains like acme.com.
669        from cookielib import CookieJar, DefaultCookiePolicy
670
671        c = CookieJar()
672
673        # two-component V0 domain is OK
674        interact_netscape(c, "http://foo.net/", 'ns=bar')
675        self.assertEquals(len(c), 1)
676        self.assertEquals(c._cookies["foo.net"]["/"]["ns"].value, "bar")
677        self.assertEquals(interact_netscape(c, "http://foo.net/"), "ns=bar")
678        # *will* be returned to any other domain (unlike RFC 2965)...
679        self.assertEquals(interact_netscape(c, "http://www.foo.net/"),
680                          "ns=bar")
681        # ...unless requested otherwise
682        pol = DefaultCookiePolicy(
683            strict_ns_domain=DefaultCookiePolicy.DomainStrictNonDomain)
684        c.set_policy(pol)
685        self.assertEquals(interact_netscape(c, "http://www.foo.net/"), "")
686
687        # unlike RFC 2965, even explicit two-component domain is OK,
688        # because .foo.net matches foo.net
689        interact_netscape(c, "http://foo.net/foo/",
690                          'spam1=eggs; domain=foo.net')
691        # even if starts with a dot -- in NS rules, .foo.net matches foo.net!
692        interact_netscape(c, "http://foo.net/foo/bar/",
693                          'spam2=eggs; domain=.foo.net')
694        self.assertEquals(len(c), 3)
695        self.assertEquals(c._cookies[".foo.net"]["/foo"]["spam1"].value,
696                          "eggs")
697        self.assertEquals(c._cookies[".foo.net"]["/foo/bar"]["spam2"].value,
698                          "eggs")
699        self.assertEquals(interact_netscape(c, "http://foo.net/foo/bar/"),
700                          "spam2=eggs; spam1=eggs; ns=bar")
701
702        # top-level domain is too general
703        interact_netscape(c, "http://foo.net/", 'nini="ni"; domain=.net')
704        self.assertEquals(len(c), 3)
705
706##         # Netscape protocol doesn't allow non-special top level domains (such
707##         # as co.uk) in the domain attribute unless there are at least three
708##         # dots in it.
709        # Oh yes it does!  Real implementations don't check this, and real
710        # cookies (of course) rely on that behaviour.
711        interact_netscape(c, "http://foo.co.uk", 'nasty=trick; domain=.co.uk')
712##         self.assertEquals(len(c), 2)
713        self.assertEquals(len(c), 4)
714
715    def test_two_component_domain_rfc2965(self):
716        from cookielib import CookieJar, DefaultCookiePolicy
717
718        pol = DefaultCookiePolicy(rfc2965=True)
719        c = CookieJar(pol)
720
721        # two-component V1 domain is OK
722        interact_2965(c, "http://foo.net/", 'foo=bar; Version="1"')
723        self.assertEquals(len(c), 1)
724        self.assertEquals(c._cookies["foo.net"]["/"]["foo"].value, "bar")
725        self.assertEquals(interact_2965(c, "http://foo.net/"),
726                          "$Version=1; foo=bar")
727        # won't be returned to any other domain (because domain was implied)
728        self.assertEquals(interact_2965(c, "http://www.foo.net/"), "")
729
730        # unless domain is given explicitly, because then it must be
731        # rewritten to start with a dot: foo.net --> .foo.net, which does
732        # not domain-match foo.net
733        interact_2965(c, "http://foo.net/foo",
734                      'spam=eggs; domain=foo.net; path=/foo; Version="1"')
735        self.assertEquals(len(c), 1)
736        self.assertEquals(interact_2965(c, "http://foo.net/foo"),
737                          "$Version=1; foo=bar")
738
739        # explicit foo.net from three-component domain www.foo.net *does* get
740        # set, because .foo.net domain-matches .foo.net
741        interact_2965(c, "http://www.foo.net/foo/",
742                      'spam=eggs; domain=foo.net; Version="1"')
743        self.assertEquals(c._cookies[".foo.net"]["/foo/"]["spam"].value,
744                          "eggs")
745        self.assertEquals(len(c), 2)
746        self.assertEquals(interact_2965(c, "http://foo.net/foo/"),
747                          "$Version=1; foo=bar")
748        self.assertEquals(interact_2965(c, "http://www.foo.net/foo/"),
749                          '$Version=1; spam=eggs; $Domain="foo.net"')
750
751        # top-level domain is too general
752        interact_2965(c, "http://foo.net/",
753                      'ni="ni"; domain=".net"; Version="1"')
754        self.assertEquals(len(c), 2)
755
756        # RFC 2965 doesn't require blocking this
757        interact_2965(c, "http://foo.co.uk/",
758                      'nasty=trick; domain=.co.uk; Version="1"')
759        self.assertEquals(len(c), 3)
760
761    def test_domain_allow(self):
762        from cookielib import CookieJar, DefaultCookiePolicy
763        from urllib2 import Request
764
765        c = CookieJar(policy=DefaultCookiePolicy(
766            blocked_domains=["acme.com"],
767            allowed_domains=["www.acme.com"]))
768
769        req = Request("http://acme.com/")
770        headers = ["Set-Cookie: CUSTOMER=WILE_E_COYOTE; path=/"]
771        res = FakeResponse(headers, "http://acme.com/")
772        c.extract_cookies(res, req)
773        self.assertEquals(len(c), 0)
774
775        req = Request("http://www.acme.com/")
776        res = FakeResponse(headers, "http://www.acme.com/")
777        c.extract_cookies(res, req)
778        self.assertEquals(len(c), 1)
779
780        req = Request("http://www.coyote.com/")
781        res = FakeResponse(headers, "http://www.coyote.com/")
782        c.extract_cookies(res, req)
783        self.assertEquals(len(c), 1)
784
785        # set a cookie with non-allowed domain...
786        req = Request("http://www.coyote.com/")
787        res = FakeResponse(headers, "http://www.coyote.com/")
788        cookies = c.make_cookies(res, req)
789        c.set_cookie(cookies[0])
790        self.assertEquals(len(c), 2)
791        # ... and check is doesn't get returned
792        c.add_cookie_header(req)
793        self.assert_(not req.has_header("Cookie"))
794
795    def test_domain_block(self):
796        from cookielib import CookieJar, DefaultCookiePolicy
797        from urllib2 import Request
798
799        pol = DefaultCookiePolicy(
800            rfc2965=True, blocked_domains=[".acme.com"])
801        c = CookieJar(policy=pol)
802        headers = ["Set-Cookie: CUSTOMER=WILE_E_COYOTE; path=/"]
803
804        req = Request("http://www.acme.com/")
805        res = FakeResponse(headers, "http://www.acme.com/")
806        c.extract_cookies(res, req)
807        self.assertEquals(len(c), 0)
808
809        p = pol.set_blocked_domains(["acme.com"])
810        c.extract_cookies(res, req)
811        self.assertEquals(len(c), 1)
812
813        c.clear()
814        req = Request("http://www.roadrunner.net/")
815        res = FakeResponse(headers, "http://www.roadrunner.net/")
816        c.extract_cookies(res, req)
817        self.assertEquals(len(c), 1)
818        req = Request("http://www.roadrunner.net/")
819        c.add_cookie_header(req)
820        self.assert_((req.has_header("Cookie") and
821                      req.has_header("Cookie2")))
822
823        c.clear()
824        pol.set_blocked_domains([".acme.com"])
825        c.extract_cookies(res, req)
826        self.assertEquals(len(c), 1)
827
828        # set a cookie with blocked domain...
829        req = Request("http://www.acme.com/")
830        res = FakeResponse(headers, "http://www.acme.com/")
831        cookies = c.make_cookies(res, req)
832        c.set_cookie(cookies[0])
833        self.assertEquals(len(c), 2)
834        # ... and check is doesn't get returned
835        c.add_cookie_header(req)
836        self.assert_(not req.has_header("Cookie"))
837
838    def test_secure(self):
839        from cookielib import CookieJar, DefaultCookiePolicy
840
841        for ns in True, False:
842            for whitespace in " ", "":
843                c = CookieJar()
844                if ns:
845                    pol = DefaultCookiePolicy(rfc2965=False)
846                    int = interact_netscape
847                    vs = ""
848                else:
849                    pol = DefaultCookiePolicy(rfc2965=True)
850                    int = interact_2965
851                    vs = "; Version=1"
852                c.set_policy(pol)
853                url = "http://www.acme.com/"
854                int(c, url, "foo1=bar%s%s" % (vs, whitespace))
855                int(c, url, "foo2=bar%s; secure%s" %  (vs, whitespace))
856                self.assert_(
857                    not c._cookies["www.acme.com"]["/"]["foo1"].secure,
858                    "non-secure cookie registered secure")
859                self.assert_(
860                    c._cookies["www.acme.com"]["/"]["foo2"].secure,
861                    "secure cookie registered non-secure")
862
863    def test_quote_cookie_value(self):
864        from cookielib import CookieJar, DefaultCookiePolicy
865        c = CookieJar(policy=DefaultCookiePolicy(rfc2965=True))
866        interact_2965(c, "http://www.acme.com/", r'foo=\b"a"r; Version=1')
867        h = interact_2965(c, "http://www.acme.com/")
868        self.assertEquals(h, r'$Version=1; foo=\\b\"a\"r')
869
870    def test_missing_final_slash(self):
871        # Missing slash from request URL's abs_path should be assumed present.
872        from cookielib import CookieJar, DefaultCookiePolicy
873        from urllib2 import Request
874        url = "http://www.acme.com"
875        c = CookieJar(DefaultCookiePolicy(rfc2965=True))
876        interact_2965(c, url, "foo=bar; Version=1")
877        req = Request(url)
878        self.assertEquals(len(c), 1)
879        c.add_cookie_header(req)
880        self.assert_(req.has_header("Cookie"))
881
882    def test_domain_mirror(self):
883        from cookielib import CookieJar, DefaultCookiePolicy
884
885        pol = DefaultCookiePolicy(rfc2965=True)
886
887        c = CookieJar(pol)
888        url = "http://foo.bar.com/"
889        interact_2965(c, url, "spam=eggs; Version=1")
890        h = interact_2965(c, url)
891        self.assert_("Domain" not in h,
892                     "absent domain returned with domain present")
893
894        c = CookieJar(pol)
895        url = "http://foo.bar.com/"
896        interact_2965(c, url, 'spam=eggs; Version=1; Domain=.bar.com')
897        h = interact_2965(c, url)
898        self.assert_('$Domain=".bar.com"' in h, "domain not returned")
899
900        c = CookieJar(pol)
901        url = "http://foo.bar.com/"
902        # note missing initial dot in Domain
903        interact_2965(c, url, 'spam=eggs; Version=1; Domain=bar.com')
904        h = interact_2965(c, url)
905        self.assert_('$Domain="bar.com"' in h, "domain not returned")
906
907    def test_path_mirror(self):
908        from cookielib import CookieJar, DefaultCookiePolicy
909
910        pol = DefaultCookiePolicy(rfc2965=True)
911
912        c = CookieJar(pol)
913        url = "http://foo.bar.com/"
914        interact_2965(c, url, "spam=eggs; Version=1")
915        h = interact_2965(c, url)
916        self.assert_("Path" not in h,
917                     "absent path returned with path present")
918
919        c = CookieJar(pol)
920        url = "http://foo.bar.com/"
921        interact_2965(c, url, 'spam=eggs; Version=1; Path=/')
922        h = interact_2965(c, url)
923        self.assert_('$Path="/"' in h, "path not returned")
924
925    def test_port_mirror(self):
926        from cookielib import CookieJar, DefaultCookiePolicy
927
928        pol = DefaultCookiePolicy(rfc2965=True)
929
930        c = CookieJar(pol)
931        url = "http://foo.bar.com/"
932        interact_2965(c, url, "spam=eggs; Version=1")
933        h = interact_2965(c, url)
934        self.assert_("Port" not in h,
935                     "absent port returned with port present")
936
937        c = CookieJar(pol)
938        url = "http://foo.bar.com/"
939        interact_2965(c, url, "spam=eggs; Version=1; Port")
940        h = interact_2965(c, url)
941        self.assert_(re.search("\$Port([^=]|$)", h),
942                     "port with no value not returned with no value")
943
944        c = CookieJar(pol)
945        url = "http://foo.bar.com/"
946        interact_2965(c, url, 'spam=eggs; Version=1; Port="80"')
947        h = interact_2965(c, url)
948        self.assert_('$Port="80"' in h,
949                     "port with single value not returned with single value")
950
951        c = CookieJar(pol)
952        url = "http://foo.bar.com/"
953        interact_2965(c, url, 'spam=eggs; Version=1; Port="80,8080"')
954        h = interact_2965(c, url)
955        self.assert_('$Port="80,8080"' in h,
956                     "port with multiple values not returned with multiple "
957                     "values")
958
959    def test_no_return_comment(self):
960        from cookielib import CookieJar, DefaultCookiePolicy
961
962        c = CookieJar(DefaultCookiePolicy(rfc2965=True))
963        url = "http://foo.bar.com/"
964        interact_2965(c, url, 'spam=eggs; Version=1; '
965                      'Comment="does anybody read these?"; '
966                      'CommentURL="http://foo.bar.net/comment.html"')
967        h = interact_2965(c, url)
968        self.assert_(
969            "Comment" not in h,
970            "Comment or CommentURL cookie-attributes returned to server")
971
972    def test_Cookie_iterator(self):
973        from cookielib import CookieJar, Cookie, DefaultCookiePolicy
974
975        cs = CookieJar(DefaultCookiePolicy(rfc2965=True))
976        # add some random cookies
977        interact_2965(cs, "http://blah.spam.org/", 'foo=eggs; Version=1; '
978                      'Comment="does anybody read these?"; '
979                      'CommentURL="http://foo.bar.net/comment.html"')
980        interact_netscape(cs, "http://www.acme.com/blah/", "spam=bar; secure")
981        interact_2965(cs, "http://www.acme.com/blah/",
982                      "foo=bar; secure; Version=1")
983        interact_2965(cs, "http://www.acme.com/blah/",
984                      "foo=bar; path=/; Version=1")
985        interact_2965(cs, "http://www.sol.no",
986                      r'bang=wallop; version=1; domain=".sol.no"; '
987                      r'port="90,100, 80,8080"; '
988                      r'max-age=100; Comment = "Just kidding! (\"|\\\\) "')
989
990        versions = [1, 1, 1, 0, 1]
991        names = ["bang", "foo", "foo", "spam", "foo"]
992        domains = [".sol.no", "blah.spam.org", "www.acme.com",
993                   "www.acme.com", "www.acme.com"]
994        paths = ["/", "/", "/", "/blah", "/blah/"]
995
996        for i in range(4):
997            i = 0
998            for c in cs:
999                self.assert_(isinstance(c, Cookie))
1000                self.assertEquals(c.version, versions[i])
1001                self.assertEquals(c.name, names[i])
1002                self.assertEquals(c.domain, domains[i])
1003                self.assertEquals(c.path, paths[i])
1004                i = i + 1
1005
1006    def test_parse_ns_headers(self):
1007        from cookielib import parse_ns_headers
1008
1009        # missing domain value (invalid cookie)
1010        self.assertEquals(
1011            parse_ns_headers(["foo=bar; path=/; domain"]),
1012            [[("foo", "bar"),
1013              ("path", "/"), ("domain", None), ("version", "0")]]
1014            )
1015        # invalid expires value
1016        self.assertEquals(
1017            parse_ns_headers(["foo=bar; expires=Foo Bar 12 33:22:11 2000"]),
1018            [[("foo", "bar"), ("expires", None), ("version", "0")]]
1019            )
1020        # missing cookie value (valid cookie)
1021        self.assertEquals(
1022            parse_ns_headers(["foo"]),
1023            [[("foo", None), ("version", "0")]]
1024            )
1025        # shouldn't add version if header is empty
1026        self.assertEquals(parse_ns_headers([""]), [])
1027
1028    def test_bad_cookie_header(self):
1029
1030        def cookiejar_from_cookie_headers(headers):
1031            from cookielib import CookieJar
1032            from urllib2 import Request
1033            c = CookieJar()
1034            req = Request("http://www.example.com/")
1035            r = FakeResponse(headers, "http://www.example.com/")
1036            c.extract_cookies(r, req)
1037            return c
1038
1039        # none of these bad headers should cause an exception to be raised
1040        for headers in [
1041            ["Set-Cookie: "],  # actually, nothing wrong with this
1042            ["Set-Cookie2: "],  # ditto
1043            # missing domain value
1044            ["Set-Cookie2: a=foo; path=/; Version=1; domain"],
1045            # bad max-age
1046            ["Set-Cookie: b=foo; max-age=oops"],
1047            ]:
1048            c = cookiejar_from_cookie_headers(headers)
1049            # these bad cookies shouldn't be set
1050            self.assertEquals(len(c), 0)
1051
1052        # cookie with invalid expires is treated as session cookie
1053        headers = ["Set-Cookie: c=foo; expires=Foo Bar 12 33:22:11 2000"]
1054        c = cookiejar_from_cookie_headers(headers)
1055        cookie = c._cookies["www.example.com"]["/"]["c"]
1056        self.assert_(cookie.expires is None)
1057
1058
1059class LWPCookieTests(TestCase):
1060    # Tests taken from libwww-perl, with a few modifications and additions.
1061
1062    def test_netscape_example_1(self):
1063        from cookielib import CookieJar, DefaultCookiePolicy
1064        from urllib2 import Request
1065
1066        #-------------------------------------------------------------------
1067        # First we check that it works for the original example at
1068        # http://www.netscape.com/newsref/std/cookie_spec.html
1069
1070        # Client requests a document, and receives in the response:
1071        #
1072        #       Set-Cookie: CUSTOMER=WILE_E_COYOTE; path=/; expires=Wednesday, 09-Nov-99 23:12:40 GMT
1073        #
1074        # When client requests a URL in path "/" on this server, it sends:
1075        #
1076        #       Cookie: CUSTOMER=WILE_E_COYOTE
1077        #
1078        # Client requests a document, and receives in the response:
1079        #
1080        #       Set-Cookie: PART_NUMBER=ROCKET_LAUNCHER_0001; path=/
1081        #
1082        # When client requests a URL in path "/" on this server, it sends:
1083        #
1084        #       Cookie: CUSTOMER=WILE_E_COYOTE; PART_NUMBER=ROCKET_LAUNCHER_0001
1085        #
1086        # Client receives:
1087        #
1088        #       Set-Cookie: SHIPPING=FEDEX; path=/fo
1089        #
1090        # When client requests a URL in path "/" on this server, it sends:
1091        #
1092        #       Cookie: CUSTOMER=WILE_E_COYOTE; PART_NUMBER=ROCKET_LAUNCHER_0001
1093        #
1094        # When client requests a URL in path "/foo" on this server, it sends:
1095        #
1096        #       Cookie: CUSTOMER=WILE_E_COYOTE; PART_NUMBER=ROCKET_LAUNCHER_0001; SHIPPING=FEDEX
1097        #
1098        # The last Cookie is buggy, because both specifications say that the
1099        # most specific cookie must be sent first.  SHIPPING=FEDEX is the
1100        # most specific and should thus be first.
1101
1102        year_plus_one = time.localtime()[0] + 1
1103
1104        headers = []
1105
1106        c = CookieJar(DefaultCookiePolicy(rfc2965 = True))
1107
1108        #req = Request("http://1.1.1.1/",
1109        #              headers={"Host": "www.acme.com:80"})
1110        req = Request("http://www.acme.com:80/",
1111                      headers={"Host": "www.acme.com:80"})
1112
1113        headers.append(
1114            "Set-Cookie: CUSTOMER=WILE_E_COYOTE; path=/ ; "
1115            "expires=Wednesday, 09-Nov-%d 23:12:40 GMT" % year_plus_one)
1116        res = FakeResponse(headers, "http://www.acme.com/")
1117        c.extract_cookies(res, req)
1118
1119        req = Request("http://www.acme.com/")
1120        c.add_cookie_header(req)
1121
1122        self.assertEqual(req.get_header("Cookie"), "CUSTOMER=WILE_E_COYOTE")
1123        self.assertEqual(req.get_header("Cookie2"), '$Version="1"')
1124
1125        headers.append("Set-Cookie: PART_NUMBER=ROCKET_LAUNCHER_0001; path=/")
1126        res = FakeResponse(headers, "http://www.acme.com/")
1127        c.extract_cookies(res, req)
1128
1129        req = Request("http://www.acme.com/foo/bar")
1130        c.add_cookie_header(req)
1131
1132        h = req.get_header("Cookie")
1133        self.assert_("PART_NUMBER=ROCKET_LAUNCHER_0001" in h and
1134                     "CUSTOMER=WILE_E_COYOTE" in h)
1135
1136        headers.append('Set-Cookie: SHIPPING=FEDEX; path=/foo')
1137        res = FakeResponse(headers, "http://www.acme.com")
1138        c.extract_cookies(res, req)
1139
1140        req = Request("http://www.acme.com/")
1141        c.add_cookie_header(req)
1142
1143        h = req.get_header("Cookie")
1144        self.assert_("PART_NUMBER=ROCKET_LAUNCHER_0001" in h and
1145                     "CUSTOMER=WILE_E_COYOTE" in h and
1146                     "SHIPPING=FEDEX" not in h)
1147
1148        req = Request("http://www.acme.com/foo/")
1149        c.add_cookie_header(req)
1150
1151        h = req.get_header("Cookie")
1152        self.assert_(("PART_NUMBER=ROCKET_LAUNCHER_0001" in h and
1153                      "CUSTOMER=WILE_E_COYOTE" in h and
1154                      h.startswith("SHIPPING=FEDEX;")))
1155
1156    def test_netscape_example_2(self):
1157        from cookielib import CookieJar
1158        from urllib2 import Request
1159
1160        # Second Example transaction sequence:
1161        #
1162        # Assume all mappings from above have been cleared.
1163        #
1164        # Client receives:
1165        #
1166        #       Set-Cookie: PART_NUMBER=ROCKET_LAUNCHER_0001; path=/
1167        #
1168        # When client requests a URL in path "/" on this server, it sends:
1169        #
1170        #       Cookie: PART_NUMBER=ROCKET_LAUNCHER_0001
1171        #
1172        # Client receives:
1173        #
1174        #       Set-Cookie: PART_NUMBER=RIDING_ROCKET_0023; path=/ammo
1175        #
1176        # When client requests a URL in path "/ammo" on this server, it sends:
1177        #
1178        #       Cookie: PART_NUMBER=RIDING_ROCKET_0023; PART_NUMBER=ROCKET_LAUNCHER_0001
1179        #
1180        #       NOTE: There are two name/value pairs named "PART_NUMBER" due to
1181        #       the inheritance of the "/" mapping in addition to the "/ammo" mapping.
1182
1183        c = CookieJar()
1184        headers = []
1185
1186        req = Request("http://www.acme.com/")
1187        headers.append("Set-Cookie: PART_NUMBER=ROCKET_LAUNCHER_0001; path=/")
1188        res = FakeResponse(headers, "http://www.acme.com/")
1189
1190        c.extract_cookies(res, req)
1191
1192        req = Request("http://www.acme.com/")
1193        c.add_cookie_header(req)
1194
1195        self.assertEquals(req.get_header("Cookie"),
1196                          "PART_NUMBER=ROCKET_LAUNCHER_0001")
1197
1198        headers.append(
1199            "Set-Cookie: PART_NUMBER=RIDING_ROCKET_0023; path=/ammo")
1200        res = FakeResponse(headers, "http://www.acme.com/")
1201        c.extract_cookies(res, req)
1202
1203        req = Request("http://www.acme.com/ammo")
1204        c.add_cookie_header(req)
1205
1206        self.assert_(re.search(r"PART_NUMBER=RIDING_ROCKET_0023;\s*"
1207                               "PART_NUMBER=ROCKET_LAUNCHER_0001",
1208                               req.get_header("Cookie")))
1209
1210    def test_ietf_example_1(self):
1211        from cookielib import CookieJar, DefaultCookiePolicy
1212        #-------------------------------------------------------------------
1213        # Then we test with the examples from draft-ietf-http-state-man-mec-03.txt
1214        #
1215        # 5.  EXAMPLES
1216
1217        c = CookieJar(DefaultCookiePolicy(rfc2965=True))
1218
1219        #
1220        # 5.1  Example 1
1221        #
1222        # Most detail of request and response headers has been omitted.  Assume
1223        # the user agent has no stored cookies.
1224        #
1225        #   1.  User Agent -> Server
1226        #
1227        #       POST /acme/login HTTP/1.1
1228        #       [form data]
1229        #
1230        #       User identifies self via a form.
1231        #
1232        #   2.  Server -> User Agent
1233        #
1234        #       HTTP/1.1 200 OK
1235        #       Set-Cookie2: Customer="WILE_E_COYOTE"; Version="1"; Path="/acme"
1236        #
1237        #       Cookie reflects user's identity.
1238
1239        cookie = interact_2965(
1240            c, 'http://www.acme.com/acme/login',
1241            'Customer="WILE_E_COYOTE"; Version="1"; Path="/acme"')
1242        self.assert_(not cookie)
1243
1244        #
1245        #   3.  User Agent -> Server
1246        #
1247        #       POST /acme/pickitem HTTP/1.1
1248        #       Cookie: $Version="1"; Customer="WILE_E_COYOTE"; $Path="/acme"
1249        #       [form data]
1250        #
1251        #       User selects an item for ``shopping basket.''
1252        #
1253        #   4.  Server -> User Agent
1254        #
1255        #       HTTP/1.1 200 OK
1256        #       Set-Cookie2: Part_Number="Rocket_Launcher_0001"; Version="1";
1257        #               Path="/acme"
1258        #
1259        #       Shopping basket contains an item.
1260
1261        cookie = interact_2965(c, 'http://www.acme.com/acme/pickitem',
1262                               'Part_Number="Rocket_Launcher_0001"; '
1263                               'Version="1"; Path="/acme"');
1264        self.assert_(re.search(
1265            r'^\$Version="?1"?; Customer="?WILE_E_COYOTE"?; \$Path="/acme"$',
1266            cookie))
1267
1268        #
1269        #   5.  User Agent -> Server
1270        #
1271        #       POST /acme/shipping HTTP/1.1
1272        #       Cookie: $Version="1";
1273        #               Customer="WILE_E_COYOTE"; $Path="/acme";
1274        #               Part_Number="Rocket_Launcher_0001"; $Path="/acme"
1275        #       [form data]
1276        #
1277        #       User selects shipping method from form.
1278        #
1279        #   6.  Server -> User Agent
1280        #
1281        #       HTTP/1.1 200 OK
1282        #       Set-Cookie2: Shipping="FedEx"; Version="1"; Path="/acme"
1283        #
1284        #       New cookie reflects shipping method.
1285
1286        cookie = interact_2965(c, "http://www.acme.com/acme/shipping",
1287                               'Shipping="FedEx"; Version="1"; Path="/acme"')
1288
1289        self.assert_(re.search(r'^\$Version="?1"?;', cookie))
1290        self.assert_(re.search(r'Part_Number="?Rocket_Launcher_0001"?;'
1291                               '\s*\$Path="\/acme"', cookie))
1292        self.assert_(re.search(r'Customer="?WILE_E_COYOTE"?;\s*\$Path="\/acme"',
1293                               cookie))
1294
1295        #
1296        #   7.  User Agent -> Server
1297        #
1298        #       POST /acme/process HTTP/1.1
1299        #       Cookie: $Version="1";
1300        #               Customer="WILE_E_COYOTE"; $Path="/acme";
1301        #               Part_Number="Rocket_Launcher_0001"; $Path="/acme";
1302        #               Shipping="FedEx"; $Path="/acme"
1303        #       [form data]
1304        #
1305        #       User chooses to process order.
1306        #
1307        #   8.  Server -> User Agent
1308        #
1309        #       HTTP/1.1 200 OK
1310        #
1311        #       Transaction is complete.
1312
1313        cookie = interact_2965(c, "http://www.acme.com/acme/process")
1314        self.assert_(
1315            re.search(r'Shipping="?FedEx"?;\s*\$Path="\/acme"', cookie) and
1316            "WILE_E_COYOTE" in cookie)
1317
1318        #
1319        # The user agent makes a series of requests on the origin server, after
1320        # each of which it receives a new cookie.  All the cookies have the same
1321        # Path attribute and (default) domain.  Because the request URLs all have
1322        # /acme as a prefix, and that matches the Path attribute, each request
1323        # contains all the cookies received so far.
1324
1325    def test_ietf_example_2(self):
1326        from cookielib import CookieJar, DefaultCookiePolicy
1327
1328        # 5.2  Example 2
1329        #
1330        # This example illustrates the effect of the Path attribute.  All detail
1331        # of request and response headers has been omitted.  Assume the user agent
1332        # has no stored cookies.
1333
1334        c = CookieJar(DefaultCookiePolicy(rfc2965=True))
1335
1336        # Imagine the user agent has received, in response to earlier requests,
1337        # the response headers
1338        #
1339        # Set-Cookie2: Part_Number="Rocket_Launcher_0001"; Version="1";
1340        #         Path="/acme"
1341        #
1342        # and
1343        #
1344        # Set-Cookie2: Part_Number="Riding_Rocket_0023"; Version="1";
1345        #         Path="/acme/ammo"
1346
1347        interact_2965(
1348            c, "http://www.acme.com/acme/ammo/specific",
1349            'Part_Number="Rocket_Launcher_0001"; Version="1"; Path="/acme"',
1350            'Part_Number="Riding_Rocket_0023"; Version="1"; Path="/acme/ammo"')
1351
1352        # A subsequent request by the user agent to the (same) server for URLs of
1353        # the form /acme/ammo/...  would include the following request header:
1354        #
1355        # Cookie: $Version="1";
1356        #         Part_Number="Riding_Rocket_0023"; $Path="/acme/ammo";
1357        #         Part_Number="Rocket_Launcher_0001"; $Path="/acme"
1358        #
1359        # Note that the NAME=VALUE pair for the cookie with the more specific Path
1360        # attribute, /acme/ammo, comes before the one with the less specific Path
1361        # attribute, /acme.  Further note that the same cookie name appears more
1362        # than once.
1363
1364        cookie = interact_2965(c, "http://www.acme.com/acme/ammo/...")
1365        self.assert_(
1366            re.search(r"Riding_Rocket_0023.*Rocket_Launcher_0001", cookie))
1367
1368        # A subsequent request by the user agent to the (same) server for a URL of
1369        # the form /acme/parts/ would include the following request header:
1370        #
1371        # Cookie: $Version="1"; Part_Number="Rocket_Launcher_0001"; $Path="/acme"
1372        #
1373        # Here, the second cookie's Path attribute /acme/ammo is not a prefix of
1374        # the request URL, /acme/parts/, so the cookie does not get forwarded to
1375        # the server.
1376
1377        cookie = interact_2965(c, "http://www.acme.com/acme/parts/")
1378        self.assert_("Rocket_Launcher_0001" in cookie and
1379                     "Riding_Rocket_0023" not in cookie)
1380
1381    def test_rejection(self):
1382        # Test rejection of Set-Cookie2 responses based on domain, path, port.
1383        from cookielib import DefaultCookiePolicy, LWPCookieJar
1384
1385        pol = DefaultCookiePolicy(rfc2965=True)
1386
1387        c = LWPCookieJar(policy=pol)
1388
1389        max_age = "max-age=3600"
1390
1391        # illegal domain (no embedded dots)
1392        cookie = interact_2965(c, "http://www.acme.com",
1393                               'foo=bar; domain=".com"; version=1')
1394        self.assert_(not c)
1395
1396        # legal domain
1397        cookie = interact_2965(c, "http://www.acme.com",
1398                               'ping=pong; domain="acme.com"; version=1')
1399        self.assertEquals(len(c), 1)
1400
1401        # illegal domain (host prefix "www.a" contains a dot)
1402        cookie = interact_2965(c, "http://www.a.acme.com",
1403                               'whiz=bang; domain="acme.com"; version=1')
1404        self.assertEquals(len(c), 1)
1405
1406        # legal domain
1407        cookie = interact_2965(c, "http://www.a.acme.com",
1408                               'wow=flutter; domain=".a.acme.com"; version=1')
1409        self.assertEquals(len(c), 2)
1410
1411        # can't partially match an IP-address
1412        cookie = interact_2965(c, "http://125.125.125.125",
1413                               'zzzz=ping; domain="125.125.125"; version=1')
1414        self.assertEquals(len(c), 2)
1415
1416        # illegal path (must be prefix of request path)
1417        cookie = interact_2965(c, "http://www.sol.no",
1418                               'blah=rhubarb; domain=".sol.no"; path="/foo"; '
1419                               'version=1')
1420        self.assertEquals(len(c), 2)
1421
1422        # legal path
1423        cookie = interact_2965(c, "http://www.sol.no/foo/bar",
1424                               'bing=bong; domain=".sol.no"; path="/foo"; '
1425                               'version=1')
1426        self.assertEquals(len(c), 3)
1427
1428        # illegal port (request-port not in list)
1429        cookie = interact_2965(c, "http://www.sol.no",
1430                               'whiz=ffft; domain=".sol.no"; port="90,100"; '
1431                               'version=1')
1432        self.assertEquals(len(c), 3)
1433
1434        # legal port
1435        cookie = interact_2965(
1436            c, "http://www.sol.no",
1437            r'bang=wallop; version=1; domain=".sol.no"; '
1438            r'port="90,100, 80,8080"; '
1439            r'max-age=100; Comment = "Just kidding! (\"|\\\\) "')
1440        self.assertEquals(len(c), 4)
1441
1442        # port attribute without any value (current port)
1443        cookie = interact_2965(c, "http://www.sol.no",
1444                               'foo9=bar; version=1; domain=".sol.no"; port; '
1445                               'max-age=100;')
1446        self.assertEquals(len(c), 5)
1447
1448        # encoded path
1449        # LWP has this test, but unescaping allowed path characters seems
1450        # like a bad idea, so I think this should fail:
1451##         cookie = interact_2965(c, "http://www.sol.no/foo/",
1452##                           r'foo8=bar; version=1; path="/%66oo"')
1453        # but this is OK, because '<' is not an allowed HTTP URL path
1454        # character:
1455        cookie = interact_2965(c, "http://www.sol.no/<oo/",
1456                               r'foo8=bar; version=1; path="/%3coo"')
1457        self.assertEquals(len(c), 6)
1458
1459        # save and restore
1460        filename = test_support.TESTFN
1461
1462        try:
1463            c.save(filename, ignore_discard=True)
1464            old = repr(c)
1465
1466            c = LWPCookieJar(policy=pol)
1467            c.load(filename, ignore_discard=True)
1468        finally:
1469            try: os.unlink(filename)
1470            except OSError: pass
1471
1472        self.assertEquals(old, repr(c))
1473
1474    def test_url_encoding(self):
1475        # Try some URL encodings of the PATHs.
1476        # (the behaviour here has changed from libwww-perl)
1477        from cookielib import CookieJar, DefaultCookiePolicy
1478
1479        c = CookieJar(DefaultCookiePolicy(rfc2965=True))
1480        interact_2965(c, "http://www.acme.com/foo%2f%25/%3c%3c%0Anew%E5/%E5",
1481                      "foo  =   bar; version    =   1")
1482
1483        cookie = interact_2965(
1484            c, "http://www.acme.com/foo%2f%25/<<%0anew�/���",
1485            'bar=baz; path="/foo/"; version=1');
1486        version_re = re.compile(r'^\$version=\"?1\"?', re.I)
1487        self.assert_("foo=bar" in cookie and version_re.search(cookie))
1488
1489        cookie = interact_2965(
1490            c, "http://www.acme.com/foo/%25/<<%0anew�/���")
1491        self.assert_(not cookie)
1492
1493        # unicode URL doesn't raise exception
1494        cookie = interact_2965(c, u"http://www.acme.com/\xfc")
1495
1496    def test_mozilla(self):
1497        # Save / load Mozilla/Netscape cookie file format.
1498        from cookielib import MozillaCookieJar, DefaultCookiePolicy
1499
1500        year_plus_one = time.localtime()[0] + 1
1501
1502        filename = test_support.TESTFN
1503
1504        c = MozillaCookieJar(filename,
1505                             policy=DefaultCookiePolicy(rfc2965=True))
1506        interact_2965(c, "http://www.acme.com/",
1507                      "foo1=bar; max-age=100; Version=1")
1508        interact_2965(c, "http://www.acme.com/",
1509                      'foo2=bar; port="80"; max-age=100; Discard; Version=1')
1510        interact_2965(c, "http://www.acme.com/", "foo3=bar; secure; Version=1")
1511
1512        expires = "expires=09-Nov-%d 23:12:40 GMT" % (year_plus_one,)
1513        interact_netscape(c, "http://www.foo.com/",
1514                          "fooa=bar; %s" % expires)
1515        interact_netscape(c, "http://www.foo.com/",
1516                          "foob=bar; Domain=.foo.com; %s" % expires)
1517        interact_netscape(c, "http://www.foo.com/",
1518                          "fooc=bar; Domain=www.foo.com; %s" % expires)
1519
1520        def save_and_restore(cj, ignore_discard):
1521            try:
1522                cj.save(ignore_discard=ignore_discard)
1523                new_c = MozillaCookieJar(filename,
1524                                         DefaultCookiePolicy(rfc2965=True))
1525                new_c.load(ignore_discard=ignore_discard)
1526            finally:
1527                try: os.unlink(filename)
1528                except OSError: pass
1529            return new_c
1530
1531        new_c = save_and_restore(c, True)
1532        self.assertEquals(len(new_c), 6)  # none discarded
1533        self.assert_("name='foo1', value='bar'" in repr(new_c))
1534
1535        new_c = save_and_restore(c, False)
1536        self.assertEquals(len(new_c), 4)  # 2 of them discarded on save
1537        self.assert_("name='foo1', value='bar'" in repr(new_c))
1538
1539    def test_netscape_misc(self):
1540        # Some additional Netscape cookies tests.
1541        from cookielib import CookieJar
1542        from urllib2 import Request
1543
1544        c = CookieJar()
1545        headers = []
1546        req = Request("http://foo.bar.acme.com/foo")
1547
1548        # Netscape allows a host part that contains dots
1549        headers.append("Set-Cookie: Customer=WILE_E_COYOTE; domain=.acme.com")
1550        res = FakeResponse(headers, "http://www.acme.com/foo")
1551        c.extract_cookies(res, req)
1552
1553        # and that the domain is the same as the host without adding a leading
1554        # dot to the domain.  Should not quote even if strange chars are used
1555        # in the cookie value.
1556        headers.append("Set-Cookie: PART_NUMBER=3,4; domain=foo.bar.acme.com")
1557        res = FakeResponse(headers, "http://www.acme.com/foo")
1558        c.extract_cookies(res, req)
1559
1560        req = Request("http://foo.bar.acme.com/foo")
1561        c.add_cookie_header(req)
1562        self.assert_(
1563            "PART_NUMBER=3,4" in req.get_header("Cookie") and
1564            "Customer=WILE_E_COYOTE" in req.get_header("Cookie"))
1565
1566    def test_intranet_domains_2965(self):
1567        # Test handling of local intranet hostnames without a dot.
1568        from cookielib import CookieJar, DefaultCookiePolicy
1569
1570        c = CookieJar(DefaultCookiePolicy(rfc2965=True))
1571        interact_2965(c, "http://example/",
1572                      "foo1=bar; PORT; Discard; Version=1;")
1573        cookie = interact_2965(c, "http://example/",
1574                               'foo2=bar; domain=".local"; Version=1')
1575        self.assert_("foo1=bar" in cookie)
1576
1577        interact_2965(c, "http://example/", 'foo3=bar; Version=1')
1578        cookie = interact_2965(c, "http://example/")
1579        self.assert_("foo2=bar" in cookie and len(c) == 3)
1580
1581    def test_intranet_domains_ns(self):
1582        from cookielib import CookieJar, DefaultCookiePolicy
1583
1584        c = CookieJar(DefaultCookiePolicy(rfc2965 = False))
1585        interact_netscape(c, "http://example/", "foo1=bar")
1586        cookie = interact_netscape(c, "http://example/",
1587                                   'foo2=bar; domain=.local')
1588        self.assertEquals(len(c), 2)
1589        self.assert_("foo1=bar" in cookie)
1590
1591        cookie = interact_netscape(c, "http://example/")
1592        self.assert_("foo2=bar" in cookie)
1593        self.assertEquals(len(c), 2)
1594
1595    def test_empty_path(self):
1596        from cookielib import CookieJar, DefaultCookiePolicy
1597        from urllib2 import Request
1598
1599        # Test for empty path
1600        # Broken web-server ORION/1.3.38 returns to the client response like
1601        #
1602        #       Set-Cookie: JSESSIONID=ABCDERANDOM123; Path=
1603        #
1604        # ie. with Path set to nothing.
1605        # In this case, extract_cookies() must set cookie to / (root)
1606        c = CookieJar(DefaultCookiePolicy(rfc2965 = True))
1607        headers = []
1608
1609        req = Request("http://www.ants.com/")
1610        headers.append("Set-Cookie: JSESSIONID=ABCDERANDOM123; Path=")
1611        res = FakeResponse(headers, "http://www.ants.com/")
1612        c.extract_cookies(res, req)
1613
1614        req = Request("http://www.ants.com/")
1615        c.add_cookie_header(req)
1616
1617        self.assertEquals(req.get_header("Cookie"),
1618                          "JSESSIONID=ABCDERANDOM123")
1619        self.assertEquals(req.get_header("Cookie2"), '$Version="1"')
1620
1621        # missing path in the request URI
1622        req = Request("http://www.ants.com:8080")
1623        c.add_cookie_header(req)
1624
1625        self.assertEquals(req.get_header("Cookie"),
1626                          "JSESSIONID=ABCDERANDOM123")
1627        self.assertEquals(req.get_header("Cookie2"), '$Version="1"')
1628
1629    def test_session_cookies(self):
1630        from cookielib import CookieJar
1631        from urllib2 import Request
1632
1633        year_plus_one = time.localtime()[0] + 1
1634
1635        # Check session cookies are deleted properly by
1636        # CookieJar.clear_session_cookies method
1637
1638        req = Request('http://www.perlmeister.com/scripts')
1639        headers = []
1640        headers.append("Set-Cookie: s1=session;Path=/scripts")
1641        headers.append("Set-Cookie: p1=perm; Domain=.perlmeister.com;"
1642                       "Path=/;expires=Fri, 02-Feb-%d 23:24:20 GMT" %
1643                       year_plus_one)
1644        headers.append("Set-Cookie: p2=perm;Path=/;expires=Fri, "
1645                       "02-Feb-%d 23:24:20 GMT" % year_plus_one)
1646        headers.append("Set-Cookie: s2=session;Path=/scripts;"
1647                       "Domain=.perlmeister.com")
1648        headers.append('Set-Cookie2: s3=session;Version=1;Discard;Path="/"')
1649        res = FakeResponse(headers, 'http://www.perlmeister.com/scripts')
1650
1651        c = CookieJar()
1652        c.extract_cookies(res, req)
1653        # How many session/permanent cookies do we have?
1654        counter = {"session_after": 0,
1655                   "perm_after": 0,
1656                   "session_before": 0,
1657                   "perm_before": 0}
1658        for cookie in c:
1659            key = "%s_before" % cookie.value
1660            counter[key] = counter[key] + 1
1661        c.clear_session_cookies()
1662        # How many now?
1663        for cookie in c:
1664            key = "%s_after" % cookie.value
1665            counter[key] = counter[key] + 1
1666
1667        self.assert_(not (
1668            # a permanent cookie got lost accidently
1669            counter["perm_after"] != counter["perm_before"] or
1670            # a session cookie hasn't been cleared
1671            counter["session_after"] != 0 or
1672            # we didn't have session cookies in the first place
1673            counter["session_before"] == 0))
1674
1675
1676def test_main(verbose=None):
1677    from test import test_sets
1678    test_support.run_unittest(
1679        DateTimeTests,
1680        HeaderTests,
1681        CookieTests,
1682        FileCookieJarTests,
1683        LWPCookieTests,
1684        )
1685
1686if __name__ == "__main__":
1687    test_main(verbose=True)
1688