66from werkzeug .datastructures import Headers
77from werkzeug .http import parse_dict_header
88
9+ from pytest_httpserver import CaseInsensitiveHeaderValueMatcher
910from pytest_httpserver import HTTPServer
11+ from pytest_httpserver import RequestMatcher
1012from pytest_httpserver .httpserver import HeaderValueMatcher
1113from pytest_httpserver .httpserver import NoMethodFoundForMatchingHeaderValueError
1214
1315
14- def test_header_matcher_fallback_does_not_cache_header_names ():
15- matchers = defaultdict (lambda : HeaderValueMatcher .default_header_value_matcher )
16- matcher = HeaderValueMatcher (matchers )
16+ def test_case_insensitive_header_matcher_fallback_does_not_cache_header_names ():
17+ comparators = iter (
18+ [HeaderValueMatcher .default_header_value_matcher , lambda actual , expected : actual == expected .upper ()]
19+ )
20+ matchers = defaultdict (lambda : next (comparators ))
21+ matcher = CaseInsensitiveHeaderValueMatcher (matchers )
1722
1823 assert matcher ("x-custom" , "value" , "value" )
19- assert not matcher ("x-custom" , "VALUE" , "value" )
20- assert not matchers
21-
22- matchers ["X-Custom" ] = lambda actual , expected : actual == expected .upper ()
2324 assert matcher ("x-custom" , "VALUE" , "value" )
25+ assert not matchers
2426
2527
26- def test_header_matcher_without_default ():
27- matcher = HeaderValueMatcher ({"x-custom" : HeaderValueMatcher .default_header_value_matcher })
28+ @pytest .mark .parametrize ("mapping_class" , [dict , defaultdict ])
29+ def test_case_insensitive_header_matcher_without_default (mapping_class ):
30+ matchers = mapping_class ()
31+ matchers ["x-custom" ] = HeaderValueMatcher .default_header_value_matcher
32+ matcher = CaseInsensitiveHeaderValueMatcher (matchers )
2833
2934 assert matcher ("X-CUSTOM" , "value" , "value" )
3035 assert not matcher ("X-CUSTOM" , "VALUE" , "value" )
3136 with pytest .raises (NoMethodFoundForMatchingHeaderValueError , match = "X-Missing" ):
3237 matcher ("X-Missing" , "value" , "value" )
3338
3439
35- @pytest .mark .parametrize ("header_name" , ["Custom" , "custom" , "CUSTOM" ])
36- def test_custom_headers (httpserver : HTTPServer , header_name : str ):
40+ def test_case_insensitive_header_matcher_copies_comparators ():
41+ matchers = {"X-Custom" : HeaderValueMatcher .default_header_value_matcher }
42+ first = CaseInsensitiveHeaderValueMatcher (matchers )
43+ matchers ["X-Custom" ] = lambda actual , expected : actual == expected .upper ()
44+ second = CaseInsensitiveHeaderValueMatcher (matchers )
45+
46+ assert first ("X-CUSTOM" , "value" , "value" )
47+ assert not first ("X-CUSTOM" , "VALUE" , "value" )
48+ assert second ("x-custom" , "VALUE" , "value" )
49+ assert not second ("x-custom" , "value" , "value" )
50+
51+
52+ def test_case_insensitive_header_matcher_duplicate_names ():
53+ matcher = CaseInsensitiveHeaderValueMatcher (
54+ {
55+ "X-Custom" : HeaderValueMatcher .default_header_value_matcher ,
56+ "x-CUSTOM" : lambda actual , expected : actual == expected .upper (),
57+ }
58+ )
59+
60+ assert matcher ("x-custom" , "VALUE" , "value" )
61+ assert not matcher ("X-Custom" , "value" , "value" )
62+
63+
64+ def test_case_insensitive_header_matcher_independent_defaults (monkeypatch ):
65+ legacy_matchers = {"X-Custom" : lambda actual , expected : actual == expected .upper ()}
66+ monkeypatch .setattr (HeaderValueMatcher , "DEFAULT_MATCHERS" , legacy_matchers )
67+ customized = CaseInsensitiveHeaderValueMatcher (legacy_matchers )
68+
69+ assert customized ("x-custom" , "VALUE" , "value" )
70+ for matcher in (CaseInsensitiveHeaderValueMatcher (), CaseInsensitiveHeaderValueMatcher ()):
71+ assert matcher ("x-custom" , "value" , "value" )
72+ assert not matcher ("x-custom" , "VALUE" , "value" )
73+ assert not matcher ("x-custom" , None , "value" )
74+ assert matcher ("authorization" , 'Digest a="1", b="2"' , 'Digest b="2", a="1"' )
75+ assert not matcher ("AUTHORIZATION" , None , 'Digest a="1"' )
76+
77+ assert list (legacy_matchers ) == ["X-Custom" ]
78+ default_matcher = RequestMatcher ("/" ).header_value_matcher
79+ assert isinstance (default_matcher , HeaderValueMatcher )
80+ assert default_matcher .matchers is legacy_matchers
81+
82+
83+ def test_legacy_header_matcher_preserves_case_sensitive_lookup ():
84+ matcher = HeaderValueMatcher ({"X-Custom" : HeaderValueMatcher .default_header_value_matcher })
85+
86+ assert matcher ("X-Custom" , "value" , "value" )
87+ with pytest .raises (NoMethodFoundForMatchingHeaderValueError , match = "x-custom" ):
88+ matcher ("x-custom" , "value" , "value" )
89+
90+
91+ def test_legacy_header_matcher_preserves_defaultdict_lookup ():
92+ matchers = defaultdict (lambda : HeaderValueMatcher .default_header_value_matcher )
93+ matcher = HeaderValueMatcher (matchers )
94+
95+ assert matcher ("x-custom" , "value" , "value" )
96+ assert matchers ["x-custom" ] is HeaderValueMatcher .default_header_value_matcher
97+ matchers ["X-Custom" ] = lambda actual , expected : actual == expected .upper ()
98+ assert not matcher ("x-custom" , "VALUE" , "value" )
99+
100+
101+ def test_case_insensitive_header_matcher_bake_is_opt_in (httpserver : HTTPServer ):
102+ matcher = CaseInsensitiveHeaderValueMatcher (
103+ {"x-CUSTOM" : lambda actual , expected : actual is not None and actual .lower () == expected .lower ()}
104+ )
105+ server = httpserver .bake (header_value_matcher = matcher )
106+ server .expect_request ("/opt-in" , headers = {"X-Custom" : "value" }).respond_with_data ("OK" )
107+ httpserver .expect_request ("/default" , headers = {"X-Custom" : "value" }).respond_with_data ("OK" )
108+
109+ assert requests .get (server .url_for ("/opt-in" ), headers = {"X-Custom" : "VALUE" }).status_code == 200
110+ assert requests .get (httpserver .url_for ("/default" ), headers = {"X-Custom" : "VALUE" }).status_code == 500
111+
112+
113+ @pytest .mark .parametrize (
114+ ("matcher_class" , "comparator_name" , "header_name" ),
115+ [
116+ (HeaderValueMatcher , "Custom" , "Custom" ),
117+ (CaseInsensitiveHeaderValueMatcher , "Custom" , "Custom" ),
118+ (CaseInsensitiveHeaderValueMatcher , "CuStOm" , "custom" ),
119+ (CaseInsensitiveHeaderValueMatcher , "custom" , "CUSTOM" ),
120+ ],
121+ )
122+ def test_custom_headers (httpserver : HTTPServer , matcher_class , comparator_name : str , header_name : str ):
37123 headers_with_values_in_direct_order = {header_name : 'Scheme key0="value0", key1="value1"' }
38124 httpserver .expect_request (uri = "/" , headers = headers_with_values_in_direct_order ).respond_with_data ("OK" )
39125 response = requests .get (httpserver .url_for ("/" ), headers = headers_with_values_in_direct_order )
@@ -46,16 +132,16 @@ def test_custom_headers(httpserver: HTTPServer, header_name: str):
46132 assert response .status_code == 500
47133
48134 # Define header_value_matcher that ignores the order of items in header value dict
49- def custom_header_value_matcher (actual : str , expected : str ) -> bool :
135+ def custom_header_value_matcher (actual : str | None , expected : str ) -> bool :
136+ if actual is None :
137+ return False
50138 actual_scheme , _ , actual_dict_str = actual .partition (" " )
51139 expected_scheme , _ , expected_dict_str = expected .partition (" " )
52140 actual_dict = parse_dict_header (actual_dict_str )
53141 expected_dict = parse_dict_header (expected_dict_str )
54142 return actual_scheme == expected_scheme and actual_dict == expected_dict
55143
56- matchers = HeaderValueMatcher .DEFAULT_MATCHERS .copy () # type: ignore
57- matchers ["Custom" ] = custom_header_value_matcher
58- header_value_matcher = HeaderValueMatcher (matchers )
144+ header_value_matcher = matcher_class ({comparator_name : custom_header_value_matcher })
59145
60146 httpserver .handlers .clear ()
61147 httpserver .expect_request (
@@ -67,8 +153,20 @@ def custom_header_value_matcher(actual: str, expected: str) -> bool:
67153
68154
69155# See https://en.wikipedia.org/wiki/Digest_access_authentication
70- @pytest .mark .parametrize ("header_name" , ["Authorization" , "authorization" , "AUTHORIZATION" , "aUtHoRiZaTiOn" ])
71- def test_authorization_headers (httpserver : HTTPServer , header_name : str ):
156+ @pytest .mark .parametrize (
157+ ("header_name" , "case_insensitive" ),
158+ [
159+ ("Authorization" , False ),
160+ ("Authorization" , True ),
161+ ("authorization" , True ),
162+ ("AUTHORIZATION" , True ),
163+ ("aUtHoRiZaTiOn" , True ),
164+ ],
165+ )
166+ def test_authorization_headers (httpserver : HTTPServer , header_name : str , * , case_insensitive : bool ):
167+ server = (
168+ httpserver .bake (header_value_matcher = CaseInsensitiveHeaderValueMatcher ()) if case_insensitive else httpserver
169+ )
72170 headers_with_values_in_direct_order = {
73171 header_name : (
74172 'Digest username="Mufasa",'
@@ -82,8 +180,8 @@ def test_authorization_headers(httpserver: HTTPServer, header_name: str):
82180 'opaque="5ccc069c403ebaf9f0171e9517f40e41"'
83181 )
84182 }
85- httpserver .expect_request (uri = "/" , headers = headers_with_values_in_direct_order ).respond_with_data ("OK" )
86- response = requests .get (httpserver .url_for ("/" ), headers = headers_with_values_in_direct_order )
183+ server .expect_request (uri = "/" , headers = headers_with_values_in_direct_order ).respond_with_data ("OK" )
184+ response = requests .get (server .url_for ("/" ), headers = headers_with_values_in_direct_order )
87185 assert response .status_code == 200
88186 assert response .text == "OK"
89187
@@ -100,7 +198,7 @@ def test_authorization_headers(httpserver: HTTPServer, header_name: str):
100198 'opaque="5ccc069c403ebaf9f0171e9517f40e41"'
101199 )
102200 }
103- response = requests .get (httpserver .url_for ("/" ), headers = headers_with_values_in_modified_order )
201+ response = requests .get (server .url_for ("/" ), headers = headers_with_values_in_modified_order )
104202 assert response .status_code == 200
105203 assert response .text == "OK"
106204
0 commit comments