3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Tests for google.appengine.tools.devappserver2.http_proxy."""
32 from google
.appengine
.api
import appinfo
33 from google
.appengine
.tools
.devappserver2
import http_proxy
34 from google
.appengine
.tools
.devappserver2
import http_runtime_constants
35 from google
.appengine
.tools
.devappserver2
import instance
36 from google
.appengine
.tools
.devappserver2
import login
37 from google
.appengine
.tools
.devappserver2
import wsgi_test_utils
40 class MockMessage(object):
41 def __init__(self
, headers
):
42 self
.headers
= headers
45 return iter(set(name
for name
, _
in self
.headers
))
47 def getheaders(self
, name
):
48 return [value
for header_name
, value
in self
.headers
if header_name
== name
]
51 class FakeHttpResponse(object):
52 def __init__(self
, status
, reason
, headers
, body
):
55 self
.partial_read_error
= None
58 self
.headers
= headers
59 self
.msg
= MockMessage(headers
)
61 def read(self
, amt
=None):
65 elif self
.partial_read_error
:
66 raise self
.partial_read_error
74 def get_instance_logs():
78 class HttpProxyTest(wsgi_test_utils
.WSGITestCase
):
81 self
.tmpdir
= tempfile
.mkdtemp()
83 self
.proxy
= http_proxy
.HttpProxy(
84 host
='localhost', port
=23456,
85 instance_died_unexpectedly
=lambda: False,
86 instance_logs_getter
=get_instance_logs
,
87 error_handler_file
=None)
89 self
.mox
.StubOutWithMock(httplib
.HTTPConnection
, 'connect')
90 self
.mox
.StubOutWithMock(httplib
.HTTPConnection
, 'request')
91 self
.mox
.StubOutWithMock(httplib
.HTTPConnection
, 'getresponse')
92 self
.mox
.StubOutWithMock(httplib
.HTTPConnection
, 'close')
93 self
.mox
.StubOutWithMock(login
, 'get_user_info')
94 self
.url_map
= appinfo
.URLMap(url
=r
'/(get|post).*',
98 shutil
.rmtree(self
.tmpdir
)
101 def test_wait_for_connection_retries_used_up(self
):
103 for _
in xrange(0, retries
+ 1):
104 httplib
.HTTPConnection
.connect().AndRaise(socket
.error
)
105 httplib
.HTTPConnection
.close()
108 self
.assertRaises(http_proxy
.HostNotReachable
,
109 self
.proxy
.wait_for_connection
, retries
)
112 def test_wait_for_connection_worked(self
):
114 for _
in xrange(0, retries
):
115 httplib
.HTTPConnection
.connect().AndRaise(socket
.error
)
116 httplib
.HTTPConnection
.close()
118 httplib
.HTTPConnection
.connect()
119 httplib
.HTTPConnection
.close()
122 self
.proxy
.wait_for_connection(retries
+ 1)
125 def test_handle_get(self
):
126 response
= FakeHttpResponse(200,
128 [('Foo', 'a'), ('Foo', 'b'), ('Var', 'c')],
130 login
.get_user_info(None).AndReturn(('', False, ''))
131 httplib
.HTTPConnection
.connect()
132 httplib
.HTTPConnection
.request(
133 'GET', '/get%20request?key=value', '',
135 http_runtime_constants
.REQUEST_ID_HEADER
: 'request id',
136 'X-AppEngine-Country': 'ZZ',
137 'X-Appengine-User-Email': '',
138 'X-Appengine-User-Id': '',
139 'X-Appengine-User-Is-Admin': '0',
140 'X-Appengine-User-Nickname': '',
141 'X-Appengine-User-Organization': '',
142 'X-APPENGINE-DEV-SCRIPT': 'get.py',
143 'X-APPENGINE-SERVER-NAME': 'localhost',
144 'X-APPENGINE-SERVER-PORT': '8080',
145 'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
147 httplib
.HTTPConnection
.getresponse().AndReturn(response
)
148 httplib
.HTTPConnection
.close()
149 environ
= {'HTTP_HEADER': 'value', 'PATH_INFO': '/get request',
150 'QUERY_STRING': 'key=value',
151 'HTTP_X_APPENGINE_USER_ID': '123',
152 'SERVER_NAME': 'localhost',
153 'SERVER_PORT': '8080',
154 'SERVER_PROTOCOL': 'HTTP/1.1',
157 expected_headers
= [('Foo', 'a'), ('Foo', 'b'), ('Var', 'c')]
158 self
.assertResponse('200 OK', expected_headers
, 'response',
159 self
.proxy
.handle
, environ
,
160 url_map
=self
.url_map
,
161 match
=re
.match(self
.url_map
.url
, '/get%20request'),
162 request_id
='request id',
163 request_type
=instance
.NORMAL_REQUEST
)
166 def test_handle_post(self
):
167 response
= FakeHttpResponse(200,
169 [('Foo', 'a'), ('Foo', 'b'), ('Var', 'c')],
171 login
.get_user_info('cookie').AndReturn(('user@example.com', True, '12345'))
172 httplib
.HTTPConnection
.connect()
173 httplib
.HTTPConnection
.request(
174 'POST', '/post', 'post data',
177 'CONTENT-TYPE': 'text/plain',
178 'CONTENT-LENGTH': '9',
179 http_runtime_constants
.REQUEST_ID_HEADER
: 'request id',
180 'X-AppEngine-Country': 'ZZ',
181 'X-Appengine-User-Email': 'user@example.com',
182 'X-Appengine-User-Id': '12345',
183 'X-Appengine-User-Is-Admin': '1',
184 'X-Appengine-User-Nickname': 'user',
185 'X-Appengine-User-Organization': 'example.com',
186 'X-APPENGINE-DEV-SCRIPT': 'post.py',
187 'X-APPENGINE-SERVER-NAME': 'localhost',
188 'X-APPENGINE-SERVER-PORT': '8080',
189 'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
191 httplib
.HTTPConnection
.getresponse().AndReturn(response
)
192 httplib
.HTTPConnection
.close()
193 environ
= {'HTTP_HEADER': 'value', 'PATH_INFO': '/post',
194 'wsgi.input': cStringIO
.StringIO('post data'),
195 'CONTENT_LENGTH': '9',
196 'CONTENT_TYPE': 'text/plain',
197 'REQUEST_METHOD': 'POST',
198 'HTTP_COOKIE': 'cookie',
199 'SERVER_NAME': 'localhost',
200 'SERVER_PORT': '8080',
201 'SERVER_PROTOCOL': 'HTTP/1.1',
204 expected_headers
= [('Foo', 'a'), ('Foo', 'b'), ('Var', 'c')]
205 self
.assertResponse('200 OK', expected_headers
, 'response',
206 self
.proxy
.handle
, environ
,
207 url_map
=self
.url_map
,
208 match
=re
.match(self
.url_map
.url
, '/post'),
209 request_id
='request id',
210 request_type
=instance
.NORMAL_REQUEST
)
213 def test_handle_with_error(self
):
214 error_handler_file
= os
.path
.join(self
.tmpdir
, 'error.html')
215 with
open(error_handler_file
, 'w') as f
:
218 self
.proxy
= http_proxy
.HttpProxy(
219 host
='localhost', port
=23456,
220 instance_died_unexpectedly
=lambda: False,
221 instance_logs_getter
=get_instance_logs
,
222 error_handler_file
=error_handler_file
)
224 response
= FakeHttpResponse(
225 500, 'Internal Server Error',
226 [(http_runtime_constants
.ERROR_CODE_HEADER
, '1')], '')
227 login
.get_user_info(None).AndReturn(('', False, ''))
228 httplib
.HTTPConnection
.connect()
229 httplib
.HTTPConnection
.request(
230 'GET', '/get%20error', '',
232 http_runtime_constants
.REQUEST_ID_HEADER
: 'request id',
233 'X-AppEngine-Country': 'ZZ',
234 'X-Appengine-User-Email': '',
235 'X-Appengine-User-Id': '',
236 'X-Appengine-User-Is-Admin': '0',
237 'X-Appengine-User-Nickname': '',
238 'X-Appengine-User-Organization': '',
239 'X-APPENGINE-DEV-SCRIPT': 'get.py',
240 'X-APPENGINE-SERVER-NAME': 'localhost',
241 'X-APPENGINE-SERVER-PORT': '8080',
242 'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
244 httplib
.HTTPConnection
.getresponse().AndReturn(response
)
245 httplib
.HTTPConnection
.close()
246 environ
= {'HTTP_HEADER': 'value', 'PATH_INFO': '/get error',
248 'HTTP_X_APPENGINE_USER_ID': '123',
249 'SERVER_NAME': 'localhost',
250 'SERVER_PORT': '8080',
251 'SERVER_PROTOCOL': 'HTTP/1.1',
255 'Content-Type': 'text/html',
256 'Content-Length': '5',
258 self
.assertResponse('500 Internal Server Error', expected_headers
, 'error',
259 self
.proxy
.handle
, environ
,
260 url_map
=self
.url_map
,
261 match
=re
.match(self
.url_map
.url
, '/get%20error'),
262 request_id
='request id',
263 request_type
=instance
.NORMAL_REQUEST
)
266 def test_handle_with_error_no_error_handler(self
):
267 self
.proxy
= http_proxy
.HttpProxy(
268 host
='localhost', port
=23456,
269 instance_died_unexpectedly
=lambda: False,
270 instance_logs_getter
=get_instance_logs
,
271 error_handler_file
=None)
272 response
= FakeHttpResponse(
273 500, 'Internal Server Error',
274 [(http_runtime_constants
.ERROR_CODE_HEADER
, '1')], '')
275 login
.get_user_info(None).AndReturn(('', False, ''))
276 httplib
.HTTPConnection
.connect()
277 httplib
.HTTPConnection
.request(
278 'GET', '/get%20error', '',
280 http_runtime_constants
.REQUEST_ID_HEADER
: 'request id',
281 'X-AppEngine-Country': 'ZZ',
282 'X-Appengine-User-Email': '',
283 'X-Appengine-User-Id': '',
284 'X-Appengine-User-Is-Admin': '0',
285 'X-Appengine-User-Nickname': '',
286 'X-Appengine-User-Organization': '',
287 'X-APPENGINE-DEV-SCRIPT': 'get.py',
288 'X-APPENGINE-SERVER-NAME': 'localhost',
289 'X-APPENGINE-SERVER-PORT': '8080',
290 'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
292 httplib
.HTTPConnection
.getresponse().AndReturn(response
)
293 httplib
.HTTPConnection
.close()
294 environ
= {'HTTP_HEADER': 'value', 'PATH_INFO': '/get error',
296 'HTTP_X_APPENGINE_USER_ID': '123',
297 'SERVER_NAME': 'localhost',
298 'SERVER_PORT': '8080',
299 'SERVER_PROTOCOL': 'HTTP/1.1',
302 self
.assertResponse('500 Internal Server Error', {}, '',
303 self
.proxy
.handle
, environ
,
304 url_map
=self
.url_map
,
305 match
=re
.match(self
.url_map
.url
, '/get%20error'),
306 request_id
='request id',
307 request_type
=instance
.NORMAL_REQUEST
)
310 def test_handle_with_error_missing_error_handler(self
):
311 error_handler_file
= os
.path
.join(self
.tmpdir
, 'error.html')
313 self
.proxy
= http_proxy
.HttpProxy(
314 host
='localhost', port
=23456,
315 instance_died_unexpectedly
=lambda: False,
316 instance_logs_getter
=get_instance_logs
,
317 error_handler_file
=error_handler_file
)
319 response
= FakeHttpResponse(
320 500, 'Internal Server Error',
321 [(http_runtime_constants
.ERROR_CODE_HEADER
, '1')], '')
322 login
.get_user_info(None).AndReturn(('', False, ''))
323 httplib
.HTTPConnection
.connect()
324 httplib
.HTTPConnection
.request(
325 'GET', '/get%20error', '',
327 http_runtime_constants
.REQUEST_ID_HEADER
: 'request id',
328 'X-AppEngine-Country': 'ZZ',
329 'X-Appengine-User-Email': '',
330 'X-Appengine-User-Id': '',
331 'X-Appengine-User-Is-Admin': '0',
332 'X-Appengine-User-Nickname': '',
333 'X-Appengine-User-Organization': '',
334 'X-APPENGINE-DEV-SCRIPT': 'get.py',
335 'X-APPENGINE-SERVER-NAME': 'localhost',
336 'X-APPENGINE-SERVER-PORT': '8080',
337 'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
339 httplib
.HTTPConnection
.getresponse().AndReturn(response
)
340 httplib
.HTTPConnection
.close()
341 environ
= {'HTTP_HEADER': 'value', 'PATH_INFO': '/get error',
343 'HTTP_X_APPENGINE_USER_ID': '123',
344 'SERVER_NAME': 'localhost',
345 'SERVER_PORT': '8080',
346 'SERVER_PROTOCOL': 'HTTP/1.1',
350 'Content-Type': 'text/html',
351 'Content-Length': '28',
353 self
.assertResponse('500 Internal Server Error', expected_headers
,
354 'Failed to load error handler', self
.proxy
.handle
,
355 environ
, url_map
=self
.url_map
,
356 match
=re
.match(self
.url_map
.url
, '/get%20error'),
357 request_id
='request id',
358 request_type
=instance
.NORMAL_REQUEST
)
361 def test_http_response_early_failure(self
):
362 header
= ('the runtime process gave a bad HTTP response: '
363 'IncompleteRead(0 bytes read)\n\n')
365 return "I'm sorry, Dave. I'm afraid I can't do that.\n"
367 self
.proxy
= http_proxy
.HttpProxy(
368 host
='localhost', port
=23456,
369 instance_died_unexpectedly
=lambda: False,
370 instance_logs_getter
=dave_message
,
371 error_handler_file
=None)
373 login
.get_user_info(None).AndReturn(('', False, ''))
374 httplib
.HTTPConnection
.connect()
375 httplib
.HTTPConnection
.request(
376 'GET', '/get%20request?key=value', '',
378 http_runtime_constants
.REQUEST_ID_HEADER
: 'request id',
379 'X-AppEngine-Country': 'ZZ',
380 'X-Appengine-User-Email': '',
381 'X-Appengine-User-Id': '',
382 'X-Appengine-User-Is-Admin': '0',
383 'X-Appengine-User-Nickname': '',
384 'X-Appengine-User-Organization': '',
385 'X-APPENGINE-DEV-SCRIPT': 'get.py',
386 'X-APPENGINE-SERVER-NAME': 'localhost',
387 'X-APPENGINE-SERVER-PORT': '8080',
388 'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
390 httplib
.HTTPConnection
.getresponse().AndRaise(httplib
.IncompleteRead(''))
391 httplib
.HTTPConnection
.close()
392 environ
= {'HTTP_HEADER': 'value', 'PATH_INFO': '/get request',
393 'QUERY_STRING': 'key=value',
394 'HTTP_X_APPENGINE_USER_ID': '123',
395 'SERVER_NAME': 'localhost',
396 'SERVER_PORT': '8080',
397 'SERVER_PROTOCOL': 'HTTP/1.1',
401 'Content-Type': 'text/plain',
402 'Content-Length': '%d' % (len(header
) + len(dave_message()))
405 self
.assertResponse('500 Internal Server Error', expected_headers
,
406 header
+ dave_message(),
407 self
.proxy
.handle
, environ
,
408 url_map
=self
.url_map
,
409 match
=re
.match(self
.url_map
.url
, '/get%20request'),
410 request_id
='request id',
411 request_type
=instance
.NORMAL_REQUEST
)
414 def test_http_response_late_failure(self
):
415 line0
= "I know I've made some very poor decisions recently...\n"
417 return "I'm afraid. I'm afraid, Dave.\n"
419 self
.proxy
= http_proxy
.HttpProxy(
420 host
='localhost', port
=23456,
421 instance_died_unexpectedly
=lambda: False,
422 instance_logs_getter
=dave_message
,
423 error_handler_file
=None)
425 response
= FakeHttpResponse(200, 'OK', [], line0
)
426 response
.partial_read_error
= httplib
.IncompleteRead('')
427 login
.get_user_info(None).AndReturn(('', False, ''))
428 httplib
.HTTPConnection
.connect()
429 httplib
.HTTPConnection
.request(
430 'GET', '/get%20request?key=value', '',
432 http_runtime_constants
.REQUEST_ID_HEADER
: 'request id',
433 'X-AppEngine-Country': 'ZZ',
434 'X-Appengine-User-Email': '',
435 'X-Appengine-User-Id': '',
436 'X-Appengine-User-Is-Admin': '0',
437 'X-Appengine-User-Nickname': '',
438 'X-Appengine-User-Organization': '',
439 'X-APPENGINE-DEV-SCRIPT': 'get.py',
440 'X-APPENGINE-SERVER-NAME': 'localhost',
441 'X-APPENGINE-SERVER-PORT': '8080',
442 'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
444 httplib
.HTTPConnection
.getresponse().AndReturn(response
)
445 httplib
.HTTPConnection
.close()
446 environ
= {'HTTP_HEADER': 'value', 'PATH_INFO': '/get request',
447 'QUERY_STRING': 'key=value',
448 'HTTP_X_APPENGINE_USER_ID': '123',
449 'SERVER_NAME': 'localhost',
450 'SERVER_PORT': '8080',
451 'SERVER_PROTOCOL': 'HTTP/1.1',
454 self
.assertResponse('200 OK', {},
456 self
.proxy
.handle
, environ
,
457 url_map
=self
.url_map
,
458 match
=re
.match(self
.url_map
.url
, '/get%20request'),
459 request_id
='request id',
460 request_type
=instance
.NORMAL_REQUEST
)
463 def test_connection_error(self
):
464 login
.get_user_info(None).AndReturn(('', False, ''))
465 httplib
.HTTPConnection
.connect().AndRaise(socket
.error())
466 httplib
.HTTPConnection
.close()
469 self
.assertRaises(socket
.error
,
472 start_response
=None, # Not used.
473 url_map
=self
.url_map
,
474 match
=re
.match(self
.url_map
.url
, '/get%20error'),
475 request_id
='request id',
476 request_type
=instance
.NORMAL_REQUEST
).next
)
479 def test_connection_error_process_quit(self
):
480 self
.proxy
= http_proxy
.HttpProxy(
481 host
='localhost', port
=123,
482 instance_died_unexpectedly
=lambda: True,
483 instance_logs_getter
=get_instance_logs
,
484 error_handler_file
=None)
485 login
.get_user_info(None).AndReturn(('', False, ''))
486 httplib
.HTTPConnection
.connect().AndRaise(socket
.error())
487 httplib
.HTTPConnection
.close()
491 'Content-Type': 'text/plain',
492 'Content-Length': '78',
494 expected_content
= ('the runtime process for the instance running on port '
495 '123 has unexpectedly quit')
496 self
.assertResponse('500 Internal Server Error',
501 url_map
=self
.url_map
,
502 match
=re
.match(self
.url_map
.url
, '/get%20error'),
503 request_id
='request id',
504 request_type
=instance
.NORMAL_REQUEST
)
507 def test_handle_background_thread(self
):
508 response
= FakeHttpResponse(200, 'OK', [('Foo', 'Bar')], 'response')
509 login
.get_user_info(None).AndReturn(('', False, ''))
510 httplib
.HTTPConnection
.connect()
511 httplib
.HTTPConnection
.request(
512 'GET', '/get%20request?key=value', '',
514 http_runtime_constants
.REQUEST_ID_HEADER
: 'request id',
515 'X-AppEngine-Country': 'ZZ',
516 'X-Appengine-User-Email': '',
517 'X-Appengine-User-Id': '',
518 'X-Appengine-User-Is-Admin': '0',
519 'X-Appengine-User-Nickname': '',
520 'X-Appengine-User-Organization': '',
521 'X-APPENGINE-DEV-SCRIPT': 'get.py',
522 'X-APPENGINE-DEV-REQUEST-TYPE': 'background',
523 'X-APPENGINE-SERVER-NAME': 'localhost',
524 'X-APPENGINE-SERVER-PORT': '8080',
525 'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
527 httplib
.HTTPConnection
.getresponse().AndReturn(response
)
528 httplib
.HTTPConnection
.close()
529 environ
= {'HTTP_HEADER': 'value', 'PATH_INFO': '/get request',
530 'QUERY_STRING': 'key=value',
531 'HTTP_X_APPENGINE_USER_ID': '123',
532 'SERVER_NAME': 'localhost',
533 'SERVER_PORT': '8080',
534 'SERVER_PROTOCOL': 'HTTP/1.1',
540 self
.assertResponse('200 OK', expected_headers
, 'response',
541 self
.proxy
.handle
, environ
,
542 url_map
=self
.url_map
,
543 match
=re
.match(self
.url_map
.url
, '/get%20request'),
544 request_id
='request id',
545 request_type
=instance
.BACKGROUND_REQUEST
)
548 def test_prior_error(self
):
549 error
= 'Oh no! Something is broken again!'
550 self
.proxy
= http_proxy
.HttpProxy(
551 host
=None, port
=None,
552 instance_died_unexpectedly
=None,
553 instance_logs_getter
=get_instance_logs
,
554 error_handler_file
=None,
557 # Expect that wait_for_connection does not hang.
558 self
.proxy
.wait_for_connection()
561 'Content-Type': 'text/plain',
562 'Content-Length': str(len(error
)),
564 self
.assertResponse('500 Internal Server Error', expected_headers
,
566 self
.proxy
.handle
, {},
567 url_map
=self
.url_map
,
568 match
=re
.match(self
.url_map
.url
, '/get%20request'),
569 request_id
='request id',
570 request_type
=instance
.NORMAL_REQUEST
)
574 if __name__
== '__main__':