2929
3030from collections import namedtuple
3131import codecs
32+ import importlib
3233from io import BytesIO
34+ import os
35+ import sys
3336from urllib .parse import urlencode , urlparse
3437
35- from werkzeug . datastructures import MultiDict
38+ from tests . support . suppconst import MIG_BASE
3639
40+ TEXTUAL_CONTENT_TYPES = set (('text/plain' , 'text/html' , 'application/json' ))
41+ OBJECTS_TYPE = 'objects'
3742
38- # named type representing the tuple that is passed to WSGI handlers
39- _PreparedWsgi = namedtuple ('_PreparedWsgi' , ['environ' , 'start_response' ])
43+
44+ def _import_forcibly (module_name , relative_module_dir = None ):
45+ """Custom import function to allow an import of a file for testing
46+ that resides within a non-module directory."""
47+
48+ module_path = os .path .join (MIG_BASE , 'mig' )
49+ if relative_module_dir is not None :
50+ module_path = os .path .join (module_path , relative_module_dir )
51+ sys .path .append (module_path )
52+ mod = importlib .import_module (module_name )
53+ sys .path .pop (- 1 ) # do not leave the forced module path
54+ return mod
55+
56+
57+ migwsgi = _import_forcibly ('migwsgi' , relative_module_dir = 'wsgi-bin' )
4058
4159
4260class FakeWsgiStartResponse :
@@ -51,7 +69,29 @@ def __call__(self, status, headers, exc=None):
5169 self .calls .append ((status , headers , exc ))
5270
5371
54- def create_wsgi_environ (configuration , wsgi_url , method = 'GET' , query = None , headers = None , form = None ):
72+ def _urlencode_form (form_content ):
73+ """
74+ Convert a data structure describing form contents to byte string
75+ that can be directly sent as the body of an HTTP request.
76+ """
77+
78+ field_key_and_value_pairs = []
79+ if isinstance (form_content , dict ):
80+ for key , value in form_content .items ():
81+ if isinstance (value , list ):
82+ for item in value :
83+ field_key_and_value_pairs .append ((key , item ))
84+ continue
85+ field_key_and_value_pairs .append ((key , value ))
86+ elif isinstance (form_content , list ):
87+ field_key_and_value_pairs = form_content
88+ else :
89+ raise AssertionError ("invalid form content" )
90+ return urlencode (field_key_and_value_pairs , doseq = True ).encode ('ascii' )
91+
92+
93+ def create_wsgi_environ (configuration , wsgi_url , method = None ,
94+ query = None , headers = None , form = None , mig_user_dn = None ):
5595 """Populate the necessary variables that will constitute a valid WSGI
5696 environment given a URL to which we will make a requests under test and
5797 various other options that set up the nature of that request."""
@@ -67,7 +107,7 @@ def create_wsgi_environ(configuration, wsgi_url, method='GET', query=None, heade
67107 method = 'POST'
68108 request_query = ''
69109
70- body = urlencode ( MultiDict ( form )). encode ( 'ascii' )
110+ body = _urlencode_form ( form )
71111
72112 headers = headers or {}
73113 if not 'Content-Type' in headers :
@@ -76,6 +116,7 @@ def create_wsgi_environ(configuration, wsgi_url, method='GET', query=None, heade
76116 headers ['Content-Length' ] = str (len (body ))
77117 wsgi_input = BytesIO (body )
78118 else :
119+ assert method is not None , "method required with no payload specified"
79120 request_query = parsed_url .query
80121 wsgi_input = ()
81122
@@ -95,10 +136,21 @@ def close(self, *ars, **kwargs):
95136 environ ['HTTP_HOST' ] = parsed_url .netloc
96137 environ ['PATH_INFO' ] = parsed_url .path
97138 environ ['QUERY_STRING' ] = request_query
139+ environ ['REMOTE_ADDR' ] = '127.0.0.1'
98140 environ ['REQUEST_METHOD' ] = method
99141 environ ['SCRIPT_URI' ] = '' .join (
100142 ('http://' , environ ['HTTP_HOST' ], environ ['PATH_INFO' ]))
101143
144+ if mig_user_dn :
145+ environ ['REMOTE_USER' ] = mig_user_dn
146+
147+ path_parts = parsed_url .path .split ('/' )
148+ maybe_script_name = path_parts [- 1 ]
149+ _ , script_ext = os .path .splitext (path_parts [- 1 ])
150+ if script_ext != '' :
151+ # the script has an extension, so treat it as a functionality file
152+ environ ['SCRIPT_NAME' ] = maybe_script_name
153+
102154 if headers :
103155 for k , v in headers .items ():
104156 header_key = k .replace ('-' , '_' ).upper ()
@@ -112,38 +164,72 @@ def close(self, *ars, **kwargs):
112164 return environ
113165
114166
115- def create_wsgi_start_response ():
116- return FakeWsgiStartResponse ()
167+ class _PreparedWsgi :
168+ """
169+ Object representing a simulated WSGI request to be exercised by a test case.
170+ """
117171
172+ def __init__ (self , configuration , url , ** kwargs ):
173+ self .configuration = configuration
174+ self .environ = create_wsgi_environ (configuration , url , ** kwargs )
175+ self .start_response = FakeWsgiStartResponse ()
118176
119- def prepare_wsgi (configuration , url , ** kwargs ):
120- return _PreparedWsgi (
121- create_wsgi_environ (configuration , url , ** kwargs ),
122- create_wsgi_start_response ()
123- )
177+ def __iter__ (self ):
178+ return iter ((self .environ , self .start_response ))
124179
180+ def _bind_invocation (self ):
181+ self .application_args = (
182+ self .environ ,
183+ self .start_response ,
184+ )
125185
126- def _trigger_and_unpack_result (wsgi_result ):
127- chunks = list (wsgi_result )
128- assert len (chunks ) > 0 , "invocation returned no output"
129- complete_value = b'' .join (chunks )
130- decoded_value = codecs .decode (complete_value , 'utf8' )
131- return decoded_value
186+ self .application_kwargs = dict (
187+ configuration = self .configuration ,
188+ _set_os_environ = False ,
189+ )
190+
191+ return migwsgi .application (
192+ * self .application_args ,
193+ ** self .application_kwargs
194+ )
195+
196+ @staticmethod
197+ def trigger_wsgi (wsgi_result ):
198+ chunks = list (wsgi_result )
199+ assert len (chunks ) > 0 , "invocation returned no output"
200+ return b'' .join (chunks )
201+
202+
203+ def prepare_wsgi (configuration , url , ** kwargs ):
204+ if 'method' not in kwargs :
205+ kwargs ['method' ] = 'GET'
206+ return _PreparedWsgi (configuration , url , ** kwargs )
132207
133208
134209class WsgiAssertMixin :
135210 """Custom assertions for verifying server code executed under test."""
136211
137- def assertWsgiResponse (self , wsgi_result , fake_wsgi , expected_status_code ):
138- assert isinstance ( fake_wsgi , _PreparedWsgi )
212+ def prepareWsgiAssert (self , configuration , url , ** kwargs ):
213+ return _PreparedWsgi ( configuration , url , ** kwargs )
139214
140- content = _trigger_and_unpack_result (wsgi_result )
215+ def assertWsgiResponse (self , wsgi_result , prepared_wsgi ,
216+ expected_status_code = None ,
217+ expected_content_type = None ,
218+ content_format = None ):
219+ assert isinstance (prepared_wsgi , _PreparedWsgi )
220+
221+ if wsgi_result :
222+ # legacy codepath
223+ pass
224+ else :
225+ wsgi_result = prepared_wsgi ._bind_invocation ()
226+ content = _PreparedWsgi .trigger_wsgi (wsgi_result )
141227
142228 def called_once (fake ):
143229 assert hasattr (fake , 'calls' )
144230 return len (fake .calls ) == 1
145231
146- fake_start_response = fake_wsgi .start_response
232+ fake_start_response = prepared_wsgi .start_response
147233
148234 try :
149235 self .assertTrue (called_once (fake_start_response ))
@@ -155,11 +241,24 @@ def called_once(fake):
155241
156242 wsgi_call = fake_start_response .calls [0 ]
157243
158- # check for expected HTTP status code
159- wsgi_status = wsgi_call [0 ]
160- actual_status_code = int (wsgi_status [0 :3 ])
161- self .assertEqual (actual_status_code , expected_status_code )
244+ actual_content_type = wsgi_call
245+ is_textual = None
246+
247+ if expected_status_code :
248+ # check for expected HTTP status code
249+ wsgi_status = wsgi_call [0 ]
250+ actual_status_code = int (wsgi_status [0 :3 ])
251+ self .assertEqual (actual_status_code , expected_status_code )
162252
163253 headers = dict (wsgi_call [1 ])
164254
255+ actual_content_type = headers .get ('Content-Type' , 'none/none' )
256+ if expected_content_type :
257+ self .assertEqual (actual_content_type , expected_content_type , "mismatched Content-Type" )
258+
259+ content_is_textual = actual_content_type in TEXTUAL_CONTENT_TYPES
260+ if content_is_textual :
261+ textual_content = codecs .decode (content , 'utf8' )
262+ return textual_content , headers
263+
165264 return content , headers
0 commit comments