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