#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Tests for google.appengine.tools.devappserver2.http_proxy."""

import cStringIO
import httplib
import os
import re
import shutil
import socket
import tempfile
import unittest

import google

import mox

from google.appengine.api import appinfo
from google.appengine.tools.devappserver2 import http_proxy
from google.appengine.tools.devappserver2 import http_runtime_constants
from google.appengine.tools.devappserver2 import instance
from google.appengine.tools.devappserver2 import login
from google.appengine.tools.devappserver2 import wsgi_test_utils


class MockMessage(object):
  def __init__(self, headers):
    self.headers = headers

  def __iter__(self):
    return iter(set(name for name, _ in self.headers))

  def getheaders(self, name):
    return [value for header_name, value in self.headers if header_name == name]


class FakeHttpResponse(object):
  def __init__(self, status, reason, headers, body):
    self.body = body
    self.has_read = False
    self.partial_read_error = None
    self.status = status
    self.reason = reason
    self.headers = headers
    self.msg = MockMessage(headers)

  def read(self, amt=None):
    if not self.has_read:
      self.has_read = True
      return self.body
    elif self.partial_read_error:
      raise self.partial_read_error
    else:
      return ''

  def getheaders(self):
    return self.headers


def get_instance_logs():
  return ''


class HttpProxyTest(wsgi_test_utils.WSGITestCase):
  def setUp(self):
    self.mox = mox.Mox()
    self.tmpdir = tempfile.mkdtemp()

    self.proxy = http_proxy.HttpProxy(
        host='localhost', port=23456,
        instance_died_unexpectedly=lambda: False,
        instance_logs_getter=get_instance_logs,
        error_handler_file=None)

    self.mox.StubOutWithMock(httplib.HTTPConnection, 'connect')
    self.mox.StubOutWithMock(httplib.HTTPConnection, 'request')
    self.mox.StubOutWithMock(httplib.HTTPConnection, 'getresponse')
    self.mox.StubOutWithMock(httplib.HTTPConnection, 'close')
    self.mox.StubOutWithMock(login, 'get_user_info')
    self.url_map = appinfo.URLMap(url=r'/(get|post).*',
                                  script=r'\1.py')

  def tearDown(self):
    shutil.rmtree(self.tmpdir)
    self.mox.UnsetStubs()

  def test_wait_for_connection_retries_used_up(self):
    retries = 5
    for _ in xrange(0, retries + 1):
      httplib.HTTPConnection.connect().AndRaise(socket.error)
      httplib.HTTPConnection.close()

    self.mox.ReplayAll()
    self.assertRaises(http_proxy.HostNotReachable,
                      self.proxy.wait_for_connection, retries)
    self.mox.VerifyAll()

  def test_wait_for_connection_worked(self):
    retries = 5
    for _ in xrange(0, retries):
      httplib.HTTPConnection.connect().AndRaise(socket.error)
      httplib.HTTPConnection.close()

    httplib.HTTPConnection.connect()
    httplib.HTTPConnection.close()

    self.mox.ReplayAll()
    self.proxy.wait_for_connection(retries + 1)
    self.mox.VerifyAll()

  def test_handle_get(self):
    response = FakeHttpResponse(200,
                                'OK',
                                [('Foo', 'a'), ('Foo', 'b'), ('Var', 'c')],
                                'response')
    login.get_user_info(None).AndReturn(('', False, ''))
    httplib.HTTPConnection.connect()
    httplib.HTTPConnection.request(
        'GET', '/get%20request?key=value', '',
        {'HEADER': 'value',
         http_runtime_constants.REQUEST_ID_HEADER: 'request id',
         'X-AppEngine-Country': 'ZZ',
         'X-Appengine-User-Email': '',
         'X-Appengine-User-Id': '',
         'X-Appengine-User-Is-Admin': '0',
         'X-Appengine-User-Nickname': '',
         'X-Appengine-User-Organization': '',
         'X-APPENGINE-DEV-SCRIPT': 'get.py',
         'X-APPENGINE-SERVER-NAME': 'localhost',
         'X-APPENGINE-SERVER-PORT': '8080',
         'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
        })
    httplib.HTTPConnection.getresponse().AndReturn(response)
    httplib.HTTPConnection.close()
    environ = {'HTTP_HEADER': 'value', 'PATH_INFO': '/get request',
               'QUERY_STRING': 'key=value',
               'HTTP_X_APPENGINE_USER_ID': '123',
               'SERVER_NAME': 'localhost',
               'SERVER_PORT': '8080',
               'SERVER_PROTOCOL': 'HTTP/1.1',
              }
    self.mox.ReplayAll()
    expected_headers = [('Foo', 'a'), ('Foo', 'b'), ('Var', 'c')]
    self.assertResponse('200 OK', expected_headers, 'response',
                        self.proxy.handle, environ,
                        url_map=self.url_map,
                        match=re.match(self.url_map.url, '/get%20request'),
                        request_id='request id',
                        request_type=instance.NORMAL_REQUEST)
    self.mox.VerifyAll()

  def test_handle_post(self):
    response = FakeHttpResponse(200,
                                'OK',
                                [('Foo', 'a'), ('Foo', 'b'), ('Var', 'c')],
                                'response')
    login.get_user_info('cookie').AndReturn(('user@example.com', True, '12345'))
    httplib.HTTPConnection.connect()
    httplib.HTTPConnection.request(
        'POST', '/post', 'post data',
        {'HEADER': 'value',
         'COOKIE': 'cookie',
         'CONTENT-TYPE': 'text/plain',
         'CONTENT-LENGTH': '9',
         http_runtime_constants.REQUEST_ID_HEADER: 'request id',
         'X-AppEngine-Country': 'ZZ',
         'X-Appengine-User-Email': 'user@example.com',
         'X-Appengine-User-Id': '12345',
         'X-Appengine-User-Is-Admin': '1',
         'X-Appengine-User-Nickname': 'user',
         'X-Appengine-User-Organization': 'example.com',
         'X-APPENGINE-DEV-SCRIPT': 'post.py',
         'X-APPENGINE-SERVER-NAME': 'localhost',
         'X-APPENGINE-SERVER-PORT': '8080',
         'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
        })
    httplib.HTTPConnection.getresponse().AndReturn(response)
    httplib.HTTPConnection.close()
    environ = {'HTTP_HEADER': 'value', 'PATH_INFO': '/post',
               'wsgi.input': cStringIO.StringIO('post data'),
               'CONTENT_LENGTH': '9',
               'CONTENT_TYPE': 'text/plain',
               'REQUEST_METHOD': 'POST',
               'HTTP_COOKIE': 'cookie',
               'SERVER_NAME': 'localhost',
               'SERVER_PORT': '8080',
               'SERVER_PROTOCOL': 'HTTP/1.1',
              }
    self.mox.ReplayAll()
    expected_headers = [('Foo', 'a'), ('Foo', 'b'), ('Var', 'c')]
    self.assertResponse('200 OK', expected_headers, 'response',
                        self.proxy.handle, environ,
                        url_map=self.url_map,
                        match=re.match(self.url_map.url, '/post'),
                        request_id='request id',
                        request_type=instance.NORMAL_REQUEST)
    self.mox.VerifyAll()

  def test_handle_with_error(self):
    error_handler_file = os.path.join(self.tmpdir, 'error.html')
    with open(error_handler_file, 'w') as f:
      f.write('error')

    self.proxy = http_proxy.HttpProxy(
        host='localhost', port=23456,
        instance_died_unexpectedly=lambda: False,
        instance_logs_getter=get_instance_logs,
        error_handler_file=error_handler_file)

    response = FakeHttpResponse(
        500, 'Internal Server Error',
        [(http_runtime_constants.ERROR_CODE_HEADER, '1')], '')
    login.get_user_info(None).AndReturn(('', False, ''))
    httplib.HTTPConnection.connect()
    httplib.HTTPConnection.request(
        'GET', '/get%20error', '',
        {'HEADER': 'value',
         http_runtime_constants.REQUEST_ID_HEADER: 'request id',
         'X-AppEngine-Country': 'ZZ',
         'X-Appengine-User-Email': '',
         'X-Appengine-User-Id': '',
         'X-Appengine-User-Is-Admin': '0',
         'X-Appengine-User-Nickname': '',
         'X-Appengine-User-Organization': '',
         'X-APPENGINE-DEV-SCRIPT': 'get.py',
         'X-APPENGINE-SERVER-NAME': 'localhost',
         'X-APPENGINE-SERVER-PORT': '8080',
         'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
        })
    httplib.HTTPConnection.getresponse().AndReturn(response)
    httplib.HTTPConnection.close()
    environ = {'HTTP_HEADER': 'value', 'PATH_INFO': '/get error',
               'QUERY_STRING': '',
               'HTTP_X_APPENGINE_USER_ID': '123',
               'SERVER_NAME': 'localhost',
               'SERVER_PORT': '8080',
               'SERVER_PROTOCOL': 'HTTP/1.1',
              }
    self.mox.ReplayAll()
    expected_headers = {
        'Content-Type': 'text/html',
        'Content-Length': '5',
    }
    self.assertResponse('500 Internal Server Error', expected_headers, 'error',
                        self.proxy.handle, environ,
                        url_map=self.url_map,
                        match=re.match(self.url_map.url, '/get%20error'),
                        request_id='request id',
                        request_type=instance.NORMAL_REQUEST)
    self.mox.VerifyAll()

  def test_handle_with_error_no_error_handler(self):
    self.proxy = http_proxy.HttpProxy(
        host='localhost', port=23456,
        instance_died_unexpectedly=lambda: False,
        instance_logs_getter=get_instance_logs,
        error_handler_file=None)
    response = FakeHttpResponse(
        500, 'Internal Server Error',
        [(http_runtime_constants.ERROR_CODE_HEADER, '1')], '')
    login.get_user_info(None).AndReturn(('', False, ''))
    httplib.HTTPConnection.connect()
    httplib.HTTPConnection.request(
        'GET', '/get%20error', '',
        {'HEADER': 'value',
         http_runtime_constants.REQUEST_ID_HEADER: 'request id',
         'X-AppEngine-Country': 'ZZ',
         'X-Appengine-User-Email': '',
         'X-Appengine-User-Id': '',
         'X-Appengine-User-Is-Admin': '0',
         'X-Appengine-User-Nickname': '',
         'X-Appengine-User-Organization': '',
         'X-APPENGINE-DEV-SCRIPT': 'get.py',
         'X-APPENGINE-SERVER-NAME': 'localhost',
         'X-APPENGINE-SERVER-PORT': '8080',
         'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
        })
    httplib.HTTPConnection.getresponse().AndReturn(response)
    httplib.HTTPConnection.close()
    environ = {'HTTP_HEADER': 'value', 'PATH_INFO': '/get error',
               'QUERY_STRING': '',
               'HTTP_X_APPENGINE_USER_ID': '123',
               'SERVER_NAME': 'localhost',
               'SERVER_PORT': '8080',
               'SERVER_PROTOCOL': 'HTTP/1.1',
              }
    self.mox.ReplayAll()
    self.assertResponse('500 Internal Server Error', {}, '',
                        self.proxy.handle, environ,
                        url_map=self.url_map,
                        match=re.match(self.url_map.url, '/get%20error'),
                        request_id='request id',
                        request_type=instance.NORMAL_REQUEST)
    self.mox.VerifyAll()

  def test_handle_with_error_missing_error_handler(self):
    error_handler_file = os.path.join(self.tmpdir, 'error.html')

    self.proxy = http_proxy.HttpProxy(
        host='localhost', port=23456,
        instance_died_unexpectedly=lambda: False,
        instance_logs_getter=get_instance_logs,
        error_handler_file=error_handler_file)

    response = FakeHttpResponse(
        500, 'Internal Server Error',
        [(http_runtime_constants.ERROR_CODE_HEADER, '1')], '')
    login.get_user_info(None).AndReturn(('', False, ''))
    httplib.HTTPConnection.connect()
    httplib.HTTPConnection.request(
        'GET', '/get%20error', '',
        {'HEADER': 'value',
         http_runtime_constants.REQUEST_ID_HEADER: 'request id',
         'X-AppEngine-Country': 'ZZ',
         'X-Appengine-User-Email': '',
         'X-Appengine-User-Id': '',
         'X-Appengine-User-Is-Admin': '0',
         'X-Appengine-User-Nickname': '',
         'X-Appengine-User-Organization': '',
         'X-APPENGINE-DEV-SCRIPT': 'get.py',
         'X-APPENGINE-SERVER-NAME': 'localhost',
         'X-APPENGINE-SERVER-PORT': '8080',
         'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
        })
    httplib.HTTPConnection.getresponse().AndReturn(response)
    httplib.HTTPConnection.close()
    environ = {'HTTP_HEADER': 'value', 'PATH_INFO': '/get error',
               'QUERY_STRING': '',
               'HTTP_X_APPENGINE_USER_ID': '123',
               'SERVER_NAME': 'localhost',
               'SERVER_PORT': '8080',
               'SERVER_PROTOCOL': 'HTTP/1.1',
              }
    self.mox.ReplayAll()
    expected_headers = {
        'Content-Type': 'text/html',
        'Content-Length': '28',
    }
    self.assertResponse('500 Internal Server Error', expected_headers,
                        'Failed to load error handler', self.proxy.handle,
                        environ, url_map=self.url_map,
                        match=re.match(self.url_map.url, '/get%20error'),
                        request_id='request id',
                        request_type=instance.NORMAL_REQUEST)
    self.mox.VerifyAll()

  def test_http_response_early_failure(self):
    header = ('the runtime process gave a bad HTTP response: '
              'IncompleteRead(0 bytes read)\n\n')
    def dave_message():
      return "I'm sorry, Dave. I'm afraid I can't do that.\n"

    self.proxy = http_proxy.HttpProxy(
        host='localhost', port=23456,
        instance_died_unexpectedly=lambda: False,
        instance_logs_getter=dave_message,
        error_handler_file=None)

    login.get_user_info(None).AndReturn(('', False, ''))
    httplib.HTTPConnection.connect()
    httplib.HTTPConnection.request(
        'GET', '/get%20request?key=value', '',
        {'HEADER': 'value',
         http_runtime_constants.REQUEST_ID_HEADER: 'request id',
         'X-AppEngine-Country': 'ZZ',
         'X-Appengine-User-Email': '',
         'X-Appengine-User-Id': '',
         'X-Appengine-User-Is-Admin': '0',
         'X-Appengine-User-Nickname': '',
         'X-Appengine-User-Organization': '',
         'X-APPENGINE-DEV-SCRIPT': 'get.py',
         'X-APPENGINE-SERVER-NAME': 'localhost',
         'X-APPENGINE-SERVER-PORT': '8080',
         'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
        })
    httplib.HTTPConnection.getresponse().AndRaise(httplib.IncompleteRead(''))
    httplib.HTTPConnection.close()
    environ = {'HTTP_HEADER': 'value', 'PATH_INFO': '/get request',
               'QUERY_STRING': 'key=value',
               'HTTP_X_APPENGINE_USER_ID': '123',
               'SERVER_NAME': 'localhost',
               'SERVER_PORT': '8080',
               'SERVER_PROTOCOL': 'HTTP/1.1',
              }
    self.mox.ReplayAll()
    expected_headers = {
        'Content-Type': 'text/plain',
        'Content-Length': '%d' % (len(header) + len(dave_message()))
    }

    self.assertResponse('500 Internal Server Error', expected_headers,
                        header + dave_message(),
                        self.proxy.handle, environ,
                        url_map=self.url_map,
                        match=re.match(self.url_map.url, '/get%20request'),
                        request_id='request id',
                        request_type=instance.NORMAL_REQUEST)
    self.mox.VerifyAll()

  def test_http_response_late_failure(self):
    line0 = "I know I've made some very poor decisions recently...\n"
    def dave_message():
      return "I'm afraid. I'm afraid, Dave.\n"

    self.proxy = http_proxy.HttpProxy(
        host='localhost', port=23456,
        instance_died_unexpectedly=lambda: False,
        instance_logs_getter=dave_message,
        error_handler_file=None)

    response = FakeHttpResponse(200, 'OK', [], line0)
    response.partial_read_error = httplib.IncompleteRead('')
    login.get_user_info(None).AndReturn(('', False, ''))
    httplib.HTTPConnection.connect()
    httplib.HTTPConnection.request(
        'GET', '/get%20request?key=value', '',
        {'HEADER': 'value',
         http_runtime_constants.REQUEST_ID_HEADER: 'request id',
         'X-AppEngine-Country': 'ZZ',
         'X-Appengine-User-Email': '',
         'X-Appengine-User-Id': '',
         'X-Appengine-User-Is-Admin': '0',
         'X-Appengine-User-Nickname': '',
         'X-Appengine-User-Organization': '',
         'X-APPENGINE-DEV-SCRIPT': 'get.py',
         'X-APPENGINE-SERVER-NAME': 'localhost',
         'X-APPENGINE-SERVER-PORT': '8080',
         'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
        })
    httplib.HTTPConnection.getresponse().AndReturn(response)
    httplib.HTTPConnection.close()
    environ = {'HTTP_HEADER': 'value', 'PATH_INFO': '/get request',
               'QUERY_STRING': 'key=value',
               'HTTP_X_APPENGINE_USER_ID': '123',
               'SERVER_NAME': 'localhost',
               'SERVER_PORT': '8080',
               'SERVER_PROTOCOL': 'HTTP/1.1',
              }
    self.mox.ReplayAll()
    self.assertResponse('200 OK', {},
                        line0,
                        self.proxy.handle, environ,
                        url_map=self.url_map,
                        match=re.match(self.url_map.url, '/get%20request'),
                        request_id='request id',
                        request_type=instance.NORMAL_REQUEST)
    self.mox.VerifyAll()

  def test_connection_error(self):
    login.get_user_info(None).AndReturn(('', False, ''))
    httplib.HTTPConnection.connect().AndRaise(socket.error())
    httplib.HTTPConnection.close()

    self.mox.ReplayAll()
    self.assertRaises(socket.error,
                      self.proxy.handle(
                          {'PATH_INFO': '/'},
                          start_response=None,  # Not used.
                          url_map=self.url_map,
                          match=re.match(self.url_map.url, '/get%20error'),
                          request_id='request id',
                          request_type=instance.NORMAL_REQUEST).next)
    self.mox.VerifyAll()

  def test_connection_error_process_quit(self):
    self.proxy = http_proxy.HttpProxy(
        host='localhost', port=123,
        instance_died_unexpectedly=lambda: True,
        instance_logs_getter=get_instance_logs,
        error_handler_file=None)
    login.get_user_info(None).AndReturn(('', False, ''))
    httplib.HTTPConnection.connect().AndRaise(socket.error())
    httplib.HTTPConnection.close()

    self.mox.ReplayAll()
    expected_headers = {
        'Content-Type': 'text/plain',
        'Content-Length': '78',
    }
    expected_content = ('the runtime process for the instance running on port '
                        '123 has unexpectedly quit')
    self.assertResponse('500 Internal Server Error',
                        expected_headers,
                        expected_content,
                        self.proxy.handle,
                        {'PATH_INFO': '/'},
                        url_map=self.url_map,
                        match=re.match(self.url_map.url, '/get%20error'),
                        request_id='request id',
                        request_type=instance.NORMAL_REQUEST)
    self.mox.VerifyAll()

  def test_handle_background_thread(self):
    response = FakeHttpResponse(200, 'OK', [('Foo', 'Bar')], 'response')
    login.get_user_info(None).AndReturn(('', False, ''))
    httplib.HTTPConnection.connect()
    httplib.HTTPConnection.request(
        'GET', '/get%20request?key=value', '',
        {'HEADER': 'value',
         http_runtime_constants.REQUEST_ID_HEADER: 'request id',
         'X-AppEngine-Country': 'ZZ',
         'X-Appengine-User-Email': '',
         'X-Appengine-User-Id': '',
         'X-Appengine-User-Is-Admin': '0',
         'X-Appengine-User-Nickname': '',
         'X-Appengine-User-Organization': '',
         'X-APPENGINE-DEV-SCRIPT': 'get.py',
         'X-APPENGINE-DEV-REQUEST-TYPE': 'background',
         'X-APPENGINE-SERVER-NAME': 'localhost',
         'X-APPENGINE-SERVER-PORT': '8080',
         'X-APPENGINE-SERVER-PROTOCOL': 'HTTP/1.1',
        })
    httplib.HTTPConnection.getresponse().AndReturn(response)
    httplib.HTTPConnection.close()
    environ = {'HTTP_HEADER': 'value', 'PATH_INFO': '/get request',
               'QUERY_STRING': 'key=value',
               'HTTP_X_APPENGINE_USER_ID': '123',
               'SERVER_NAME': 'localhost',
               'SERVER_PORT': '8080',
               'SERVER_PROTOCOL': 'HTTP/1.1',
              }
    self.mox.ReplayAll()
    expected_headers = {
        'Foo': 'Bar',
    }
    self.assertResponse('200 OK', expected_headers, 'response',
                        self.proxy.handle, environ,
                        url_map=self.url_map,
                        match=re.match(self.url_map.url, '/get%20request'),
                        request_id='request id',
                        request_type=instance.BACKGROUND_REQUEST)
    self.mox.VerifyAll()

  def test_prior_error(self):
    error = 'Oh no! Something is broken again!'
    self.proxy = http_proxy.HttpProxy(
        host=None, port=None,
        instance_died_unexpectedly=None,
        instance_logs_getter=get_instance_logs,
        error_handler_file=None,
        prior_error=error)

    expected_headers = {
        'Content-Type': 'text/plain',
        'Content-Length': str(len(error)),
    }
    self.assertResponse('500 Internal Server Error', expected_headers,
                        error,
                        self.proxy.handle, {},
                        url_map=self.url_map,
                        match=re.match(self.url_map.url, '/get%20request'),
                        request_id='request id',
                        request_type=instance.NORMAL_REQUEST)
    self.mox.VerifyAll()


if __name__ == '__main__':
  unittest.main()
