App Engine Python SDK version 1.9.12
[gae.git] / python / google / appengine / tools / devappserver2 / http_proxy_test.py
blob6281d3ce255d893b417e7bb0191cb700d256866f
1 #!/usr/bin/env python
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."""
19 import cStringIO
20 import httplib
21 import os
22 import re
23 import shutil
24 import socket
25 import tempfile
26 import unittest
28 import google
30 import mox
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
44 def __iter__(self):
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):
53 self.body = body
54 self.has_read = False
55 self.partial_read_error = None
56 self.status = status
57 self.reason = reason
58 self.headers = headers
59 self.msg = MockMessage(headers)
61 def read(self, amt=None):
62 if not self.has_read:
63 self.has_read = True
64 return self.body
65 elif self.partial_read_error:
66 raise self.partial_read_error
67 else:
68 return ''
70 def getheaders(self):
71 return self.headers
74 def get_instance_logs():
75 return ''
78 class HttpProxyTest(wsgi_test_utils.WSGITestCase):
79 def setUp(self):
80 self.mox = mox.Mox()
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).*',
95 script=r'\1.py')
97 def tearDown(self):
98 shutil.rmtree(self.tmpdir)
99 self.mox.UnsetStubs()
101 def test_wait_for_connection_retries_used_up(self):
102 retries = 5
103 for _ in xrange(0, retries + 1):
104 httplib.HTTPConnection.connect().AndRaise(socket.error)
105 httplib.HTTPConnection.close()
107 self.mox.ReplayAll()
108 self.assertRaises(http_proxy.HostNotReachable,
109 self.proxy.wait_for_connection, retries)
110 self.mox.VerifyAll()
112 def test_wait_for_connection_worked(self):
113 retries = 5
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()
121 self.mox.ReplayAll()
122 self.proxy.wait_for_connection(retries + 1)
123 self.mox.VerifyAll()
125 def test_handle_get(self):
126 response = FakeHttpResponse(200,
127 'OK',
128 [('Foo', 'a'), ('Foo', 'b'), ('Var', 'c')],
129 'response')
130 login.get_user_info(None).AndReturn(('', False, ''))
131 httplib.HTTPConnection.connect()
132 httplib.HTTPConnection.request(
133 'GET', '/get%20request?key=value', '',
134 {'HEADER': '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',
156 self.mox.ReplayAll()
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)
164 self.mox.VerifyAll()
166 def test_handle_post(self):
167 response = FakeHttpResponse(200,
168 'OK',
169 [('Foo', 'a'), ('Foo', 'b'), ('Var', 'c')],
170 'response')
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',
175 {'HEADER': 'value',
176 'COOKIE': 'cookie',
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',
203 self.mox.ReplayAll()
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)
211 self.mox.VerifyAll()
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:
216 f.write('error')
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', '',
231 {'HEADER': 'value',
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',
247 'QUERY_STRING': '',
248 'HTTP_X_APPENGINE_USER_ID': '123',
249 'SERVER_NAME': 'localhost',
250 'SERVER_PORT': '8080',
251 'SERVER_PROTOCOL': 'HTTP/1.1',
253 self.mox.ReplayAll()
254 expected_headers = {
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)
264 self.mox.VerifyAll()
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', '',
279 {'HEADER': 'value',
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',
295 'QUERY_STRING': '',
296 'HTTP_X_APPENGINE_USER_ID': '123',
297 'SERVER_NAME': 'localhost',
298 'SERVER_PORT': '8080',
299 'SERVER_PROTOCOL': 'HTTP/1.1',
301 self.mox.ReplayAll()
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)
308 self.mox.VerifyAll()
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', '',
326 {'HEADER': 'value',
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',
342 'QUERY_STRING': '',
343 'HTTP_X_APPENGINE_USER_ID': '123',
344 'SERVER_NAME': 'localhost',
345 'SERVER_PORT': '8080',
346 'SERVER_PROTOCOL': 'HTTP/1.1',
348 self.mox.ReplayAll()
349 expected_headers = {
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)
359 self.mox.VerifyAll()
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')
364 def dave_message():
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', '',
377 {'HEADER': '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',
399 self.mox.ReplayAll()
400 expected_headers = {
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)
412 self.mox.VerifyAll()
414 def test_http_response_late_failure(self):
415 line0 = "I know I've made some very poor decisions recently...\n"
416 def dave_message():
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', '',
431 {'HEADER': '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',
453 self.mox.ReplayAll()
454 self.assertResponse('200 OK', {},
455 line0,
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)
461 self.mox.VerifyAll()
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()
468 self.mox.ReplayAll()
469 self.assertRaises(socket.error,
470 self.proxy.handle(
471 {'PATH_INFO': '/'},
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)
477 self.mox.VerifyAll()
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()
489 self.mox.ReplayAll()
490 expected_headers = {
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',
497 expected_headers,
498 expected_content,
499 self.proxy.handle,
500 {'PATH_INFO': '/'},
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)
505 self.mox.VerifyAll()
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', '',
513 {'HEADER': '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',
536 self.mox.ReplayAll()
537 expected_headers = {
538 'Foo': 'Bar',
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)
546 self.mox.VerifyAll()
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,
555 prior_error=error)
557 # Expect that wait_for_connection does not hang.
558 self.proxy.wait_for_connection()
560 expected_headers = {
561 'Content-Type': 'text/plain',
562 'Content-Length': str(len(error)),
564 self.assertResponse('500 Internal Server Error', expected_headers,
565 error,
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)
571 self.mox.VerifyAll()
574 if __name__ == '__main__':
575 unittest.main()