1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3"""Tests for code in cookies.py.
4"""
5from __future__ import unicode_literals
6import re
7import sys
8import logging
9if sys.version_info < (3, 0, 0):
10    from urllib import quote, unquote
11else:
12    from urllib.parse import quote, unquote
13    unichr = chr
14    basestring = str
15from datetime import datetime, tzinfo, timedelta
16from pytest import raises
17
18from cookies import (
19        InvalidCookieError, InvalidCookieAttributeError,
20        Definitions,
21        Cookie, Cookies,
22        render_date, parse_date,
23        parse_string, parse_value, parse_domain, parse_path,
24        parse_one_response,
25        encode_cookie_value, encode_extension_av,
26        valid_value, valid_date, valid_domain, valid_path,
27        strip_spaces_and_quotes, _total_seconds,
28        )
29
30
31class RFC1034:
32    """Definitions from RFC 1034: 'DOMAIN NAMES - CONCEPTS AND FACILITIES'
33    section 3.5, as cited in RFC 6265 4.1.1.
34    """
35    digit = "[0-9]"
36    letter = "[A-Za-z]"
37    let_dig = "[0-9A-Za-z]"
38    let_dig_hyp = "[0-9A-Za-z\-]"
39    assert "\\" in let_dig_hyp
40    ldh_str = "%s+" % let_dig_hyp
41    label = "(?:%s|%s|%s)" % (
42            letter,
43            letter + let_dig,
44            letter + ldh_str + let_dig)
45    subdomain = "(?:%s\.)*(?:%s)" % (label, label)
46    domain = "( |%s)" % (subdomain)
47
48    def test_sanity(self):
49        "Basic smoke tests that definitions transcribed OK"
50        match = re.compile("^%s\Z" % self.domain).match
51        assert match("A.ISI.EDU")
52        assert match("XX.LCS.MIT.EDU")
53        assert match("SRI-NIC.ARPA")
54        assert not match("foo+bar")
55        assert match("foo.com")
56        assert match("foo9.com")
57        assert not match("9foo.com")
58        assert not match("26.0.0.73.COM")
59        assert not match(".woo.com")
60        assert not match("blop.foo.")
61        assert match("foo-bar.com")
62        assert not match("-foo.com")
63        assert not match("foo.com-")
64
65
66class RFC1123:
67    """Definitions from RFC 1123: "Requirements for Internet Hosts --
68    Application and Support" section 2.1, cited in RFC 6265 section
69    4.1.1 as an update to RFC 1034.
70    Here this is really just used for testing Domain attribute values.
71    """
72    # Changed per 2.1 (similar to some changes in RFC 1101)
73    # this implementation is a bit simpler...
74    # n.b.: there are length limits in the real thing
75    label = "{let_dig}(?:(?:{let_dig_hyp}+)?{let_dig})?".format(
76            let_dig=RFC1034.let_dig, let_dig_hyp=RFC1034.let_dig_hyp)
77    subdomain = "(?:%s\.)*(?:%s)" % (label, label)
78    domain = "( |%s)" % (subdomain)
79
80    def test_sanity(self):
81        "Basic smoke tests that definitions transcribed OK"
82        match = re.compile("^%s\Z" % self.domain).match
83        assert match("A.ISI.EDU")
84        assert match("XX.LCS.MIT.EDU")
85        assert match("SRI-NIC.ARPA")
86        assert not match("foo+bar")
87        assert match("foo.com")
88        assert match("9foo.com")
89        assert match("3Com.COM")
90        assert match("3M.COM")
91
92
93class RFC2616:
94    """Definitions from RFC 2616 section 2.2, as cited in RFC 6265 4.1.1
95    """
96    SEPARATORS = '()<>@,;:\\"/[]?={} \t'
97
98
99class RFC5234:
100    """Basic definitions per RFC 5234: 'Augmented BNF for Syntax
101    Specifications'
102    """
103    CHAR = "".join([chr(i) for i in range(0, 127 + 1)])
104    CTL = "".join([chr(i) for i in range(0, 31 + 1)]) + "\x7f"
105    # this isn't in the RFC but it can be handy
106    NONCTL = "".join([chr(i) for i in range(32, 127)])
107    # this is what the RFC says about a token more or less verbatim
108    TOKEN = "".join(sorted(set(NONCTL) - set(RFC2616.SEPARATORS)))
109
110
111class FixedOffsetTz(tzinfo):
112    """A tzinfo subclass for attaching to datetime objects.
113
114    Used for various tests involving date parsing, since Python stdlib does not
115    obviously provide tzinfo subclasses and testing this module only requires
116    a very simple one.
117    """
118    def __init__(self, offset):
119        # tzinfo.utcoffset() throws an error for sub-minute amounts,
120        # so round
121        minutes = round(offset / 60.0, 0)
122        self.__offset = timedelta(minutes=minutes)
123
124    def utcoffset(self, dt):
125        return self.__offset
126
127    def tzname(self, dt):
128        return "FixedOffsetTz" + str(self.__offset.seconds)
129
130    def dst(self, dt):
131        return timedelta(0)
132
133
134class TestInvalidCookieError(object):
135    """Exercise the trivial behavior of the InvalidCookieError exception.
136    """
137    def test_simple(self):
138        "This be the test"
139        def exception(data):
140            "Gather an InvalidCookieError exception"
141            try:
142                raise InvalidCookieError(data)
143            except InvalidCookieError as exception:
144                return exception
145            # other exceptions will pass through
146            return None
147        assert exception("no donut").data == "no donut"
148
149        # Spot check for obvious junk in loggable representations.
150        e = exception("yay\x00whee")
151        assert "\x00" not in repr(e)
152        assert "\x00" not in str(e)
153        assert "yaywhee" not in repr(e)
154        assert "yaywhee" not in str(e)
155        assert "\n" not in repr(exception("foo\nbar"))
156
157
158class TestInvalidCookieAttributeError(object):
159    """Exercise the trivial behavior of InvalidCookieAttributeError.
160    """
161    def exception(self, *args, **kwargs):
162        "Generate an InvalidCookieAttributeError exception naturally"
163        try:
164            raise InvalidCookieAttributeError(*args, **kwargs)
165        except InvalidCookieAttributeError as exception:
166            return exception
167        return None
168
169    def test_simple(self):
170        e = self.exception("foo", "bar")
171        assert e.name == "foo"
172        assert e.value == "bar"
173
174    def test_junk_in_loggables(self):
175        # Spot check for obvious junk in loggable representations.
176        # This isn't completely idle: for example, nulls are ignored in
177        # %-formatted text, and this could be very misleading
178        e = self.exception("ya\x00y", "whee")
179        assert "\x00" not in repr(e)
180        assert "\x00" not in str(e)
181        assert "yay" not in repr(e)
182        assert "yay" not in str(e)
183
184        e = self.exception("whee", "ya\x00y")
185        assert "\x00" not in repr(e)
186        assert "\x00" not in str(e)
187        assert "yay" not in repr(e)
188        assert "yay" not in str(e)
189
190        assert "\n" not in repr(self.exception("yay", "foo\nbar"))
191        assert "\n" not in repr(self.exception("foo\nbar", "yay"))
192
193    def test_no_name(self):
194        # not recommended to do this, but we want to handle it if people do
195        e = self.exception(None, "stuff")
196        assert e.name == None
197        assert e.value == "stuff"
198        assert e.reason == None
199        assert 'stuff' in str(e)
200
201
202class TestDefinitions(object):
203    """Test the patterns in cookies.Definitions against specs.
204    """
205    def test_cookie_name(self, check_unicode=False):
206        """Check COOKIE_NAME against the token definition in RFC 2616 2.2 (as
207        cited in RFC 6265):
208
209       token          = 1*<any CHAR except CTLs or separators>
210       separators     = "(" | ")" | "<" | ">" | "@"
211                      | "," | ";" | ":" | "\" | <">
212                      | "/" | "[" | "]" | "?" | "="
213                      | "{" | "}" | SP | HT
214
215        (Definitions.COOKIE_NAME is regex-ready while RFC5234.TOKEN is more
216        clearly related to the RFC; they should be functionally the same)
217        """
218        regex = Definitions.COOKIE_NAME_RE
219        assert regex.match(RFC5234.TOKEN)
220        assert not regex.match(RFC5234.NONCTL)
221        for c in RFC5234.CTL:
222            assert not regex.match(c)
223        for c in RFC2616.SEPARATORS:
224            # Skip special case - some number of Java and PHP apps have used
225            # colon in names, while this is dumb we want to not choke on this
226            # by default since it may be the single biggest cause of bugs filed
227            # against Python's cookie libraries
228            if c == ':':
229                continue
230            assert not regex.match(c)
231        # Unicode over 7 bit ASCII shouldn't match, but this takes a while
232        if check_unicode:
233            for i in range(127, 0x10FFFF + 1):
234                assert not regex.match(unichr(i))
235
236    def test_cookie_octet(self):
237        """Check COOKIE_OCTET against the definition in RFC 6265:
238
239        cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
240                              ; US-ASCII characters excluding CTLs,
241                              ; whitespace DQUOTE, comma, semicolon,
242                              ; and backslash
243        """
244        match = re.compile("^[%s]+\Z" % Definitions.COOKIE_OCTET).match
245        for c in RFC5234.CTL:
246            assert not match(c)
247            assert not match("a%sb" % c)
248        # suspect RFC typoed 'whitespace, DQUOTE' as 'whitespace DQUOTE'
249        assert not match(' ')
250        assert not match('"')
251        assert not match(',')
252        assert not match(';')
253        assert not match('\\')
254        # the spec above DOES include =.-
255        assert match("=")
256        assert match(".")
257        assert match("-")
258
259        # Check that everything else in CHAR works.
260        safe_cookie_octet = "".join(sorted(
261            set(RFC5234.NONCTL) - set(' ",;\\')))
262        assert match(safe_cookie_octet)
263
264    def test_set_cookie_header(self):
265        """Smoke test SET_COOKIE_HEADER (used to compile SET_COOKIE_HEADER_RE)
266        against HEADER_CASES.
267        """
268        # should match if expectation is not an error, shouldn't match if it is
269        # an error. set-cookie-header is for responses not requests, so use
270        # response expectation rather than request expectation
271        match = re.compile(Definitions.SET_COOKIE_HEADER).match
272        for case in HEADER_CASES:
273            arg, kwargs, request_result, expected = case
274            this_match = match(arg)
275            if expected and not isinstance(expected, type):
276                assert this_match, "should match as response: " + repr(arg)
277            else:
278                if not request_result:
279                    assert not this_match, \
280                            "should not match as response: " + repr(arg)
281
282    def test_cookie_cases(self):
283        """Smoke test COOKIE_HEADER (used to compile COOKIE_HEADER_RE) against
284        HEADER_CASES.
285        """
286        # should match if expectation is not an error, shouldn't match if it is
287        # an error. cookie-header is for requests not responses, so use request
288        # expectation rather than response expectation
289        match = re.compile(Definitions.COOKIE).match
290        for case in HEADER_CASES:
291            arg, kwargs, expected, response_result = case
292            this_match = match(arg)
293            if expected and not isinstance(expected, type):
294                assert this_match, "should match as request: " + repr(arg)
295            else:
296                if not response_result:
297                    assert not this_match, \
298                    "should not match as request: " + repr(arg)
299
300    def test_cookie_pattern(self):
301        """Smoke test Definitions.COOKIE (used to compile COOKIE_RE) against
302        the grammar for cookie-header as in RFC 6265.
303
304         cookie-header     = "Cookie:" OWS cookie-string OWS
305         cookie-string     = cookie-pair *( ";" SP cookie-pair )
306         cookie-pair       = cookie-name "=" cookie-value
307         cookie-name       = token
308         cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
309
310         cookie-name and cookie-value are not broken apart for separate
311         testing, as the former is essentially just token and the latter
312         essentially just cookie-octet.
313        """
314        match = re.compile(Definitions.COOKIE).match
315        # cookie-pair behavior around =
316        assert match("foo").group('invalid')
317        assert match("foo=bar")
318        # Looks dumb, but this is legal because "=" is valid for cookie-octet.
319        assert match("a=b=c")
320        # DQUOTE *cookie-octet DQUOTE - allowed
321        assert match('foo="bar"')
322
323        # for testing on the contents of cookie name and cookie value,
324        # see test_cookie_name and test_cookie_octet.
325
326        regex = re.compile(Definitions.COOKIE)
327        correct = [
328            ('foo', 'yar', ''),
329            ('bar', 'eeg', ''),
330            ('baz', 'wog', ''),
331            ('frob', 'laz', '')]
332
333        def assert_correct(s):
334            #naive = re.findall(" *([^;]+)=([^;]+) *(?:;|\Z)", s)
335            result = regex.findall(s)
336            assert result == correct
337        # normal-looking case should work normally
338        assert_correct("foo=yar; bar=eeg; baz=wog; frob=laz")
339        # forgive lack of whitespace as long as semicolons are explicit
340        assert_correct("foo=yar;bar=eeg;baz=wog;frob=laz")
341        # forgive too much whitespace AROUND values
342        assert_correct("  foo=yar;  bar=eeg;  baz=wog;   frob=laz  ")
343
344        # Actually literal spaces are NOT allowed in cookie values per RFC 6265
345        # and it is UNWISE to put them in without escaping. But we want the
346        # flexibility to let this pass with a warning, because this is the kind
347        # of bad idea which is very common and results in loud complaining on
348        # issue trackers on the grounds that PHP does it or something. So the
349        # regex is weakened, but the presence of a space should still be at
350        # least noted, and an exception must be raised if = is also used
351        # - because that would often indicate the loss of cookies due to
352        # forgotten separator, as in "foo=yar bar=eeg baz=wog frob=laz".
353        assert regex.findall("foo=yar; bar=eeg; baz=wog; frob=l az") == [
354            ('foo', 'yar', ''),
355            ('bar', 'eeg', ''),
356            ('baz', 'wog', ''),
357            # handle invalid internal whitespace.
358            ('frob', 'l az', '')
359            ]
360
361        # Without semicolons or inside semicolon-delimited blocks, the part
362        # before the first = should be interpreted as a name, and the rest as
363        # a value (since = is not forbidden for cookie values). Thus:
364        result = regex.findall("foo=yarbar=eegbaz=wogfrob=laz")
365        assert result[0][0] == 'foo'
366        assert result[0][1] == 'yarbar=eegbaz=wogfrob=laz'
367        assert result[0][2] == ''
368
369        # Make some bad values and see that it's handled reasonably.
370        # (related to http://bugs.python.org/issue2988)
371        # don't test on semicolon because the regexp stops there, reasonably.
372        for c in '\x00",\\':
373            nasty = "foo=yar" + c + "bar"
374            result = regex.findall(nasty + "; baz=bam")
375            # whole bad pair reported in the 'invalid' group (the third one)
376            assert result[0][2] == nasty
377            # kept on truckin' and got the other one just fine.
378            assert result[1] == ('baz', 'bam', '')
379            # same thing if the good one is first and the bad one second
380            result = regex.findall("baz=bam; " + nasty)
381            assert result[0] == ('baz', 'bam', '')
382            assert result[1][2] == ' ' + nasty
383
384    def test_extension_av(self, check_unicode=False):
385        """Test Definitions.EXTENSION_AV against extension-av per RFC 6265.
386
387        extension-av      = <any CHAR except CTLs or ";">
388        """
389        # This is how it's defined in RFC 6265, just about verbatim.
390        extension_av_explicit = "".join(sorted(
391                set(RFC5234.CHAR) - set(RFC5234.CTL + ";")))
392        # ... that should turn out to be the same as Definitions.EXTENSION_AV
393        match = re.compile("^([%s]+)\Z" % Definitions.EXTENSION_AV).match
394        # Verify I didn't mess up on escaping here first
395        assert match(r']')
396        assert match(r'[')
397        assert match(r"'")
398        assert match(r'"')
399        assert match("\\")
400        assert match(extension_av_explicit)
401        # There should be some CHAR not matched
402        assert not match(RFC5234.CHAR)
403        # Every single CTL should not match
404        for c in RFC5234.CTL + ";":
405            assert not match(c)
406        # Unicode over 7 bit ASCII shouldn't match, but this takes a while
407        if check_unicode:
408            for i in range(127, 0x10FFFF + 1):
409                assert not match(unichr(i))
410
411    def test_max_age_av(self):
412        "Smoke test Definitions.MAX_AGE_AV"
413        # Not a lot to this, it's just digits
414        match = re.compile("^%s\Z" % Definitions.MAX_AGE_AV).match
415        assert not match("")
416        assert not match("Whiskers")
417        assert not match("Max-Headroom=992")
418        for c in "123456789":
419            assert not match(c)
420            assert match("Max-Age=%s" % c)
421        assert match("Max-Age=0")
422        for c in RFC5234.CHAR:
423            assert not match(c)
424
425    def test_label(self, check_unicode=False):
426        "Test label, as used in Domain attribute"
427        match = re.compile("^(%s)\Z" % Definitions.LABEL).match
428        for i in range(0, 10):
429            assert match(str(i))
430        assert not match(".")
431        assert not match(",")
432        for c in RFC5234.CTL:
433            assert not match("a%sb" % c)
434            assert not match("%sb" % c)
435            assert not match("a%s" % c)
436        # Unicode over 7 bit ASCII shouldn't match, but this takes a while
437        if check_unicode:
438            for i in range(127, 0x10FFFF + 1):
439                assert not match(unichr(i))
440
441    def test_domain_av(self):
442        "Smoke test Definitions.DOMAIN_AV"
443        # This is basically just RFC1123.subdomain, which has its own
444        # assertions in the class definition
445        bad_domains = [
446                ""
447                ]
448        good_domains = [
449                "foobar.com",
450                "foo-bar.com",
451                "3Com.COM"
452                ]
453
454        # First test DOMAIN via DOMAIN_RE
455        match = Definitions.DOMAIN_RE.match
456        for domain in bad_domains:
457            assert not match(domain)
458        for domain in good_domains:
459            assert match(domain)
460
461        # Now same tests through DOMAIN_AV
462        match = re.compile("^%s\Z" % Definitions.DOMAIN_AV).match
463        for domain in bad_domains:
464            assert not match("Domain=%s" % domain)
465        for domain in good_domains:
466            assert not match(domain)
467            assert match("Domain=%s" % domain)
468        # This is NOT valid and shouldn't be tolerated in cookies we create,
469        # but it should be tolerated in existing cookies since people do it;
470        # interpreted by stripping the initial .
471        assert match("Domain=.foo.net")
472
473    def test_path_av(self):
474        "Smoke test PATH and PATH_AV"
475        # This is basically just EXTENSION_AV, see test_extension_av
476        bad_paths = [
477                ""
478                ]
479        good_paths = [
480                "/",
481                "/foo",
482                "/foo/bar"
483                ]
484        match = Definitions.PATH_RE.match
485        for path in bad_paths:
486            assert not match(path)
487        for path in good_paths:
488            assert match(path)
489
490        match = re.compile("^%s\Z" % Definitions.PATH_AV).match
491        for path in bad_paths:
492            assert not match("Path=%s" % path)
493        for path in good_paths:
494            assert not match(path)
495            assert match("Path=%s" % path)
496
497    def test_months(self):
498        """Sanity checks on MONTH_SHORT and MONTH_LONG month name recognizers.
499
500        The RFCs set these in stone, they aren't locale-dependent.
501        """
502        match = re.compile(Definitions.MONTH_SHORT).match
503        assert match("Jan")
504        assert match("Feb")
505        assert match("Mar")
506        assert match("Apr")
507        assert match("May")
508        assert match("Jun")
509        assert match("Jul")
510        assert match("Aug")
511        assert match("Sep")
512        assert match("Oct")
513        assert match("Nov")
514        assert match("Dec")
515
516        match = re.compile(Definitions.MONTH_LONG).match
517        assert match("January")
518        assert match("February")
519        assert match("March")
520        assert match("April")
521        assert match("May")
522        assert match("June")
523        assert match("July")
524        assert match("August")
525        assert match("September")
526        assert match("October")
527        assert match("November")
528        assert match("December")
529
530    def test_weekdays(self):
531        """Sanity check on WEEKDAY_SHORT and WEEKDAY_LONG weekday
532        recognizers.
533
534        The RFCs set these in stone, they aren't locale-dependent.
535        """
536        match = re.compile(Definitions.WEEKDAY_SHORT).match
537        assert match("Mon")
538        assert match("Tue")
539        assert match("Wed")
540        assert match("Thu")
541        assert match("Fri")
542        assert match("Sat")
543        assert match("Sun")
544
545        match = re.compile(Definitions.WEEKDAY_LONG).match
546        assert match("Monday")
547        assert match("Tuesday")
548        assert match("Wednesday")
549        assert match("Thursday")
550        assert match("Friday")
551        assert match("Saturday")
552        assert match("Sunday")
553
554    def test_day_of_month(self):
555        """Check that the DAY_OF_MONTH regex allows all actual days, but
556        excludes obviously wrong ones (so they are tossed in the first pass).
557        """
558        match = re.compile(Definitions.DAY_OF_MONTH).match
559        for day in ['01', '02', '03', '04', '05', '06', '07', '08', '09', ' 1',
560                ' 2', ' 3', ' 4', ' 5', ' 6', ' 7', ' 8', ' 9', '1', '2', '3',
561                '4', '5', '6', '7', '8', '9'] \
562                    + [str(i) for i in range(10, 32)]:
563            assert match(day)
564        assert not match("0")
565        assert not match("00")
566        assert not match("000")
567        assert not match("111")
568        assert not match("99")
569        assert not match("41")
570
571    def test_expires_av(self):
572        "Smoke test the EXPIRES_AV regex pattern"
573        # Definitions.EXPIRES_AV is actually pretty bad because it's a disaster
574        # to test three different date formats with lots of definition
575        # dependencies, and odds are good that other implementations are loose.
576        # so this parser is also loose. "liberal in what you accept,
577        # conservative in what you produce"
578        match = re.compile("^%s\Z" % Definitions.EXPIRES_AV).match
579        assert not match("")
580        assert not match("Expires=")
581
582        assert match("Expires=Tue, 15-Jan-2013 21:47:38 GMT")
583        assert match("Expires=Sun, 06 Nov 1994 08:49:37 GMT")
584        assert match("Expires=Sunday, 06-Nov-94 08:49:37 GMT")
585        assert match("Expires=Sun Nov  6 08:49:37 1994")
586        # attributed to Netscape in RFC 2109 10.1.2
587        assert match("Expires=Mon, 13-Jun-93 10:00:00 GMT")
588
589        assert not match("Expires=S9n, 06 Nov 1994 08:49:37 GMT")
590        assert not match("Expires=Sun3ay, 06-Nov-94 08:49:37 GMT")
591        assert not match("Expires=S9n Nov  6 08:49:37 1994")
592
593        assert not match("Expires=Sun, A6 Nov 1994 08:49:37 GMT")
594        assert not match("Expires=Sunday, 0B-Nov-94 08:49:37 GMT")
595        assert not match("Expires=Sun No8  6 08:49:37 1994")
596
597        assert not match("Expires=Sun, 06 N3v 1994 08:49:37 GMT")
598        assert not match("Expires=Sunday, 06-N8v-94 08:49:37 GMT")
599        assert not match("Expires=Sun Nov  A 08:49:37 1994")
600
601        assert not match("Expires=Sun, 06 Nov 1B94 08:49:37 GMT")
602        assert not match("Expires=Sunday, 06-Nov-C4 08:49:37 GMT")
603        assert not match("Expires=Sun Nov  6 08:49:37 1Z94")
604
605    def test_no_obvious_need_for_disjunctive_attr_pattern(self):
606        """Smoke test the assumption that extension-av is a reasonable set of
607        chars for all attrs (and thus that there is no reason to use a fancy
608        disjunctive pattern in the findall that splits out the attrs, freeing
609        us to use EXTENSION_AV instead).
610
611        If this works, then ATTR should work
612        """
613        match = re.compile("^[%s]+\Z" % Definitions.EXTENSION_AV).match
614        assert match("Expires=Sun, 06 Nov 1994 08:49:37 GMT")
615        assert match("Expires=Sunday, 06-Nov-94 08:49:37 GMT")
616        assert match("Expires=Sun Nov  6 08:49:37 1994")
617        assert match("Max-Age=14658240962")
618        assert match("Domain=FoO.b9ar.baz")
619        assert match("Path=/flakes")
620        assert match("Secure")
621        assert match("HttpOnly")
622
623    def test_attr(self):
624        """Smoke test ATTR, used to compile ATTR_RE.
625        """
626        match = re.compile(Definitions.ATTR).match
627
628        def recognized(pattern):
629            "macro for seeing if ATTR recognized something"
630            this_match = match(pattern)
631            if not this_match:
632                return False
633            groupdict = this_match.groupdict()
634            if groupdict['unrecognized']:
635                return False
636            return True
637
638        # Quickly test that a batch of attributes matching the explicitly
639        # recognized patterns make it through without anything in the
640        # 'unrecognized' catchall capture group.
641        for pattern in [
642                "Secure",
643                "HttpOnly",
644                "Max-Age=9523052",
645                "Domain=frobble.com",
646                "Domain=3Com.COM",
647                "Path=/",
648                "Expires=Wed, 09 Jun 2021 10:18:14 GMT",
649                ]:
650            assert recognized(pattern)
651
652        # Anything else is in extension-av and that's very broad;
653        # see test_extension_av for that test.
654        # This is only about the recognized ones.
655        assert not recognized("Frob=mugmannary")
656        assert not recognized("Fqjewp@1j5j510923")
657        assert not recognized(";aqjwe")
658        assert not recognized("ETJpqw;fjw")
659        assert not recognized("fjq;")
660        assert not recognized("Expires=\x00")
661
662        # Verify interface from regexp for extracting values isn't changed;
663        # a little rigidity here is a good idea
664        expires = "Wed, 09 Jun 2021 10:18:14 GMT"
665        m = match("Expires=%s" % expires)
666        assert m.group("expires") == expires
667
668        max_age = "233951698"
669        m = match("Max-Age=%s" % max_age)
670        assert m.group("max_age") == max_age
671
672        domain = "flarp"
673        m = match("Domain=%s" % domain)
674        assert m.group("domain") == domain
675
676        path = "2903"
677        m = match("Path=%s" % path)
678        assert m.group("path") == path
679
680        m = match("Secure")
681        assert m.group("secure")
682        assert not m.group("httponly")
683
684        m = match("HttpOnly")
685        assert not m.group("secure")
686        assert m.group("httponly")
687
688    def test_date_accepts_formats(self):
689        """Check that DATE matches most formats used in Expires: headers,
690        and explain what the different formats are about.
691
692        The value extraction of this regexp is more comprehensively exercised
693        by test_date_parsing().
694        """
695        # Date formats vary widely in the wild. Even the standards vary widely.
696        # This series of tests does spot-checks with instances of formats that
697        # it makes sense to support. In the following comments, each format is
698        # discussed and the rationale for the overall regexp is developed.
699
700        match = re.compile(Definitions.DATE).match
701
702        # The most common formats, related to the old Netscape cookie spec
703        # (NCSP), are supposed to follow this template:
704        #
705        #   Wdy, DD-Mon-YYYY HH:MM:SS GMT
706        #
707        # (where 'Wdy' is a short weekday, and 'Mon' is a named month).
708        assert match("Mon, 20-Jan-1994 00:00:00 GMT")
709
710        # Similarly, RFC 850 proposes this format:
711        #
712        #   Weekday, DD-Mon-YY HH:MM:SS GMT
713        #
714        # (with a long-form weekday and a 2-digit year).
715        assert match("Tuesday, 12-Feb-92 23:25:42 GMT")
716
717        # RFC 1036 obsoleted the RFC 850 format:
718        #
719        #   Wdy, DD Mon YY HH:MM:SS GMT
720        #
721        # (shortening the weekday format and changing dashes to spaces).
722        assert match("Wed, 30 Mar 92 13:16:12 GMT")
723
724        # RFC 6265 cites a definition from RFC 2616, which uses the RFC 1123
725        # definition but limits it to GMT (consonant with NCSP). RFC 1123
726        # expanded RFC 822 with 2-4 digit years (more permissive than NCSP);
727        # RFC 822 left weekday and seconds as optional, and a day of 1-2 digits
728        # (all more permissive than NCSP). Giving something like this:
729        #
730        #   [Wdy, ][D]D Mon [YY]YY HH:MM[:SS] GMT
731        #
732        assert match("Thu, 3 Apr 91 12:46 GMT")
733        # No weekday, two digit year.
734        assert match("13 Apr 91 12:46 GMT")
735
736        # Similarly, there is RFC 2822:
737        #
738        #   [Wdy, ][D]D Mon YYYY HH:MM[:SS] GMT
739        # (which only differs in requiring a 4-digit year, where RFC  1123
740        # permits 2 or 3 digit years).
741        assert match("13 Apr 1991 12:46 GMT")
742        assert match("Wed, 13 Apr 1991 12:46 GMT")
743
744        # The generalized format given above encompasses RFC 1036 and RFC 2822
745        # and would encompass NCSP except for the dashes; allowing long-form
746        # weekdays also encompasses the format proposed in RFC 850. Taken
747        # together, this should cover something like 99% of Expires values
748        # (see, e.g., https://bugzilla.mozilla.org/show_bug.cgi?id=610218)
749
750        # Finally, we also want to support asctime format, as mentioned in RFC
751        # 850 and RFC 2616 and occasionally seen in the wild:
752        #       Wdy Mon DD HH:MM:SS YYYY
753        # e.g.: Sun Nov  6 08:49:37 1994
754        assert match("Sun Nov  6 08:49:37 1994")
755        assert match("Sun Nov 26 08:49:37 1994")
756        # Reportedly someone has tacked 'GMT' on to the end of an asctime -
757        # although this is not RFC valid, it is pretty harmless
758        assert match("Sun Nov 26 08:49:37 1994 GMT")
759
760        # This test is not passed until it is shown that it wasn't trivially
761        # because DATE was matching .* or similar. This isn't intended to be
762        # a thorough test, just rule out the obvious reason. See test_date()
763        # for a more thorough workout of the whole parse and render mechanisms
764        assert not match("")
765        assert not match("       ")
766        assert not match("wobbly")
767        assert not match("Mon")
768        assert not match("Mon, 20")
769        assert not match("Mon, 20 Jan")
770        assert not match("Mon, 20,Jan,1994 00:00:00 GMT")
771        assert not match("Tuesday, 12-Feb-992 23:25:42 GMT")
772        assert not match("Wed, 30 Mar 92 13:16:1210 GMT")
773        assert not match("Wed, 30 Mar 92 13:16:12:10 GMT")
774        assert not match("Thu, 3 Apr 91 12:461 GMT")
775
776    def test_eol(self):
777        """Test that the simple EOL regex works basically as expected.
778        """
779        split = Definitions.EOL.split
780        assert split("foo\nbar") == ["foo", "bar"]
781        assert split("foo\r\nbar") == ["foo", "bar"]
782        letters = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
783        assert split("\n".join(letters)) == letters
784        assert split("\r\n".join(letters)) == letters
785
786    def test_compiled(self):
787        """Check that certain patterns are present as compiled regexps
788        """
789        re_type = type(re.compile(''))
790
791        def present(name):
792            "Macro for testing existence of an re in Definitions"
793            item = getattr(Definitions, name)
794            return item and isinstance(item, re_type)
795
796        assert present("COOKIE_NAME_RE")
797        assert present("COOKIE_RE")
798        assert present("SET_COOKIE_HEADER_RE")
799        assert present("ATTR_RE")
800        assert present("DATE_RE")
801        assert present("EOL")
802
803
804def _test_init(cls, args, kwargs, expected):
805    "Core instance test function for test_init"
806    print("test_init", cls, args, kwargs)
807    try:
808        instance = cls(*args, **kwargs)
809    except Exception as exception:
810        if type(exception) == expected:
811            return
812        logging.error("expected %s, got %s", expected, repr(exception))
813        raise
814    if isinstance(expected, type) and issubclass(expected, Exception):
815        raise AssertionError("No exception raised; "
816        "expected %s for %s/%s" % (
817            expected.__name__,
818            repr(args),
819            repr(kwargs)))
820    for attr_name, attr_value in expected.items():
821        assert getattr(instance, attr_name) == attr_value
822
823
824class TestCookie(object):
825    """Tests for the Cookie class.
826    """
827    # Test cases exercising different constructor calls to make a new Cookie
828    # from scratch. Each case is tuple:
829    # args, kwargs, exception or dict of expected attribute values
830    # this exercises the default validators as well.
831    creation_cases = [
832            # bad call gives TypeError
833            (("foo",), {}, TypeError),
834            (("a", "b", "c"), {}, TypeError),
835            # give un-ascii-able name - raises error due to likely
836            # compatibility problems (cookie ignored, etc.)
837            # in value it's fine, it'll be encoded and not inspected anyway.
838            (("ăŊĻ", "b"), {}, InvalidCookieError),
839            (("b", "ăŊĻ"), {}, {'name': 'b', 'value': "ăŊĻ"}),
840            # normal simple construction gives name and value
841            (("foo", "bar"), {}, {'name': 'foo', 'value': 'bar'}),
842            # add a valid attribute and get it set
843            (("baz", "bam"), {'max_age': 9},
844                {'name': 'baz', 'value': 'bam', 'max_age': 9}),
845            # multiple valid attributes
846            (("x", "y"), {'max_age': 9, 'comment': 'fruity'},
847                {'name': 'x', 'value': 'y',
848                 'max_age': 9, 'comment': 'fruity'}),
849            # invalid max-age
850            (("w", "m"), {'max_age': 'loopy'}, InvalidCookieAttributeError),
851            (("w", "m"), {'max_age': -1}, InvalidCookieAttributeError),
852            (("w", "m"), {'max_age': 1.2}, InvalidCookieAttributeError),
853            # invalid expires
854            (("w", "m"), {'expires': 0}, InvalidCookieAttributeError),
855            (("w", "m"), {'expires':
856                datetime(2010, 1, 1, tzinfo=FixedOffsetTz(600))},
857                InvalidCookieAttributeError),
858            # control: valid expires
859            (("w", "m"),
860                {'expires': datetime(2010, 1, 1)},
861                {'expires': datetime(2010, 1, 1)}),
862            # invalid domain
863            (("w", "m"), {'domain': ''}, InvalidCookieAttributeError),
864            (("w", "m"), {'domain': '@'}, InvalidCookieAttributeError),
865            (("w", "m"), {'domain': '.foo.net'}, {'domain': '.foo.net'}),
866            # control: valid domain
867            (("w", "m"),
868                {'domain': 'foo.net'},
869                {'domain': 'foo.net'},),
870            # invalid path
871            (("w", "m"), {'path': ''}, InvalidCookieAttributeError),
872            (("w", "m"), {'path': '""'}, InvalidCookieAttributeError),
873            (("w", "m"), {'path': 'foo'}, InvalidCookieAttributeError),
874            (("w", "m"), {'path': '"/foo"'}, InvalidCookieAttributeError),
875            (("w", "m"), {'path': ' /foo  '}, InvalidCookieAttributeError),
876            # control: valid path
877            (("w", "m"), {'path': '/'},
878                    {'path': '/'}),
879            (("w", "m"), {'path': '/axes'},
880                    {'path': '/axes'}),
881            # invalid version per RFC 2109/RFC 2965
882            (("w", "m"), {'version': ''}, InvalidCookieAttributeError),
883            (("w", "m"), {'version': 'baa'}, InvalidCookieAttributeError),
884            (("w", "m"), {'version': -2}, InvalidCookieAttributeError),
885            (("w", "m"), {'version': 2.3}, InvalidCookieAttributeError),
886            # control: valid version
887            (("w", "m"), {'version': 0}, {'version': 0}),
888            (("w", "m"), {'version': 1}, {'version': 1}),
889            (("w", "m"), {'version': 3042}, {'version': 3042}),
890            # invalid secure, httponly
891            (("w", "m"), {'secure': ''}, InvalidCookieAttributeError),
892            (("w", "m"), {'secure': 0}, InvalidCookieAttributeError),
893            (("w", "m"), {'secure': 1}, InvalidCookieAttributeError),
894            (("w", "m"), {'secure': 'a'}, InvalidCookieAttributeError),
895            (("w", "m"), {'httponly': ''}, InvalidCookieAttributeError),
896            (("w", "m"), {'httponly': 0}, InvalidCookieAttributeError),
897            (("w", "m"), {'httponly': 1}, InvalidCookieAttributeError),
898            (("w", "m"), {'httponly': 'a'}, InvalidCookieAttributeError),
899            # valid comment
900            (("w", "m"), {'comment': 'a'}, {'comment': 'a'}),
901            # invalid names
902            # (unicode cases are done last because they mess with pytest print)
903            ((None, "m"), {}, InvalidCookieError),
904            (("", "m"), {}, InvalidCookieError),
905            (("ü", "m"), {}, InvalidCookieError),
906            # invalid values
907            (("w", None), {}, {'name': 'w'}),
908            # a control - unicode is valid value, just gets encoded on way out
909            (("w", "üm"), {}, {'value': "üm"}),
910            # comma
911            (('a', ','), {}, {'value': ','}),
912            # semicolons
913            (('a', ';'), {}, {'value': ';'}),
914            # spaces
915            (('a', ' '), {}, {'value': ' '}),
916            ]
917
918    def test_init(self):
919        """Exercise __init__ and validators.
920
921        This is important both because it is a user-facing API, and also
922        because the parse/render tests depend heavily on it.
923        """
924        creation_cases = self.creation_cases + [
925            (("a", "b"), {'frob': 10}, InvalidCookieAttributeError)
926            ]
927        counter = 0
928        for args, kwargs, expected in creation_cases:
929            counter += 1
930            logging.error("counter %d, %s, %s, %s", counter, args, kwargs,
931                    expected)
932            _test_init(Cookie, args, kwargs, expected)
933
934    def test_set_attributes(self):
935        """Exercise setting, validation and getting of attributes without
936        much involving __init__. Also sets value and name.
937        """
938        for args, kwargs, expected in self.creation_cases:
939            if not kwargs:
940                continue
941            try:
942                cookie = Cookie("yarp", "flam")
943                for attr, value in kwargs.items():
944                    setattr(cookie, attr, value)
945                if args:
946                    cookie.name = args[0]
947                    cookie.value = args[1]
948            except Exception as e:
949                if type(e) == expected:
950                    continue
951                raise
952            if isinstance(expected, type) and issubclass(expected, Exception):
953                raise AssertionError("No exception raised; "
954                "expected %s for %s" % (
955                    expected.__name__,
956                    repr(kwargs)))
957            for attr_name, attr_value in expected.items():
958                assert getattr(cookie, attr_name) == attr_value
959
960    def test_get_defaults(self):
961        "Test that defaults are right for cookie attrs"
962        cookie = Cookie("foo", "bar")
963        for attr in (
964                "expires",
965                "max_age",
966                "domain",
967                "path",
968                "comment",
969                "version",
970                "secure",
971                "httponly"):
972            assert hasattr(cookie, attr)
973            assert getattr(cookie, attr) == None
974        # Verify that not every name is getting something
975        for attr in ("foo", "bar", "baz"):
976            assert not hasattr(cookie, attr)
977            with raises(AttributeError):
978                getattr(cookie, attr)
979
980    names_values = [
981        ("a", "b"),
982        ("foo", "bar"),
983        ("baz", "1234567890"),
984        ("!!#po99!", "blah"),
985        ("^_~`*", "foo"),
986        ("%s+|-.&$", "snah"),
987        ("lub", "!@#$%^&*()[]{}|/:'<>~.?`"),
988        ("woah", "====+-_"),
989        ]
990
991    def test_render_response(self):
992        "Test rendering Cookie object for Set-Cookie: header"
993        for name, value in self.names_values:
994            cookie = Cookie(name, value)
995            expected = "{name}={value}".format(
996                    name=name, value=value)
997            assert cookie.render_response() == expected
998        for data, result in [
999                ({'name': 'a', 'value': 'b'}, "a=b"),
1000                ({'name': 'foo', 'value': 'bar'}, "foo=bar"),
1001                ({'name': 'baz', 'value': 'bam'}, "baz=bam"),
1002                ({'name': 'baz', 'value': 'bam', 'max_age': 2},
1003                    "baz=bam; Max-Age=2"),
1004                ({'name': 'baz', 'value': 'bam',
1005                    'max_age': 2, 'comment': 'foobarbaz'},
1006                    "baz=bam; Max-Age=2; Comment=foobarbaz"),
1007                ({'name': 'baz', 'value': 'bam',
1008                    'max_age': 2,
1009                    'expires': datetime(1970, 1, 1),
1010                    },
1011                    "baz=bam; Max-Age=2; "
1012                    "Expires=Thu, 01 Jan 1970 00:00:00 GMT"),
1013                ({'name': 'baz', 'value': 'bam', 'path': '/yams',
1014                    'domain': '3Com.COM'},
1015                    "baz=bam; Domain=3Com.COM; Path=/yams"),
1016                ({'name': 'baz', 'value': 'bam', 'path': '/', 'secure': True,
1017                    'httponly': True},
1018                    "baz=bam; Path=/; Secure; HttpOnly"),
1019                ({'name': 'baz', 'value': 'bam', 'domain': '.domain'},
1020                    'baz=bam; Domain=domain'),
1021                ]:
1022            cookie = Cookie(**data)
1023            actual = sorted(cookie.render_response().split("; "))
1024            ideal = sorted(result.split("; "))
1025            assert actual == ideal
1026
1027    def test_render_encode(self):
1028        """Test encoding of a few special characters.
1029
1030        as in http://bugs.python.org/issue9824
1031        """
1032        cases = {
1033                ("x", "foo,bar;baz"): 'x=foo%2Cbar%3Bbaz',
1034                ("y", 'yap"bip'): 'y=yap%22bip',
1035                }
1036        for args, ideal in cases.items():
1037            cookie = Cookie(*args)
1038            assert cookie.render_response() == ideal
1039            assert cookie.render_request() == ideal
1040
1041    def test_legacy_quotes(self):
1042        """Check that cookies which delimit values with quotes are understood
1043        but that this non-6265 behavior is not repeated in the output
1044        """
1045        cookie = Cookie.from_string(
1046                'Set-Cookie: y="foo"; version="1"; Path="/foo"')
1047        assert cookie.name == 'y'
1048        assert cookie.value == 'foo'
1049        assert cookie.version == 1
1050        assert cookie.path == "/foo"
1051        pieces = cookie.render_response().split("; ")
1052        assert pieces[0] == 'y=foo'
1053        assert set(pieces[1:]) == set([
1054            'Path=/foo', 'Version=1'
1055            ])
1056
1057    def test_render_response_expires(self):
1058        "Simple spot check of cookie expires rendering"
1059        a = Cookie('a', 'blah')
1060        a.expires = parse_date("Wed, 23-Jan-1992 00:01:02 GMT")
1061        assert a.render_response() == \
1062                'a=blah; Expires=Thu, 23 Jan 1992 00:01:02 GMT'
1063
1064        b = Cookie('b', 'blr')
1065        b.expires = parse_date("Sun Nov  6 08:49:37 1994")
1066        assert b.render_response() == \
1067                'b=blr; Expires=Sun, 06 Nov 1994 08:49:37 GMT'
1068
1069    def test_eq(self):
1070        "Smoke test equality/inequality with Cookie objects"
1071        ref = Cookie('a', 'b')
1072        # trivial cases
1073        assert ref == ref
1074        assert not (ref != ref)
1075        assert None != ref
1076        assert not (None == ref)
1077        assert ref != None
1078        assert not (ref == None)
1079        # equivalence and nonequivalence
1080        assert Cookie('a', 'b') is not ref
1081        assert Cookie('a', 'b') == ref
1082        assert Cookie('x', 'y') != ref
1083        assert Cookie('a', 'y') != ref
1084        assert Cookie('a', 'b', path='/') != ref
1085        assert {'c': 'd'} != ref
1086        assert ref != {'c': 'd'}
1087        # unlike attribute values and sets of attributes
1088        assert Cookie('a', 'b', path='/a') \
1089                != Cookie('a', 'b', path='/')
1090        assert Cookie('x', 'y', max_age=3) != \
1091                Cookie('x', 'y', path='/b')
1092        assert Cookie('yargo', 'z', max_age=5) != \
1093                Cookie('yargo', 'z', max_age=6)
1094        assert ref != Cookie('a', 'b', domain='yab')
1095        # Exercise bytes conversion
1096        assert Cookie(b'a', 'b') == Cookie('a', 'b')
1097        assert Cookie(b'a', 'b') == Cookie(b'a', 'b')
1098
1099    def test_manifest(self):
1100        "Test presence of important stuff on Cookie class"
1101        for name in ("attribute_names", "attribute_renderers",
1102                "attribute_parsers", "attribute_validators"):
1103            dictionary = getattr(Cookie, name)
1104            assert dictionary
1105            assert isinstance(dictionary, dict)
1106
1107    def test_simple_extension(self):
1108        "Trivial example/smoke test of extending Cookie"
1109
1110        count_state = [0]
1111
1112        def call_counter(item=None):
1113            count_state[0] += 1
1114            return True if item else False
1115
1116        class Cookie2(Cookie):
1117            "Example Cookie subclass with new behavior"
1118            attribute_names = {
1119                    'foo': 'Foo',
1120                    'bar': 'Bar',
1121                    'baz': 'Baz',
1122                    'ram': 'Ram',
1123                    }
1124            attribute_parsers = {
1125                    'foo': lambda s: "/".join(s),
1126                    'bar': call_counter,
1127                    'value': lambda s:
1128                             parse_value(s, allow_spaces=True),
1129                    }
1130            attribute_validators = {
1131                    'foo': lambda item: True,
1132                    'bar': call_counter,
1133                    'baz': lambda item: False,
1134                    }
1135            attribute_renderers = {
1136                    'foo': lambda s: "|".join(s) if s else None,
1137                    'bar': call_counter,
1138                    'name': lambda item: item,
1139                    }
1140        cookie = Cookie2("a", "b")
1141        for key in Cookie2.attribute_names:
1142            assert hasattr(cookie, key)
1143            assert getattr(cookie, key) == None
1144        cookie.foo = "abc"
1145        assert cookie.render_request() == "a=b"
1146        assert cookie.render_response() == "a=b; Foo=a|b|c"
1147        cookie.foo = None
1148        # Setting it to None makes it drop from the listing
1149        assert cookie.render_response() == "a=b"
1150
1151        cookie.bar = "what"
1152        assert cookie.bar == "what"
1153        assert cookie.render_request() == "a=b"
1154        # bar's renderer returns a bool; if it's True we get Bar.
1155        # that's a special case for flags like HttpOnly.
1156        assert cookie.render_response() == "a=b; Bar"
1157
1158        with raises(InvalidCookieAttributeError):
1159            cookie.baz = "anything"
1160
1161        Cookie2('a', 'b fog')
1162        Cookie2('a', ' b=fo g')
1163
1164    def test_from_string(self):
1165        with raises(InvalidCookieError):
1166            Cookie.from_string("")
1167        with raises(InvalidCookieError):
1168            Cookie.from_string("", ignore_bad_attributes=True)
1169        assert Cookie.from_string("", ignore_bad_cookies=True) == None
1170
1171    def test_from_dict(self):
1172        assert Cookie.from_dict({'name': 'a', 'value': 'b'}) == \
1173                Cookie('a', 'b')
1174        assert Cookie.from_dict(
1175                {'name': 'a', 'value': 'b', 'duh': 'no'},
1176                ignore_bad_attributes=True) == \
1177                Cookie('a', 'b')
1178        with raises(InvalidCookieError):
1179            Cookie.from_dict({}, ignore_bad_attributes=True)
1180        with raises(InvalidCookieError):
1181            Cookie.from_dict({}, ignore_bad_attributes=False)
1182        with raises(InvalidCookieError):
1183            Cookie.from_dict({'name': ''}, ignore_bad_attributes=False)
1184        with raises(InvalidCookieError):
1185            Cookie.from_dict({'name': None, 'value': 'b'},
1186                    ignore_bad_attributes=False)
1187        assert Cookie.from_dict({'name': 'foo'}) == Cookie('foo', None)
1188        assert Cookie.from_dict({'name': 'foo', 'value': ''}) == \
1189                Cookie('foo', None)
1190        with raises(InvalidCookieAttributeError):
1191            assert Cookie.from_dict(
1192                    {'name': 'a', 'value': 'b', 'duh': 'no'},
1193                    ignore_bad_attributes=False)
1194        assert Cookie.from_dict({'name': 'a', 'value': 'b', 'expires': 2},
1195            ignore_bad_attributes=True) == Cookie('a', 'b')
1196        with raises(InvalidCookieAttributeError):
1197            assert Cookie.from_dict({'name': 'a', 'value': 'b', 'expires': 2},
1198                ignore_bad_attributes=False)
1199
1200
1201class Scone(object):
1202    """Non-useful alternative to Cookie class for tests only.
1203    """
1204    def __init__(self, name, value):
1205        self.name = name
1206        self.value = value
1207
1208    @classmethod
1209    def from_dict(cls, cookie_dict):
1210        instance = cls(cookie_dict['name'], cookie_dict['value'])
1211        return instance
1212
1213    def __eq__(self, other):
1214        if type(self) != type(other):
1215            return False
1216        if self.name != other.name:
1217            return False
1218        if self.value != other.value:
1219            return False
1220        return True
1221
1222
1223class Scones(Cookies):
1224    """Non-useful alternative to Cookies class for tests only.
1225    """
1226    DEFAULT_COOKIE_CLASS = Scone
1227
1228
1229class TestCookies(object):
1230    """Tests for the Cookies class.
1231    """
1232    creation_cases = [
1233            # Only args - simple
1234            ((Cookie("a", "b"),), {}, 1),
1235            # Only kwargs - simple
1236            (tuple(), {'a': 'b'}, 1),
1237            # Only kwargs - bigger
1238            (tuple(),
1239                {'axl': 'bosk',
1240                 'x': 'y',
1241                 'foo': 'bar',
1242                 'baz': 'bam'}, 4),
1243            # Sum between args/kwargs
1244            ((Cookie('a', 'b'),),
1245                {'axl': 'bosk',
1246                 'x': 'y',
1247                 'foo': 'bar',
1248                 'baz': 'bam'}, 5),
1249            # Redundant between args/kwargs
1250            ((Cookie('a', 'b'),
1251              Cookie('x', 'y')),
1252                {'axl': 'bosk',
1253                 'x': 'y',
1254                 'foo': 'bar',
1255                 'baz': 'bam'}, 5),
1256            ]
1257
1258    def test_init(self):
1259        """Create some Cookies objects with __init__, varying the constructor
1260        arguments, and check on the results.
1261
1262        Exercises __init__, __repr__, render_request, render_response, and
1263        simple cases of parse_response and parse_request.
1264        """
1265        def same(a, b):
1266            keys = sorted(set(a.keys() + b.keys()))
1267            for key in keys:
1268                assert a[key] == b[key]
1269
1270        for args, kwargs, length in self.creation_cases:
1271            # Make a Cookies object using the args.
1272            cookies = Cookies(*args, **kwargs)
1273            assert len(cookies) == length
1274
1275            # Render into various text formats.
1276            rep = repr(cookies)
1277            res = cookies.render_response()
1278            req = cookies.render_request()
1279
1280            # Very basic sanity check on renders, fail fast and in a simple way
1281            # if output is truly terrible
1282            assert rep.count('=') == length
1283            assert len(res) == length
1284            assert [item.count('=') == 1 for item in res]
1285            assert req.count('=') == length
1286            assert len(req.split(";")) == length
1287
1288            # Explicitly parse out the data (this can be simple since the
1289            # output should be in a highly consistent format)
1290            pairs = [item.split("=") for item in req.split("; ")]
1291            assert len(pairs) == length
1292            for name, value in pairs:
1293                cookie = cookies[name]
1294                assert cookie.name == name
1295                assert cookie.value == value
1296
1297            # Parse the rendered output, check that result is equal to the
1298            # originally produced object.
1299
1300            parsed = Cookies()
1301            parsed.parse_request(req)
1302            assert parsed == cookies
1303
1304            parsed = Cookies()
1305            for item in res:
1306                parsed.parse_response(item)
1307            assert parsed == cookies
1308
1309            # Check that all the requested cookies were created correctly:
1310            # indexed with correct names in dict, also with correctly set name
1311            # and value attributes.
1312            for cookie in args:
1313                assert cookies[cookie.name] == cookie
1314            for name, value in kwargs.items():
1315                cookie = cookies[name]
1316                assert cookie.name == name
1317                assert cookie.value == value
1318                assert name in rep
1319                assert value in rep
1320
1321            # Spot check that setting an attribute still works
1322            # with these particular parameters. Not a torture test.
1323            for key in cookies:
1324                cookies[key].max_age = 42
1325            for line in cookies.render_response():
1326                assert line.endswith("Max-Age=42")
1327
1328            # Spot check attribute deletion
1329            assert cookies[key].max_age
1330            del cookies[key].max_age
1331            assert cookies[key].max_age is None
1332
1333            # Spot check cookie deletion
1334            keys = [key for key in cookies.keys()]
1335            for key in keys:
1336                del cookies[key]
1337                assert key not in cookies
1338
1339    def test_eq(self):
1340        "Smoke test equality/inequality of Cookies objects"
1341        ref = Cookies(a='b')
1342        assert Cookies(a='b') == ref
1343        assert Cookies(b='c') != ref
1344        assert ref != Cookies(d='e')
1345        assert Cookies(a='x') != ref
1346
1347        class Dummy(object):
1348            "Just any old object"
1349            pass
1350        x = Dummy()
1351        x.keys = True
1352        with raises(TypeError):
1353            assert ref != x
1354
1355    def test_add(self):
1356        "Test the Cookies.add method"
1357        for args, kwargs, length in self.creation_cases:
1358            cookies = Cookies()
1359            cookies.add(*args, **kwargs)
1360            assert len(cookies) == length
1361            for cookie in args:
1362                assert cookies[cookie.name] == cookie
1363            for name, value in kwargs.items():
1364                cookie = cookies[name]
1365                assert cookie.value == value
1366            count = len(cookies)
1367            assert 'w' not in cookies
1368            cookies.add(w='m')
1369            assert 'w' in cookies
1370            assert count == len(cookies) - 1
1371            assert cookies['w'].value == 'm'
1372
1373    def test_empty(self):
1374        "Trivial test of behavior of empty Cookies object"
1375        cookies = Cookies()
1376        assert len(cookies) == 0
1377        assert Cookies() == cookies
1378
1379    def test_parse_request(self):
1380        """Test Cookies.parse_request.
1381        """
1382        def run(arg, **kwargs):
1383            "run Cookies.parse_request on an instance"
1384            cookies = Cookies()
1385            result = runner(cookies.parse_request)(arg, **kwargs)
1386            return result
1387
1388        for i, case in enumerate(HEADER_CASES):
1389            arg, kwargs, expected, response_result = case
1390
1391            # parse_request doesn't take ignore_bad_attributes. remove it
1392            # without changing original kwargs for further tests
1393            kwargs = kwargs.copy()
1394            if 'ignore_bad_attributes' in kwargs:
1395                del kwargs['ignore_bad_attributes']
1396
1397            def expect(arg, kwargs):
1398                "repeated complex assertion"
1399                result = run(arg, **kwargs)
1400                assert result == expected \
1401                       or isinstance(expected, type) \
1402                       and type(result) == expected, \
1403                        "unexpected result for (%s): %s. should be %s" \
1404                        % (repr(arg), repr(result), repr(expected))
1405
1406            # Check result - should be same with and without the prefix
1407            expect("Cookie: " + arg, kwargs)
1408            expect(arg, kwargs)
1409
1410            # But it should not match with the response prefix.
1411            other_result = run("Set-Cookie: " + arg, **kwargs)
1412            assert other_result != expected
1413            assert other_result != response_result
1414
1415            # If case expects InvalidCookieError, verify that it is suppressed
1416            # by ignore_bad_cookies.
1417            if expected == InvalidCookieError:
1418                kwargs2 = kwargs.copy()
1419                kwargs2['ignore_bad_cookies'] = True
1420                cookies = Cookies()
1421                # Let natural exception raise, easier to figure out
1422                cookies.parse_request(arg, **kwargs2)
1423
1424        # Spot check that exception is raised for clearly wrong format
1425        assert not isinstance(run("Cookie: a=b"), InvalidCookieError)
1426        assert isinstance(run("Set-Cookie: a=b"), InvalidCookieError)
1427
1428    def test_parse_response(self):
1429        """Test Cookies.parse_response.
1430        """
1431        def run(arg, **kwargs):
1432            "run parse_response method of a Cookies instance"
1433            cookies = Cookies()
1434            return runner(cookies.parse_response)(arg, **kwargs)
1435
1436        for case in HEADER_CASES:
1437            arg, kwargs, request_result, expected = case
1438            # If we expect InvalidCookieError or InvalidCookieAttributeError,
1439            # telling the function to ignore those should result in no
1440            # exception.
1441            kwargs2 = kwargs.copy()
1442            if expected == InvalidCookieError:
1443                kwargs2['ignore_bad_cookies'] = True
1444                assert not isinstance(
1445                        run(arg, **kwargs2),
1446                        Exception)
1447            elif expected == InvalidCookieAttributeError:
1448                kwargs2['ignore_bad_attributes'] = True
1449                result = run(arg, **kwargs2)
1450                if isinstance(result, InvalidCookieAttributeError):
1451                    raise AssertionError("InvalidCookieAttributeError "
1452                        "should have been silenced/logged")
1453                else:
1454                    assert not isinstance(result, Exception)
1455            # Check result - should be same with and without the prefix
1456            sys.stdout.flush()
1457            result = run(arg, **kwargs)
1458            assert result == expected \
1459                    or isinstance(expected, type) \
1460                    and type(result) == expected, \
1461                    "unexpected result for (%s): %s. should be %s" \
1462                    % (repr(arg), repr(result), repr(expected))
1463            result = run("Set-Cookie: " + arg, **kwargs)
1464            assert result == expected \
1465                    or isinstance(expected, type) \
1466                    and type(result) == expected, \
1467                    "unexpected result for (%s): %s. should be %s" \
1468                    % (repr("Set-Cookie: " + arg),
1469                       repr(result), repr(expected))
1470            # But it should not match with the request prefix.
1471            other_result = run("Cookie: " + arg, **kwargs)
1472            assert other_result != expected
1473            assert other_result != request_result
1474
1475        assert not isinstance(run("Set-Cookie: a=b"), InvalidCookieError)
1476        assert isinstance(run("Cookie: a=b"), InvalidCookieError)
1477
1478    def test_exercise_parse_one_response_asctime(self):
1479        asctime = 'Sun Nov  6 08:49:37 1994'
1480        line = "Set-Cookie: a=b; Expires=%s" % asctime
1481        response_dict = parse_one_response(line)
1482        assert response_dict == \
1483            {'expires': 'Sun Nov  6 08:49:37 1994', 'name': 'a', 'value': 'b'}
1484        assert Cookie.from_dict(response_dict) == \
1485                Cookie('a', 'b', expires=parse_date(asctime))
1486
1487    def test_get_all(self):
1488        cookies = Cookies.from_request('a=b; a=c; b=x')
1489        assert cookies['a'].value == 'b'
1490        assert cookies['b'].value == 'x'
1491        values = [cookie.value for cookie in cookies.get_all('a')]
1492        assert values == ['b', 'c']
1493
1494    def test_custom_cookie_class_on_instance(self):
1495        cookies = Cookies(_cookie_class=Scone)
1496        cookies.add(a="b")
1497        assert cookies['a'] == Scone("a", "b")
1498
1499    def test_custom_cookie_class_on_subclass(self):
1500        cookies = Scones()
1501        cookies.add(a="b")
1502        assert cookies['a'] == Scone("a", "b")
1503
1504    def test_custom_cookie_class_on_instance_parse_request(self):
1505        cookies = Scones()
1506        cookies.parse_request("Cookie: c=d")
1507        assert cookies['c'] == Scone("c", "d")
1508
1509    def test_custom_cookie_class_on_instance_parse_response(self):
1510        cookies = Scones()
1511        cookies.parse_response("Set-Cookie: c=d")
1512        assert cookies['c'] == Scone("c", "d")
1513
1514
1515def test_parse_date():
1516    """Throw a ton of dirty samples at the date parse/render and verify the
1517    exact output of rendering the parsed version of the sample.
1518    """
1519    cases = [
1520        # Obviously off format
1521        ("", None),
1522        ("    ", None),
1523        ("\t", None),
1524        ("\n", None),
1525        ("\x02\x03\x04", None),
1526        ("froppity", None),
1527        ("@@@@@%@#:%", None),
1528        ("foo bar baz", None),
1529        # We'll do a number of overall manglings.
1530        # First, show that the baseline passes
1531        ("Sat, 10 Oct 2009 13:47:21 GMT", "Sat, 10 Oct 2009 13:47:21 GMT"),
1532        # Delete semantically important pieces
1533        (" Oct 2009 13:47:21 GMT", None),
1534        ("Fri, Oct 2009 13:47:21 GMT", None),
1535        ("Fri, 10 2009 13:47:21 GMT", None),
1536        ("Sat, 10 Oct 2009 :47:21 GMT", None),
1537        ("Sat, 10 Oct 2009 13::21 GMT", None),
1538        ("Sat, 10 Oct 2009 13:47: GMT", None),
1539        # Replace single characters out of tokens with spaces - harder to
1540        # do programmatically because some whitespace can reasonably be
1541        # tolerated.
1542        ("F i, 10 Oct 2009 13:47:21 GMT", None),
1543        ("Fr , 10 Oct 2009 13:47:21 GMT", None),
1544        ("Fri, 10  ct 2009 13:47:21 GMT", None),
1545        ("Fri, 10 O t 2009 13:47:21 GMT", None),
1546        ("Fri, 10 Oc  2009 13:47:21 GMT", None),
1547        ("Sat, 10 Oct  009 13:47:21 GMT", None),
1548        ("Sat, 10 Oct 2 09 13:47:21 GMT", None),
1549        ("Sat, 10 Oct 20 9 13:47:21 GMT", None),
1550        ("Sat, 10 Oct 200  13:47:21 GMT", None),
1551        ("Sat, 10 Oct 2009 1 :47:21 GMT", None),
1552        ("Sat, 10 Oct 2009 13 47:21 GMT", None),
1553        ("Sat, 10 Oct 2009 13: 7:21 GMT", None),
1554        ("Sat, 10 Oct 2009 13:4 :21 GMT", None),
1555        ("Sat, 10 Oct 2009 13:47 21 GMT", None),
1556        ("Sat, 10 Oct 2009 13:47: 1 GMT", None),
1557        ("Sat, 10 Oct 2009 13:47:2  GMT", None),
1558        ("Sat, 10 Oct 2009 13:47:21  MT", None),
1559        ("Sat, 10 Oct 2009 13:47:21 G T", None),
1560        ("Sat, 10 Oct 2009 13:47:21 GM ", None),
1561        # Replace numeric elements with stuff that contains A-Z
1562        ("Fri, Burp Oct 2009 13:47:21 GMT", None),
1563        ("Fri, 10 Tabalqplar 2009 13:47:21 GMT", None),
1564        ("Sat, 10 Oct Fruit 13:47:21 GMT", None),
1565        ("Sat, 10 Oct 2009 13:47:21 Fruits", None),
1566        # Weekday
1567        (", Dec 31 00:00:00 2003", None),
1568        ("T, Dec 31 00:00:00 2003", None),
1569        ("Tu, Dec 31 00:00:00 2003", None),
1570        ("Hi, Dec 31 00:00:00 2003", None),
1571        ("Heretounforeseen, Dec 31 00:00:00 2003", None),
1572        ("Wednesday2, Dec 31 00:00:00 2003", None),
1573        ("Mon\x00frobs, Dec 31 00:00:00 2003", None),
1574        ("Mon\x10day, Dec 31 00:00:00 2003", None),
1575        # Day of month
1576        ("Fri, Oct 2009 13:47:21 GMT", None),
1577        ("Fri, 110 Oct 2009 13:47:21 GMT", None),
1578        ("Fri, 0 Oct 2009 13:47:21 GMT", None),
1579        ("Fri, 00 Oct 2009 13:47:21 GMT", None),
1580        ("Fri, 0  Oct 2009 13:47:21 GMT", None),
1581        ("Fri,  0 Oct 2009 13:47:21 GMT", None),
1582        ("Fri, 00 Oct 2009 13:47:21 GMT", None),
1583        ("Fri, 33 Oct 2009 13:47:21 GMT", None),
1584        ("Fri, 40 Oct 2009 13:47:21 GMT", None),
1585        ("Fri, A2 Oct 2009 13:47:21 GMT", None),
1586        ("Fri, 2\x00 Oct 2009 13:47:21 GMT", None),
1587        ("Fri, \t3 Oct 2009 13:47:21 GMT", None),
1588        ("Fri, 3\t Oct 2009 13:47:21 GMT", None),
1589        # Month
1590        ("Fri, 10  2009 13:47:21 GMT", None),
1591        ("Fri, 10 O 2009 13:47:21 GMT", None),
1592        ("Fri, 10 Oc 2009 13:47:21 GMT", None),
1593        ("Sat, 10 Octuarial 2009 13:47:21 GMT", None),
1594        ("Sat, 10 Octuary 2009 13:47:21 GMT", None),
1595        ("Sat, 10 Octubre 2009 13:47:21 GMT", None),
1596        # Year
1597        ("Sat, 10 Oct 009 13:47:21 GMT", None),
1598        ("Sat, 10 Oct 200 13:47:21 GMT", None),
1599        ("Sat, 10 Oct 209 13:47:21 GMT", None),
1600        ("Sat, 10 Oct 20 9 13:47:21 GMT", None),
1601        # Hour
1602        ("Sat, 10 Oct 2009 25:47:21 GMT", None),
1603        ("Sat, 10 Oct 2009 1@:47:21 GMT", None),
1604        # Minute
1605        ("Sat, 10 Oct 2009 13:71:21 GMT", None),
1606        ("Sat, 10 Oct 2009 13:61:21 GMT", None),
1607        ("Sat, 10 Oct 2009 13:60:21 GMT", None),
1608        ("Sat, 10 Oct 2009 24:01:00 GMT", None),
1609        # Second
1610        ("Sat, 10 Oct 2009 13:47 GMT", "Sat, 10 Oct 2009 13:47:00 GMT"),
1611        ("Sat, 10 Oct 2009 13:47:00 GMT", "Sat, 10 Oct 2009 13:47:00 GMT"),
1612        ("Sat, 10 Oct 2009 24:00:01 GMT", None),
1613        # Some reasonable cases (ignore weekday)
1614        ("Mon Dec 24 16:32:39 1977 GMT", "Sat, 24 Dec 1977 16:32:39 GMT"),
1615        ("Sat, 7 Dec 1991 13:56:05 GMT", "Sat, 07 Dec 1991 13:56:05 GMT"),
1616        ("Saturday, 8-Mar-2012 21:35:09 GMT", "Thu, 08 Mar 2012 21:35:09 GMT"),
1617        ("Sun, 1-Feb-1998 00:00:00 GMT", "Sun, 01 Feb 1998 00:00:00 GMT"),
1618        ("Thursday, 01-Jan-1983 01:01:01 GMT",
1619                "Sat, 01 Jan 1983 01:01:01 GMT"),
1620        ("Tue, 15-Nov-1973 22:23:24 GMT", "Thu, 15 Nov 1973 22:23:24 GMT"),
1621        ("Wed, 09 Dec 1999 23:59:59 GMT", "Thu, 09 Dec 1999 23:59:59 GMT"),
1622        ("Mon, 12-May-05 20:25:03 GMT", "Thu, 12 May 2005 20:25:03 GMT"),
1623        ("Thursday, 01-Jan-12 09:00:00 GMT", "Sun, 01 Jan 2012 09:00:00 GMT"),
1624        # starts like asctime, but flips the time and year - nonsense
1625        ("Wed Mar 12 2007 08:25:07 GMT", None),
1626        # starts like RFC 1123, but flips the time and year - nonsense
1627        ("Thu, 31 Dec 23:55:55 2107 GMT", None),
1628        ('Fri, 21-May-2004 10:40:51 GMT', "Fri, 21 May 2004 10:40:51 GMT"),
1629        # extra 2-digit year exercises
1630        ("Sat, 10 Oct 11 13:47:21 GMT", "Mon, 10 Oct 2011 13:47:21 GMT"),
1631        ("Sat, 10 Oct 09 13:47:22 GMT", "Sat, 10 Oct 2009 13:47:22 GMT"),
1632        ("Sat, 10 Oct 93 13:47:23 GMT", "Sun, 10 Oct 1993 13:47:23 GMT"),
1633        ("Sat, 10 Oct 85 13:47:24 GMT", "Thu, 10 Oct 1985 13:47:24 GMT"),
1634        ("Sat, 10 Oct 70 13:47:25 GMT", "Sat, 10 Oct 1970 13:47:25 GMT"),
1635        ("Sat, 10 Oct 69 13:47:26 GMT", "Thu, 10 Oct 2069 13:47:26 GMT"),
1636        # dealing with 3-digit year is incredibly tedious, will do as needed
1637        ("Sat, 10 Oct 969 13:47:26 GMT", None),
1638        ("Sat, 10 Oct 9 13:47:26 GMT", None),
1639        ("Fri, 10 Oct 19691 13:47:26 GMT", None),
1640    ]
1641
1642    def change(string, position, new_value):
1643        "Macro to change a string"
1644        return string[:position] + new_value + string[position + 1:]
1645
1646    original = "Sat, 10 Oct 2009 13:47:21 GMT"
1647
1648    # Stuff garbage in every position - none of these characters should
1649    # ever be allowed in a date string.
1650    # not included because pytest chokes: "¿�␦"
1651    bad_chars = "/<>()\\*$#&=;\x00\b\f\n\r\"\'`?"
1652    for pos in range(0, len(original)):
1653        for bad_char in bad_chars:
1654            cases.append((change(original, pos, bad_char), None))
1655
1656    # Invalidate each letter
1657    letter_positions = [i for (i, c) in enumerate(original)  \
1658                        if re.match("[A-Za-z]", c)]
1659    for pos in letter_positions:
1660        cases.append((change(original, pos, 'q'), None))
1661        cases.append((change(original, pos, '0'), None))
1662        cases.append((change(original, pos, '-'), None))
1663        cases.append((change(original, pos, ''), None))
1664        # But do tolerate case changes.
1665        c = original[pos]
1666        if c.isupper():
1667            c = c.lower()
1668        else:
1669            c = c.upper()
1670        cases.append((change(original, pos, c), original))
1671
1672    # Invalidate each digit
1673    digit_positions = [i for (i, c) in enumerate(original) \
1674                       if c in "0123456789"]
1675    for pos in digit_positions:
1676        c = original[pos]
1677        cases.append((change(original, pos, 'q'), None))
1678        cases.append((change(original, pos, '-' + c), None))
1679        cases.append((change(original, pos, '+' + c), None))
1680
1681    # Invalidate each space
1682    space_positions = [i for (i, c) in enumerate(original) \
1683                       if c in " \t\n\r"]
1684    for pos in space_positions:
1685        cases.append((change(original, pos, 'x'), None))
1686        cases.append((change(original, pos, '\t'), None))
1687        cases.append((change(original, pos, '  '), None))
1688        cases.append((change(original, pos, ''), None))
1689
1690    # Invalidate each colon
1691    colon_positions = [i for (i, c) in enumerate(original) \
1692                       if c == ":"]
1693    for pos in colon_positions:
1694        cases.append((change(original, pos, 'z'), None))
1695        cases.append((change(original, pos, '0'), None))
1696        cases.append((change(original, pos, ' '), None))
1697        cases.append((change(original, pos, ''), None))
1698
1699    for data, ideal in cases:
1700        actual = render_date(parse_date(data))
1701        assert actual == ideal
1702
1703
1704def runner(function):
1705    """Generate a function which collects the result/exception from another
1706    function, for easier assertions.
1707    """
1708    def run(*args, **kwargs):
1709        "Function which collects result/exception"
1710        actual_result, actual_exception = None, None
1711        try:
1712            actual_result = function(*args, **kwargs)
1713        except Exception as exception:
1714            actual_exception = exception
1715        return actual_exception or actual_result
1716    return run
1717
1718
1719# Define cases for testing parsing and rendering.
1720# Format: input, kwargs, expected parse_request result, expected parse_response
1721# result.
1722
1723HEADER_CASES = [
1724        # cases with nothing that can be parsed out result in
1725        # InvalidCookieError. unless ignore_bad_cookies=True, then they give an
1726        # empty Cookies().
1727        ("", {},
1728            InvalidCookieError,
1729            InvalidCookieError),
1730        ('a', {},
1731            InvalidCookieError,
1732            InvalidCookieError),
1733        ("    ", {},
1734            InvalidCookieError,
1735            InvalidCookieError),
1736        (";;;;;", {},
1737            InvalidCookieError,
1738            InvalidCookieError),
1739        ("qwejrkqlwjere", {},
1740            InvalidCookieError,
1741            InvalidCookieError),
1742        # vacuous headers should give invalid
1743        ('Cookie: ', {},
1744            InvalidCookieError,
1745            InvalidCookieError),
1746        ('Set-Cookie: ', {},
1747            InvalidCookieError,
1748            InvalidCookieError),
1749        # Single pair should work the same as request or response
1750        ("foo=bar", {},
1751            Cookies(foo='bar'),
1752            Cookies(foo='bar')),
1753        ("SID=242d96421d4e", {},
1754            Cookies(SID='242d96421d4e'),
1755            Cookies(SID='242d96421d4e')),
1756        # Two pairs on SAME line should work with request, fail with response.
1757        # if ignore_bad_attributes, response should not raise.
1758        # and ignore_bad_attributes behavior should be default
1759        ("a=b; c=dx", {'ignore_bad_attributes': True},
1760            Cookies(a='b', c='dx'),
1761            Cookies(a='b')),
1762        ("a=b; c=d", {'ignore_bad_attributes': False},
1763            Cookies(a='b', c='d'),
1764            InvalidCookieAttributeError),
1765        ('g=h;j=k', {},
1766            Cookies(g='h', j='k'),
1767            Cookies(g='h')),
1768        # tolerance: response shouldn't barf on unrecognized attr by default,
1769        # but request should recognize as malformed
1770        ('a=b; brains', {},
1771            InvalidCookieError,
1772            Cookies(a='b')),
1773        # tolerance: should strip quotes and spaces
1774        ('A="BBB"', {},
1775            Cookies(A='BBB'),
1776            Cookies(A='BBB'),
1777            ),
1778        ('A=  "BBB"  ', {},
1779            Cookies(A='BBB'),
1780            Cookies(A='BBB'),
1781            ),
1782        # tolerance: should ignore dumb trailing ;
1783        ('foo=bar;', {},
1784            Cookies(foo='bar'),
1785            Cookies(foo='bar'),
1786            ),
1787        ('A="BBB";', {},
1788            Cookies(A='BBB'),
1789            Cookies(A='BBB'),
1790            ),
1791        ('A=  "BBB"  ;', {},
1792            Cookies(A='BBB'),
1793            Cookies(A='BBB'),
1794            ),
1795        # empty value
1796        ("lang=; Expires=Sun, 06 Nov 1994 08:49:37 GMT", {},
1797            InvalidCookieError,
1798            Cookies(
1799                Cookie('lang', '',
1800                    expires=parse_date(
1801                        "Sun, 06 Nov 1994 08:49:37 GMT")))),
1802        # normal examples of varying complexity
1803        ("frob=varvels; Expires=Wed, 09 Jun 2021 10:18:14 GMT", {},
1804            InvalidCookieError,
1805            Cookies(
1806                Cookie('frob', 'varvels',
1807                    expires=parse_date(
1808                        "Wed, 09 Jun 2021 10:18:14 GMT"
1809                        )))),
1810        ("lang=en-US; Expires=Wed, 03 Jun 2019 10:18:14 GMT", {},
1811            InvalidCookieError,
1812            Cookies(
1813                Cookie('lang', 'en-US',
1814                    expires=parse_date(
1815                        "Wed, 03 Jun 2019 10:18:14 GMT"
1816                        )))),
1817        # easily interpretable as multiple request cookies!
1818        ("CID=39b4d9be4d42; Path=/; Domain=example.com", {},
1819            Cookies(CID="39b4d9be4d42", Path='/', Domain='example.com'),
1820            Cookies(Cookie('CID', '39b4d9be4d42', path='/',
1821                domain='example.com'))),
1822        ("lang=en-US; Path=/; Domain=example.com", {},
1823            Cookies(lang='en-US', Path='/', Domain='example.com'),
1824            Cookies(Cookie('lang', 'en-US',
1825                path='/', domain='example.com'))),
1826        ("foo=bar; path=/; expires=Mon, 04-Dec-2001 12:43:00 GMT", {},
1827            InvalidCookieError,
1828            Cookies(
1829                Cookie('foo', 'bar', path='/',
1830                    expires=parse_date("Mon, 04-Dec-2001 12:43:00 GMT")
1831                ))),
1832        ("SID=0fae49; Path=/; Secure; HttpOnly", {},
1833            InvalidCookieError,
1834            Cookies(Cookie('SID', '0fae49',
1835                path='/', secure=True, httponly=True))),
1836        ('TMID=DQAAXKEaeo_aYp; Domain=mail.nauk.com; '
1837            'Path=/accounts; Expires=Wed, 13-Jan-2021 22:23:01 GMT; '
1838            'Secure; HttpOnly', {},
1839            InvalidCookieError,
1840            Cookies(
1841                Cookie('TMID', 'DQAAXKEaeo_aYp',
1842                    domain='mail.nauk.com',
1843                    path='/accounts', secure=True, httponly=True,
1844                    expires=parse_date("Wed, 13-Jan-2021 22:23:01 GMT")
1845                    ))),
1846        ("test=some_value; expires=Sat, 01-Jan-2000 00:00:00 GMT; "
1847         "path=/;", {},
1848            InvalidCookieError,
1849            Cookies(
1850                Cookie('test', 'some_value', path='/',
1851                    expires=parse_date('Sat, 01 Jan 2000 00:00:00 GMT')
1852                ))),
1853        # From RFC 2109 - accept the lots-of-dquotes style but don't produce.
1854        ('Customer="WILE_E_COYOTE"; Version="1"; Path="/acme"; '
1855         'Part_Number="Rocket_Launcher_0001"', {},
1856            Cookies(Customer='WILE_E_COYOTE', Version='1', Path='/acme',
1857                Part_Number='Rocket_Launcher_0001'),
1858            Cookies(Cookie('Customer', 'WILE_E_COYOTE',
1859                    version=1, path='/acme'))),
1860        # However, we don't honor RFC 2109 type meta-attributes
1861        ('Cookie: $Version="1"; Customer="WILE_E_COYOTE"; $Path="/acme"', {},
1862            InvalidCookieError,
1863            InvalidCookieError),
1864        # degenerate Domain=. is common, so should be handled though invalid
1865        ("lu=Qg3OHJZLehYLjVgAqiZbZbzo; Expires=Tue, 15-Jan-2013 "
1866         "21:47:38 GMT; Path=/; Domain=.foo.com; HttpOnly", {},
1867            InvalidCookieError,
1868            Cookies(Cookie('lu', "Qg3OHJZLehYLjVgAqiZbZbzo",
1869                expires=parse_date('Tue, 15 Jan 2013 21:47:38 GMT'),
1870                path='/', domain='.foo.com', httponly=True,
1871                ))),
1872        ('ZQID=AYBEVnDKrdst; Domain=.nauk.com; Path=/; '
1873         'Expires=Wed, 13-Jan-2021 22:23:01 GMT; HttpOnly', {},
1874            InvalidCookieError,
1875            Cookies(Cookie('ZQID', "AYBEVnDKrdst",
1876                httponly=True, domain='.nauk.com', path='/',
1877                expires=parse_date('Wed, 13 Jan 2021 22:23:01 GMT'),
1878                ))),
1879        ("OMID=Ap4PQQEq; Domain=.nauk.com; Path=/; "
1880            'Expires=Wed, 13-Jan-2021 22:23:01 GMT; Secure; HttpOnly', {},
1881            InvalidCookieError,
1882            Cookies(Cookie('OMID', "Ap4PQQEq",
1883                path='/', domain='.nauk.com', secure=True, httponly=True,
1884                expires=parse_date('Wed, 13 Jan 2021 22:23:01 GMT')
1885                ))),
1886        # question mark in value
1887        ('foo="?foo"; Path=/', {},
1888            Cookies(foo='?foo', Path='/'),
1889            Cookies(Cookie('foo', '?foo', path='/'))),
1890        # unusual format for secure/httponly
1891        ("a=b; Secure=true; HttpOnly=true;", {},
1892            Cookies(a='b', Secure='true', HttpOnly='true'),
1893            Cookies(Cookie('a', 'b', secure=True, httponly=True))),
1894        # invalid per RFC to have spaces in value, but here they are
1895        # URL-encoded by default. Extend the mechanism if this is no good
1896        ('user=RJMmei IORqmD; expires=Wed, 3 Nov 2007 23:20:39 GMT; path=/',
1897            {},
1898            InvalidCookieError,
1899            Cookies(
1900                Cookie('user', 'RJMmei IORqmD', path='/',
1901                    expires=parse_date("Wed, 3 Nov 2007 23:20:39 GMT")))),
1902        # Most characters from 32 to \x31 + 1 should be allowed in values -
1903        # not including space/32, dquote/34, comma/44.
1904        ("x=!#$%&'()*+-./01", {},
1905            Cookies(x="!#$%&'()*+-./01"),
1906            Cookies(x="!#$%&'()*+-./01")),
1907        # don't crash when value wrapped with quotes
1908        # http://bugs.python.org/issue3924
1909        ('a=b; version="1"', {},
1910            Cookies(a='b', version='1'),
1911            Cookies(Cookie('a', 'b', version=1))),
1912        # cookie with name 'expires'. inadvisable, but valid.
1913        # http://bugs.python.org/issue1117339
1914        ('expires=foo', {},
1915            Cookies(expires='foo'),
1916            Cookies(expires='foo')),
1917        # http://bugs.python.org/issue8826
1918        # quick date parsing spot-check, see test_parse_date for a real workout
1919        ('foo=bar; expires=Fri, 31-Dec-2010 23:59:59 GMT', {},
1920            InvalidCookieError,
1921            Cookies(
1922                Cookie('foo', 'bar',
1923                expires=datetime(2010, 12, 31, 23, 59, 59)))),
1924        # allow VALID equals sign in values - not even an issue in RFC 6265 or
1925        # this module, but very helpful for base64 and always worth checking.
1926        # http://bugs.python.org/issue403473
1927        ('a=Zm9vIGJhcg==', {},
1928            Cookies(a='Zm9vIGJhcg=='),
1929            Cookies(a='Zm9vIGJhcg==')),
1930        ('blah="Foo=2"', {},
1931            Cookies(blah='Foo=2'),
1932            Cookies(blah='Foo=2')),
1933        # take the first cookie in request parsing.
1934        # (response parse ignores the second one as a bad attribute)
1935        # http://bugs.python.org/issue1375011
1936        # http://bugs.python.org/issue1372650
1937        # http://bugs.python.org/issue7504
1938        ('foo=33;foo=34', {},
1939            Cookies(foo='33'),
1940            Cookies(foo='33')),
1941        # Colons in names (invalid!), as used by some dumb old Java/PHP code
1942        # http://bugs.python.org/issue2988
1943        # http://bugs.python.org/issue472646
1944        # http://bugs.python.org/issue2193
1945        ('a:b=c', {},
1946            Cookies(
1947                Cookie('a:b', 'c')),
1948            Cookies(
1949                Cookie('a:b', 'c'))),
1950#       # http://bugs.python.org/issue991266
1951#       # This module doesn't do the backslash quoting so this would
1952#       # effectively require allowing all possible characters inside arbitrary
1953#       # attributes, which does not seem reasonable.
1954#       ('foo=bar; Comment="\342\230\243"', {},
1955#           Cookies(foo='bar', Comment='\342\230\243'),
1956#           Cookies(
1957#               Cookie('foo', 'bar', comment='\342\230\243')
1958#               )),
1959        ]
1960
1961
1962def _cheap_request_parse(arg1, arg2):
1963    """Really cheap parse like what client code often does, for
1964    testing request rendering (determining order-insensitively whether two
1965    cookies-as-text are equivalent). 'a=b; x=y' type format
1966    """
1967    def crumble(arg):
1968        "Break down string into pieces"
1969        pieces = [piece.strip('\r\n ;') for piece in re.split("(\r\n|;)", arg)]
1970        pieces = [piece for piece in pieces if piece and '=' in piece]
1971        pieces = [tuple(piece.split("=", 1)) for piece in pieces]
1972        pieces = [(name.strip(), value.strip('" ')) for name, value in pieces]
1973        # Keep the first one in front (can use set down the line);
1974        # the rest are sorted
1975        if len(pieces) > 1:
1976            pieces = [pieces[0]] + sorted(pieces[1:])
1977        return pieces
1978
1979    def dedupe(pieces):
1980        "Eliminate duplicate pieces"
1981        deduped = {}
1982        for name, value in pieces:
1983            if name in deduped:
1984                continue
1985            deduped[name] = value
1986        return sorted(deduped.items(),
1987                key=pieces.index)
1988
1989    return dedupe(crumble(arg1)), crumble(arg2)
1990
1991
1992def _cheap_response_parse(arg1, arg2):
1993    """Silly parser for 'name=value; attr=attrvalue' format,
1994    to test out response renders
1995    """
1996    def crumble(arg):
1997        "Break down string into pieces"
1998        lines = [line for line in arg if line]
1999        done = []
2000        for line in lines:
2001            clauses = [clause for clause in line.split(';')]
2002            import logging
2003            logging.error("clauses %r", clauses)
2004            name, value = re.split(" *= *", clauses[0], 1)
2005            value = unquote(value.strip(' "'))
2006            attrs = [re.split(" *= *", clause, 1) \
2007                    for clause in clauses[1:] if clause]
2008            attrs = [attr for attr in attrs \
2009                     if attr[0] in Cookie.attribute_names]
2010            attrs = [(k, v.strip(' "')) for k, v in attrs]
2011            done.append((name, value, tuple(attrs)))
2012        return done
2013    result1 = crumble([arg1])
2014    result2 = crumble(arg2)
2015    return result1, result2
2016
2017
2018def test_render_request():
2019    """Test the request renderer against HEADER_CASES.
2020    Perhaps a wider range of values is tested in TestCookies.test_init.
2021    """
2022    for case in HEADER_CASES:
2023        arg, kwargs, cookies, _ = case
2024        # can't reproduce examples which are supposed to throw parse errors
2025        if isinstance(cookies, type) and issubclass(cookies, Exception):
2026            continue
2027        rendered = cookies.render_request()
2028        expected, actual = _cheap_request_parse(arg, rendered)
2029        # we can only use set() here because requests aren't order sensitive.
2030        assert set(actual) == set(expected)
2031
2032
2033def test_render_response():
2034    """Test the response renderer against HEADER_CASES.
2035    Perhaps a wider range of values is tested in TestCookies.test_init.
2036    """
2037    def filter_attrs(items):
2038        "Filter out the items which are Cookie attributes"
2039        return [(name, value) for (name, value) in items \
2040                if name.lower() in Cookie.attribute_names]
2041
2042    for case in HEADER_CASES:
2043        arg, kwargs, _, cookies = case
2044        # can't reproduce examples which are supposed to throw parse errors
2045        if isinstance(cookies, type) and issubclass(cookies, Exception):
2046            continue
2047        rendered = cookies.render_response()
2048        expected, actual = _cheap_response_parse(arg, rendered)
2049        expected, actual = set(expected), set(actual)
2050        assert actual == expected, \
2051                "failed: %s -> %s | %s != %s" % (arg, repr(cookies), actual,
2052                        expected)
2053
2054
2055def test_backslash_roundtrip():
2056    """Check that backslash in input or value stays backslash internally but
2057    goes out as %5C, and comes back in again as a backslash.
2058    """
2059    reference = Cookie('xx', '\\')
2060    assert len(reference.value) == 1
2061    reference_request = reference.render_request()
2062    reference_response = reference.render_response()
2063    assert '\\' not in reference_request
2064    assert '\\' not in reference_response
2065    assert '%5C' in reference_request
2066    assert '%5C' in reference_response
2067
2068    # Parse from multiple entry points
2069    raw_cookie = r'xx="\"'
2070    parsed_cookies = [Cookie.from_string(raw_cookie),
2071              Cookies.from_request(raw_cookie)['xx'],
2072              Cookies.from_response(raw_cookie)['xx']]
2073    for parsed_cookie in parsed_cookies:
2074        assert parsed_cookie.name == reference.name
2075        assert parsed_cookie.value == reference.value
2076        # Renders should match exactly
2077        request = parsed_cookie.render_request()
2078        response = parsed_cookie.render_response()
2079        assert request == reference_request
2080        assert response == reference_response
2081        # Reparses should too
2082        rrequest = Cookies.from_request(request)['xx']
2083        rresponse = Cookies.from_response(response)['xx']
2084        assert rrequest.name == reference.name
2085        assert rrequest.value == reference.value
2086        assert rresponse.name == reference.name
2087        assert rresponse.value == reference.value
2088
2089
2090def _simple_test(function, case_dict):
2091    "Macro for making simple case-based tests for a function call"
2092    def actual_test():
2093        "Test generated by _simple_test"
2094        for arg, expected in case_dict.items():
2095            logging.info("case for %s: %s %s",
2096                          repr(function), repr(arg), repr(expected))
2097            result = function(arg)
2098            assert result == expected, \
2099                    "%s(%s) != %s, rather %s" % (
2100                            function.__name__,
2101                            repr(arg),
2102                            repr(expected),
2103                            repr(result))
2104    actual_test.cases = case_dict
2105    return actual_test
2106
2107test_strip_spaces_and_quotes = _simple_test(strip_spaces_and_quotes, {
2108    '   ': '',
2109    '""': '',
2110    '"': '"',
2111    "''": "''",
2112    '   foo    ': 'foo',
2113    'foo    ': 'foo',
2114    '    foo': 'foo',
2115    '   ""  ': '',
2116    ' " "  ': ' ',
2117    '   "  ': '"',
2118    'foo bar': 'foo bar',
2119    '"foo bar': '"foo bar',
2120    'foo bar"': 'foo bar"',
2121    '"foo bar"': 'foo bar',
2122    '"dquoted"': 'dquoted',
2123    '   "dquoted"': 'dquoted',
2124    '"dquoted"     ': 'dquoted',
2125    '   "dquoted"     ': 'dquoted',
2126    })
2127
2128test_parse_string = _simple_test(parse_string, {
2129    None: None,
2130    '': '',
2131    b'': '',
2132    })
2133
2134test_parse_domain = _simple_test(parse_domain, {
2135    '  foo   ': 'foo',
2136    '"foo"': 'foo',
2137    '  "foo"  ': 'foo',
2138    '.foo': '.foo',
2139    })
2140
2141test_parse_path = _simple_test(parse_path, {
2142    })
2143
2144
2145def test_render_date():
2146    "Test date render routine directly with raw datetime objects"
2147    # Date rendering is also exercised pretty well in test_parse_date.
2148
2149    cases = {
2150        # Error for anything which is not known UTC/GMT
2151        datetime(2001, 10, 11, tzinfo=FixedOffsetTz(60 * 60)):
2152            AssertionError,
2153        # A couple of baseline tests
2154        datetime(1970, 1, 1, 0, 0, 0):
2155            'Thu, 01 Jan 1970 00:00:00 GMT',
2156        datetime(2007, 9, 2, 13, 59, 49):
2157            'Sun, 02 Sep 2007 13:59:49 GMT',
2158        # Don't produce 1-digit hour
2159        datetime(2007, 9, 2, 1, 59, 49):
2160            "Sun, 02 Sep 2007 01:59:49 GMT",
2161        # Don't produce 1-digit minute
2162        datetime(2007, 9, 2, 1, 1, 49):
2163            "Sun, 02 Sep 2007 01:01:49 GMT",
2164        # Don't produce 1-digit second
2165        datetime(2007, 9, 2, 1, 1, 2):
2166            "Sun, 02 Sep 2007 01:01:02 GMT",
2167        # Allow crazy past/future years for cookie delete/persist
2168        datetime(1900, 9, 2, 1, 1, 2):
2169            "Sun, 02 Sep 1900 01:01:02 GMT",
2170        datetime(3000, 9, 2, 1, 1, 2):
2171            "Tue, 02 Sep 3000 01:01:02 GMT"
2172        }
2173
2174    for dt, expected in cases.items():
2175        if isinstance(expected, type) and issubclass(expected, Exception):
2176            try:
2177                render_date(dt)
2178            except expected:
2179                continue
2180            except Exception as exception:
2181                raise AssertionError("expected %s, got %s"
2182                        % (expected, exception))
2183            raise AssertionError("expected %s, got no exception"
2184                % (expected))
2185        else:
2186            assert render_date(dt) == expected
2187
2188
2189def test_encoding_assumptions(check_unicode=False):
2190    "Document and test assumptions underlying URL encoding scheme"
2191    # Use the RFC 6265 based character class to build a regexp matcher that
2192    # will tell us whether or not a character is okay to put in cookie values.
2193    cookie_value_re = re.compile("[%s]" % Definitions.COOKIE_OCTET)
2194    # Figure out which characters are okay. (unichr doesn't exist in Python 3,
2195    # in Python 2 it shouldn't be an issue)
2196    cookie_value_safe1 = set(chr(i) for i in range(0, 256) \
2197                            if cookie_value_re.match(chr(i)))
2198    cookie_value_safe2 = set(unichr(i) for i in range(0, 256) \
2199                            if cookie_value_re.match(unichr(i)))
2200    # These two are NOT the same on Python3
2201    assert cookie_value_safe1 == cookie_value_safe2
2202    # Now which of these are quoted by urllib.quote?
2203    # caveat: Python 2.6 crashes if chr(127) is passed to quote and safe="",
2204    # so explicitly set it to b"" to avoid the issue
2205    safe_but_quoted = set(c for c in cookie_value_safe1
2206                          if quote(c, safe=b"") != c)
2207    # Produce a set of characters to give to urllib.quote for the safe parm.
2208    dont_quote = "".join(sorted(safe_but_quoted))
2209    # Make sure it works (and that it works because of what we passed)
2210    for c in dont_quote:
2211        assert quote(c, safe="") != c
2212        assert quote(c, safe=dont_quote) == c
2213
2214    # Make sure that the result of using dont_quote as the safe characters for
2215    # urllib.quote produces stuff which is safe as a cookie value, but not
2216    # different unless it has to be.
2217    for i in range(0, 255):
2218        original = chr(i)
2219        quoted = quote(original, safe=dont_quote)
2220        # If it is a valid value for a cookie, that quoting should leave it
2221        # alone.
2222        if cookie_value_re.match(original):
2223            assert original == quoted
2224        # If it isn't a valid value, then the quoted value should be valid.
2225        else:
2226            assert cookie_value_re.match(quoted)
2227
2228    assert set(dont_quote) == set("!#$%&'()*+/:<=>?@[]^`{|}~")
2229
2230    # From 128 on urllib.quote will not work on a unichr() return value.
2231    # We'll want to encode utf-8 values into ASCII, then do the quoting.
2232    # Verify that this is reversible.
2233    if check_unicode:
2234        for c in (unichr(i) for i in range(0, 1114112)):
2235            asc = c.encode('utf-8')
2236            quoted = quote(asc, safe=dont_quote)
2237            unquoted = unquote(asc)
2238            unicoded = unquoted.decode('utf-8')
2239            assert unicoded == c
2240
2241    # Now do the same for extension-av.
2242    extension_av_re = re.compile("[%s]" % Definitions.EXTENSION_AV)
2243    extension_av_safe = set(chr(i) for i in range(0, 256) \
2244                            if extension_av_re.match(chr(i)))
2245    safe_but_quoted = set(c for c in extension_av_safe \
2246                          if quote(c, safe="") != c)
2247    dont_quote = "".join(sorted(safe_but_quoted))
2248    for c in dont_quote:
2249        assert quote(c, safe="") != c
2250        assert quote(c, safe=dont_quote) == c
2251
2252    for i in range(0, 255):
2253        original = chr(i)
2254        quoted = quote(original, safe=dont_quote)
2255        if extension_av_re.match(original):
2256            assert original == quoted
2257        else:
2258            assert extension_av_re.match(quoted)
2259
2260    assert set(dont_quote) == set(' !"#$%&\'()*+,/:<=>?@[\\]^`{|}~')
2261
2262
2263test_encode_cookie_value = _simple_test(encode_cookie_value,
2264    {
2265        None: None,
2266        ' ': '%20',
2267        # let through
2268        '!': '!',
2269        '#': '#',
2270        '$': '$',
2271        '%': '%',
2272        '&': '&',
2273        "'": "'",
2274        '(': '(',
2275        ')': ')',
2276        '*': '*',
2277        '+': '+',
2278        '/': '/',
2279        ':': ':',
2280        '<': '<',
2281        '=': '=',
2282        '>': '>',
2283        '?': '?',
2284        '@': '@',
2285        '[': '[',
2286        ']': ']',
2287        '^': '^',
2288        '`': '`',
2289        '{': '{',
2290        '|': '|',
2291        '}': '}',
2292        '~': '~',
2293        # not let through
2294        ' ': '%20',
2295        '"': '%22',
2296        ',': '%2C',
2297        '\\': '%5C',
2298        'crud,': 'crud%2C',
2299    })
2300
2301test_encode_extension_av = _simple_test(encode_extension_av,
2302    {
2303        None: '',
2304        '': '',
2305        'foo': 'foo',
2306        # stuff this lets through that cookie-value does not
2307        ' ': ' ',
2308        '"': '"',
2309        ',': ',',
2310        '\\': '\\',
2311        'yo\\b': 'yo\\b',
2312    })
2313
2314test_valid_value = _simple_test(valid_value,
2315    {
2316        None: False,
2317        '': True,
2318        'ಠ_ಠ': True,
2319        'μῆνιν ἄειδε θεὰ Πηληϊάδεω Ἀχιλῆος': True,
2320        '这事情得搞好啊': True,
2321        '宮崎 駿': True,
2322        'أم كلثوم': True,
2323        'ედუარდ შევარდნაძე': True,
2324        'Myötähäpeä': True,
2325        'Pedro Almodóvar': True,
2326#       b'': True,
2327#       b'ABCDEFGHIJKLMNOPQRSTUVWXYZ': True,
2328        'Pedro Almodóvar'.encode('utf-8'): False,
2329    })
2330
2331test_valid_date = _simple_test(valid_date,
2332    {
2333        datetime(2011, 1, 1): True,
2334        datetime(2011, 1, 1, tzinfo=FixedOffsetTz(1000)): False,
2335        datetime(2011, 1, 1, tzinfo=FixedOffsetTz(0)): True,
2336    })
2337
2338test_valid_domain = _simple_test(valid_domain,
2339    {
2340        '': False,
2341        ' ': False,
2342        '.': False,
2343        '..': False,
2344        '.foo': True,
2345        '"foo"': False,
2346        'foo': True,
2347    })
2348
2349test_valid_path = _simple_test(valid_path,
2350    {
2351        '': False,
2352        ' ': False,
2353        '/': True,
2354        'a': False,
2355        '/a': True,
2356        '\x00': False,
2357        '/\x00': False,
2358    })
2359
2360
2361def test_many_pairs():
2362    """Simple 'lots of pairs' test
2363    """
2364    from_request = Cookies.from_request
2365    header = "a0=0"
2366    for i in range(1, 100):
2367        i_range = list(range(0, i))
2368        cookies = from_request(header)
2369        assert len(cookies) == i
2370        for j in i_range:
2371            key = 'a%d' % j
2372            assert cookies[key].value == str(j * 10)
2373            assert cookies[key].render_request() == \
2374                    "a%d=%d" % (j, j * 10)
2375
2376        # same test, different entry point
2377        cookies = Cookies()
2378        cookies.parse_request(header)
2379        assert len(cookies) == i
2380        for j in i_range:
2381            key = 'a%d' % j
2382            assert cookies[key].value == str(j * 10)
2383            assert cookies[key].render_request() == \
2384                    "a%d=%d" % (j, j * 10)
2385
2386        # Add another piece to the header
2387        header += "; a%d=%d" % (i, i * 10)
2388
2389
2390def test_parse_value():
2391    # this really just glues together strip_spaces_and_quotes
2392    # and parse_string, so reuse their test cases
2393    cases = {}
2394    cases.update(test_strip_spaces_and_quotes.cases)
2395    cases.update(test_parse_string.cases)
2396    for inp, expected in cases.items():
2397        print("case", inp, expected)
2398        # Test with spaces allowed
2399        obtained = parse_value(inp, allow_spaces=True)
2400        assert obtained == expected
2401
2402        # Test with spaces disallowed, if it could do anything
2403        if (isinstance(inp, bytes) and ' ' in inp.decode('utf-8').strip()) \
2404        or (not isinstance(inp, bytes) and inp and ' ' in inp.strip()):
2405            try:
2406                obtained = parse_value(inp, allow_spaces=False)
2407            except AssertionError:
2408                pass
2409            else:
2410                raise AssertionError("parse_value(%s, allow_spaces=False) "
2411                                     "did not raise" % repr(inp))
2412
2413
2414def test_total_seconds():
2415    """This wrapper probably doesn't need testing so much, and it's not
2416    entirely trivial to fully exercise, but the coverage is nice to have
2417    """
2418    def basic_sanity(td_type):
2419        assert _total_seconds(td_type(seconds=1)) == 1
2420        assert _total_seconds(td_type(seconds=1, minutes=1)) == 1 + 60
2421        assert _total_seconds(td_type(seconds=1, minutes=1, hours=1)) == \
2422                1 + 60 + 60 * 60
2423
2424    basic_sanity(timedelta)
2425
2426    class FakeTimeDelta(object):
2427        def __init__(self, days=0, hours=0, minutes=0, seconds=0,
2428                     microseconds=0):
2429            self.days = days
2430            self.seconds = seconds + minutes * 60 + hours * 60 * 60
2431            self.microseconds = microseconds
2432
2433    assert not hasattr(FakeTimeDelta, "total_seconds")
2434    basic_sanity(FakeTimeDelta)
2435
2436    FakeTimeDelta.total_seconds = lambda: None.missing_attribute
2437    try:
2438        _total_seconds(None)
2439    except AttributeError as e:
2440        assert 'total_seconds' not in str(e)
2441
2442
2443def test_valid_value_bad_quoter():
2444    def bad_quote(s):
2445        return "Frogs"
2446
2447    assert valid_value("eep", quote=bad_quote) == False
2448