11import http .client
2+ from collections import defaultdict
23
4+ import pytest
35import requests
46from werkzeug .datastructures import Headers
57from werkzeug .http import parse_dict_header
68
9+ from pytest_httpserver import CaseInsensitiveHeaderValueMatcher
710from pytest_httpserver import HTTPServer
11+ from pytest_httpserver import RequestMatcher
812from pytest_httpserver .httpserver import HeaderValueMatcher
13+ from pytest_httpserver .httpserver import NoMethodFoundForMatchingHeaderValueError
914
1015
11- def test_custom_headers (httpserver : HTTPServer ):
12- headers_with_values_in_direct_order = {"Custom" : 'Scheme key0="value0", key1="value1"' }
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 )
22+
23+ assert matcher ("x-custom" , "value" , "value" )
24+ assert matcher ("x-custom" , "VALUE" , "value" )
25+ assert not matchers
26+
27+
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 )
33+
34+ assert matcher ("X-CUSTOM" , "value" , "value" )
35+ assert not matcher ("X-CUSTOM" , "VALUE" , "value" )
36+ with pytest .raises (NoMethodFoundForMatchingHeaderValueError , match = "X-Missing" ):
37+ matcher ("X-Missing" , "value" , "value" )
38+
39+
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 ):
123+ headers_with_values_in_direct_order = {header_name : 'Scheme key0="value0", key1="value1"' }
13124 httpserver .expect_request (uri = "/" , headers = headers_with_values_in_direct_order ).respond_with_data ("OK" )
14125 response = requests .get (httpserver .url_for ("/" ), headers = headers_with_values_in_direct_order )
15126 assert response .status_code == 200
@@ -21,16 +132,16 @@ def test_custom_headers(httpserver: HTTPServer):
21132 assert response .status_code == 500
22133
23134 # Define header_value_matcher that ignores the order of items in header value dict
24- 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
25138 actual_scheme , _ , actual_dict_str = actual .partition (" " )
26139 expected_scheme , _ , expected_dict_str = expected .partition (" " )
27140 actual_dict = parse_dict_header (actual_dict_str )
28141 expected_dict = parse_dict_header (expected_dict_str )
29142 return actual_scheme == expected_scheme and actual_dict == expected_dict
30143
31- matchers = HeaderValueMatcher .DEFAULT_MATCHERS .copy () # type: ignore
32- matchers ["Custom" ] = custom_header_value_matcher
33- header_value_matcher = HeaderValueMatcher (matchers )
144+ header_value_matcher = matcher_class ({comparator_name : custom_header_value_matcher })
34145
35146 httpserver .handlers .clear ()
36147 httpserver .expect_request (
@@ -42,9 +153,22 @@ def custom_header_value_matcher(actual: str, expected: str) -> bool:
42153
43154
44155# See https://en.wikipedia.org/wiki/Digest_access_authentication
45- def test_authorization_headers (httpserver : HTTPServer ):
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+ )
46170 headers_with_values_in_direct_order = {
47- "Authorization" : (
171+ header_name : (
48172 'Digest username="Mufasa",'
49173 'realm="testrealm@host.com",'
50174 'nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",'
@@ -56,8 +180,8 @@ def test_authorization_headers(httpserver: HTTPServer):
56180 'opaque="5ccc069c403ebaf9f0171e9517f40e41"'
57181 )
58182 }
59- httpserver .expect_request (uri = "/" , headers = headers_with_values_in_direct_order ).respond_with_data ("OK" )
60- 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 )
61185 assert response .status_code == 200
62186 assert response .text == "OK"
63187
@@ -74,7 +198,7 @@ def test_authorization_headers(httpserver: HTTPServer):
74198 'opaque="5ccc069c403ebaf9f0171e9517f40e41"'
75199 )
76200 }
77- 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 )
78202 assert response .status_code == 200
79203 assert response .text == "OK"
80204
0 commit comments