# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2007-2009 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.

from twisted.trial.unittest import TestCase
from twisted.internet import reactor, error, protocol
from twisted.web2.http import StatusResponse, Response
from twisted.web2.channel.http import HTTPFactory, HTTPChannel
from twisted.web2 import iweb, server
from twisted.web2 import responsecode
from twisted.web2.stream import BufferedStream, MemoryStream
from twisted.web2.http_headers import Headers

from twisted.protocols import basic
from twisted.internet import ssl
from OpenSSL import crypto, SSL

from elisa.core.utils import defer

import os
import base64
import tempfile

from zope.interface import implements

from elisa.plugins.http_client import utils
from elisa.plugins.http_client.extern.client_http import ClientRequest, \
     ProtocolError

from elisa.plugins.http_client.http_client import ElisaHttpClient, \
    ElisaAdvancedHttpClient, ElisaHttpClientNotOpenedError, \
    ElisaHttpClientFactory

SERVER_HOST = 'localhost'
SERVER_PORT = 4242
SERVER_PORT_2 = 4243
SERVER_TITLE = 'Elisa test HTTP Server'


class DummyHttpServerOtherResource(object):
    implements(iweb.IResource)

    """
    This class represents a resource served by our test HTTP server.
    Our test server is able to serve other resources than just '/'.
    """

    def __init__(self, resource_name, close=False):
        # If close is True, the server will close the connection after sending
        # the response.
        self.resource_name = resource_name
        self.close = close

    def renderHTTP(self, request):
        # Render the HTTP response. The body of the response simply contains
        # the name of the resource repeated ten times.
        response = StatusResponse(responsecode.OK, self.resource_name * 10,
                                  SERVER_TITLE)

        dummy_header = request.headers.getRawHeaders('DummyTestHeader')
        if dummy_header:
            response.headers.setRawHeaders('DummyTestHeader', dummy_header)

        if request.method == 'POST':
            # identity: send back to the client the contents of the
            # initial request body.
            response = Response(code=responsecode.OK, stream=request.stream)

        if self.close:
            # The connection will be closed by the server after sending the
            # response.
            response.headers.setHeader('Connection', ['close'])

        return response

class DummyHttpServerFattyResource(object):
    implements(iweb.IResource)

    """
    This class represents a large resource served by our test HTTP server.
    This resource contains a lot of content, and is used to test that the
    whole contents get correctly transferred.
    """

    def renderHTTP(self, request):
        # Render the HTTP response. The body of the response contains 10000
        # lines of text, each one prefixed by its line number.
        text = os.linesep.join([str(i + 1).zfill(5) + \
                                ': I am one dumb fatty line of data' \
                                for i in xrange(10000)])
        response = StatusResponse(responsecode.OK, text, SERVER_TITLE)
        return response

class DummyHttpServerRedirectResource(object):
    implements(iweb.IResource)

    """
    This class represents a resource served by our test HTTP server.
    This resource redirects to another resource with a given HTTP redirection
    code. For more details on HTTP redirection codes, see
    http://www.w3.org/Protocols/HTTP/HTRESP.html#z10
    """

    def __init__(self, http_code, resource_name, close=False):
        # If close is True, the server will close the connection after sending
        # the response.
        self.http_code = http_code
        self.resource_name = resource_name
        self.close = close

    def renderHTTP(self, request):
        # Render the HTTP response.
        response = StatusResponse(self.http_code, 'redirecting to /' + \
                                  self.resource_name, SERVER_TITLE)
        response.headers.setHeader('Location', '/' + self.resource_name)

        dummy_header = request.headers.getRawHeaders('DummyTestHeader')
        if dummy_header:
            response.headers.setRawHeaders('DummyTestHeader', dummy_header)

        if self.close:
            # The connection will be closed by the server after sending the
            # response.
            response.headers.setHeader('Connection', ['close'])
        return response

class BrokenResponse(object):
    implements(iweb.IResponse)

    def __init__(self, code=None, headers=None, stream=None):
        self.code = code
        self.headers = headers
        self.stream = stream

class DummyBrokenServerResource(object):
    implements(iweb.IResource)

    def renderHTTP(self, request):
        code = "foo"
        headers = Headers()
        response = BrokenResponse(code, headers)
        return response

crashed_server = None

class DummyHttpServerOtherResourceCrash(object):
    implements(iweb.IResource)

    """
    This class represents a resource served by our test HTTP server.
    After being requested for this resource, the server will crash.
    """

    def renderHTTP(self, request):
        # Render the HTTP response
        response = StatusResponse(responsecode.OK, 'crashed', SERVER_TITLE)

        # The connection will be closed by the server after sending the
        # response.
        response.headers.setHeader('Connection', ['close'])

        # Crash the server so that it does not respond anymore
        global crashed_server
        crashed_server.http_factory.buildProtocol = lambda addr: None

        return response

class AuthResource(object):
    implements(iweb.IResource)

    _user = 'guest'
    _password = 'guest123'

    def renderHTTP(self, request):
        """ Overridden 'render' method which takes care of
        HTTP basic authorization """

        cleartext_token = self._user + ':' + self._password
        try:
            auth_header = request.headers.getHeader('authorization')
            challenge = base64.decodestring(auth_header[1])
        except:
            code = responsecode.UNAUTHORIZED
            text = 'Authorization required!'
        else:
            if challenge != cleartext_token:
                code = responsecode.UNAUTHORIZED
                text = 'Authorization Failed!'
            else:
                code = responsecode.OK
                text = 'Authenticated'

        response = StatusResponse(code, text, SERVER_TITLE)
        return response


class DummyHttpServerRootResource(object):
    implements(iweb.IResource)

    """
    This class represents the root resource served by our test HTTP server
    ('/'). This resource is responsible for addressing the other resources (its
    children) when they are requested.
    """

    def __init__(self):
        # Children resources for our test server
        self.my_children = {'': self,
            'auth': AuthResource(),
            'foo': DummyHttpServerOtherResource('foo'),
            'bar': DummyHttpServerOtherResource('bar'),
            'fatty': DummyHttpServerFattyResource(),
            'foo_and_close': DummyHttpServerOtherResource('foo', close=True),
            'simple_redirect': DummyHttpServerRedirectResource(responsecode.MOVED_PERMANENTLY, 'foo'),
            'double_redirect': DummyHttpServerRedirectResource(responsecode.MOVED_PERMANENTLY, 'simple_redirect'),
            'redirect_and_close': DummyHttpServerRedirectResource(responsecode.MOVED_PERMANENTLY, 'foo', close=True),
            'foo_and_crash': DummyHttpServerOtherResourceCrash(),
            'broken_server': DummyBrokenServerResource()}

    def locateChild(self, request, segments):
        res = self.my_children.get(segments[0])
        if res == self:
            return (self, server.StopTraversal)
        elif res == None:
            return (None, segments)
        else:
            return (res, segments[1:])

    def renderHTTP(self, request):
        response = StatusResponse(responsecode.OK,
                                  'Server\'s root resource: /', SERVER_TITLE)
        return response

class DummyHttpServerRequest(server.Request):
    """
    A site with all the resources that need to be served by our test server.
    """
    site = server.Site(DummyHttpServerRootResource())


class SSLContextFactory(ssl.ContextFactory):

    def __init__(self):
        self.ssl_key_fd, self.ssl_key_file = tempfile.mkstemp()
        self.ssl_cert_fd, self.ssl_cert_file = tempfile.mkstemp()

    def getContext(self):
        key = open(self.ssl_key_file).read()
        if not key:
            # generate & write SSL key
            key = crypto.PKey()
            key.generate_key(crypto.TYPE_RSA, 1024)
            key_dump = crypto.dump_privatekey(crypto.FILETYPE_PEM, key)
            os.write(self.ssl_key_fd, key_dump)
            os.close(self.ssl_key_fd)

            # generate & write X509 certificate
            certificate = crypto.X509()
            certificate.gmtime_adj_notBefore(0)
            certificate.gmtime_adj_notAfter(60*60*24*365*1)
            certificate.set_pubkey(key)
            certificate.sign(key, 'md5')
            certificate_dump = crypto.dump_certificate(crypto.FILETYPE_PEM,
                                                       certificate)
            os.write(self.ssl_cert_fd, certificate_dump)
            os.close(self.ssl_cert_fd)

        context = SSL.Context(SSL.TLSv1_METHOD)
        context.use_privatekey_file(self.ssl_key_file)
        context.use_certificate_file(self.ssl_cert_file)
        return context

    def clean(self):
        os.unlink(self.ssl_key_file)
        os.unlink(self.ssl_cert_file)


def client_ssl_context():
    context = ssl.ClientContextFactory()
    context.method = SSL.TLSv1_METHOD
    return context


class ClientServerMixin(object):
    """
    A test HTTP server that listens on a given port and serves our test
    resources. A client for this server is also created.
    """

    context_factory = None
    client_class = ElisaHttpClient
    client_constructor_args = (SERVER_HOST, SERVER_PORT,)
    client_constructor_kwargs = {}

    def setUp(self):
        # Start listening
        self.http_factory = HTTPFactory(DummyHttpServerRequest)
        if self.ssl_enabled:
            self.context_factory = SSLContextFactory()
            self._port = reactor.listenSSL(SERVER_PORT, self.http_factory,
                                           self.context_factory)
        else:
            self._port = reactor.listenTCP(SERVER_PORT, self.http_factory)
        self.client = self.client_class(*self.client_constructor_args,
                                        **self.client_constructor_kwargs)

    def tearDown(self):
        def port_closed(result):
            if self.context_factory:
                self.context_factory.clean()

        def close_server(result):
            # Stop listening
            dfr = defer.maybeDeferred(self._port.stopListening)
            dfr.addCallbacks(port_closed)
            return dfr

        dfr = self.client.close()
        dfr.addCallbacks(close_server, close_server)
        return dfr

    @property
    def ssl_enabled(self):
        return 'ssl_context' in self.client_constructor_kwargs

class RawDataProtocol(HTTPChannel):
    first_line = None
    req_to_answer = {'GET /no_size HTTP/1.1' : (
                        "HTTP/1.1 200 OK\r\n"\
                        "Date: Wed, 27 Aug 2008 08:52:03 GMT\r\n" \
                        "Server: Apache/2.2.3 (CentOS)\r\n" \
                        "X-Powered-By: PHP/5.1.6\r\n" \
                        "Cache-Control: max-age=0, must-revalidate," \
                                "no-transform,private\r\n" \
                        "Vary: Accept-Encoding,User-Agent\r\n" \
                        "Connection: close\r\n" \
                        "Content-Type: application/json\r\n" \
                        "\r\n\r\n" + \
                        ' '.join(['test%s' % x for x in xrange(10000)]),
                        True),
                    'HEAD /some_thing HTTP/1.1' : (
                        "HTTP/1.1 200 OK\r\n"\
                        "Date: Wed, 27 Aug 2008 08:52:03 GMT\r\n" \
                        "Server: Apache/2.2.3 (CentOS)\r\n" \
                        "X-Powered-By: PHP/5.1.6\r\n" \
                        "Cache-Control: max-age=0, must-revalidate," \
                                "no-transform,private\r\n" \
                        "Vary: Accept-Encoding,User-Agent\r\n" \
                        "\r\n", False),
                    'GET /no_content HTTP/1.1' : (
                        "HTTP/1.1 204 NO CONTENT\r\n"\
                        "Date: Wed, 27 Aug 2008 08:52:03 GMT\r\n" \
                        "Server: Apache/2.2.3 (CentOS)\r\n" \
                        "X-Powered-By: PHP/5.1.6\r\n" \
                        "Cache-Control: max-age=0, must-revalidate," \
                                "no-transform,private\r\n" \
                        "Vary: Accept-Encoding,User-Agent\r\n" \
                        "\r\n", False),
                    'GET /not_modified HTTP/1.1' : (
                        "HTTP/1.1 304 NOT MODIFIED\r\n"\
                        "Date: Wed, 27 Aug 2008 08:52:03 GMT\r\n" \
                        "Server: Apache/2.2.3 (CentOS)\r\n" \
                        "X-Powered-By: PHP/5.1.6\r\n" \
                        "Cache-Control: max-age=0, must-revalidate," \
                                "no-transform,private\r\n" \
                        "Vary: Accept-Encoding,User-Agent\r\n" \
                        "\r\n", False),
                }
    def lineReceived(self, line):
        if self.first_line is None:
            self.first_line = line
        if line != '':
            return
        try:
            data, close = self.req_to_answer[self.first_line]
        except KeyError:
            # not found. return nothing and close it
            data = 'HTTP/1.1 404 NOT FOUND\r\n\r\n'
            close = True

        self.transport.write(data)
        if close:
            self.transport.loseConnection()

class RawDataFactory(HTTPFactory):
    """Factory for HTTP server."""

    protocol = RawDataProtocol

    protocolArgs = None

    outstandingRequests = 0

    def __init__(self):
        HTTPFactory.__init__(self, DummyHttpServerRequest)

# Actual tests on the HTTP client.

class TestElisaHttpClientRawData(TestCase):
    """
    Tests that need the RawServer as they try to ensure the right behaviour
    according to the message length. See the RFC for more information:
    http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
    """
    ssl_context = None

    def setUp(self):
        # Start listening
        self.http_factory = RawDataFactory()
        if self.ssl_context:
            self.context_factory = SSLContextFactory()
            self._port = reactor.listenSSL(SERVER_PORT, self.http_factory,
                                           self.context_factory)
        else:
            self.context_factory = None
            self._port = reactor.listenTCP(SERVER_PORT, self.http_factory)
        self.client = ElisaHttpClient(SERVER_HOST, SERVER_PORT,
                                      ssl_context=self.ssl_context)

    def tearDown(self):
        if self.context_factory:
            self.context_factory.clean()

        # Stop listening
        dfr = self.client.close()
        dfr.addCallback(lambda x: defer.maybeDeferred(
                self._port.stopListening))
        return dfr

    def test_large_response_contents_without_specified_size(self):
        """
        Test that a 'large' resource is completely read even though the size is
        not specified.
        """
        resource = 'no_size'

        def response_read(response):
            # check if we really got all the data we wanted to have
            end = response[-8:]
            self.assertEqual(end, 'test9999')

        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            # Read the response's contents
            read_dfr = BufferedStream(response.stream).readExactly()
            read_dfr.addCallback(response_read)
            return read_dfr

        request_dfr = self.client.request('/' + resource)
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_head_does_not_wait_for_content(self):
        """
        Sending a head to the server. As soon as the headers (and the empty
        line) are back the deferred should fire.
        """

        def response_read(response):
            # check if we really got all the data we wanted to have
            self.assertEqual(response, '')

        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            # Read the response's contents
            read_dfr = BufferedStream(response.stream).readExactly()
            read_dfr.addCallback(response_read)
            return read_dfr

        request = ClientRequest('HEAD', '/some_thing', None, None)

        request_dfr = self.client.request_full(request)
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_no_content_does_not_wait_for_content(self):
        """
        A request that is answered with "no content". As soon as the headers are
        here the callback should fire and there should not be any content at
        all.
        """
        def response_read(response):
            # check if we really got all the data we wanted to have
            self.assertEqual(response, '')

        def request_done(response):
            self.assertEqual(response.code, 204)
            # Read the response's contents
            read_dfr = BufferedStream(response.stream).readExactly()
            read_dfr.addCallback(response_read)
            return read_dfr

        request_dfr = self.client.request('/no_content')
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_not_modified_does_not_wait_for_content(self):
        """
        A request that is answered with a "not modified". As soon as the headers
        are here the callback should fire and there should be no content at all.
        """
        def response_read(response):
            # check if we really got all the data we wanted to have
            self.assertEqual(response, '')

        def request_done(response):
            self.assertEqual(response.code, 304)
            # Read the response's contents
            read_dfr = BufferedStream(response.stream).readExactly()
            read_dfr.addCallback(response_read)
            return read_dfr

        request_dfr = self.client.request('/not_modified')
        request_dfr.addCallback(request_done)
        return request_dfr

class TestElisaHttpClientRawDataSSL(TestElisaHttpClientRawData):
    ssl_context = client_ssl_context()

class TestElisaHttpClientConnection(ClientServerMixin, TestCase):

    """
    This test case tests basic connection operations on the ElisaHttpClient
    class.
    """


    def test_first_request_opens_connection(self):
        """
        Test that the client will open the connection before attempting to send
        the first request to the server.
        """
        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            return response

        # First check that the connection is not opened yet
        self.failUnless(self.client._closed)

        request_dfr = self.client.request('/foo')
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_reset_retries(self):
        """
        Test that after successfully reconnecting the number of retries is
        resetted.
        """
        uri = '/foo_and_close'

        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            self.assertEqual(self.client._connector.factory.retries, 0)
            return response

        request_dfrs = []
        for i in xrange(ElisaHttpClientFactory.maxRetries + 1):
            request_dfr = self.client.request(uri)
            request_dfr.addCallback(request_done)
            request_dfrs.append(request_dfr)

        return defer.DeferredList(request_dfrs)

class TestElisaHttpClientConnectionSSL(TestElisaHttpClientConnection):
    client_constructor_kwargs=dict(ssl_context=client_ssl_context())

class TestElisaHttpClientConnectionTerminate(ClientServerMixin, TestCase):

    """
    This test case tests basic connection operations on the ElisaHttpClient
    class.
    """

    def test_close_before_accepted(self):
        """
        Open a connection but terminate before it is accepted.
        Test that cleanup is correctly performed.
        """
        # FIXME: cleanup is not correctly performed...
        def connection_aborted(failure):
            # UserError: User aborted connection.
            failure.trap(error.UserError)

        request_defer = self.client.request('/foo')
        request_defer.addErrback(connection_aborted)
        return defer.succeed(self)

class TestElisaHttpClientConnectionTerminateSSL(TestElisaHttpClientConnectionTerminate):
    client_constructor_kwargs=dict(ssl_context=client_ssl_context())

class TestElisaHttpClientConnectionClosed(ClientServerMixin, TestCase):

    """
    This test case tests basic connection operations on the ElisaHttpClient
    class.
    """

    def setUp(self):
        self.client = ElisaHttpClient(SERVER_HOST, SERVER_PORT)

    def tearDown(self):
        pass

    def test_close_not_opened(self):
        """
        Test that trying to close a connection that has not been previously
        opened fails.
        """
        close_dfr = self.client.close()
        self.failUnlessFailure(close_dfr, ElisaHttpClientNotOpenedError)
        return close_dfr

class TestElisaHttpClientConnectionClosedSSL(TestElisaHttpClientConnectionClosed):
    client_constructor_kwargs=dict(ssl_context=client_ssl_context())

class TestElisaHttpClientConnectionError(ClientServerMixin, TestCase):

    """
    This test case tests connection errors on the ElisaHttpClient class.
    """
    # Connecting on a port greater than 2^16-1 is not allowed
    client_constructor_args = (SERVER_HOST, 2**16)

    def test_connection_error(self):
        """
        Test that trying to connect on an invalid port raises the expected
        exception.
        """
        request_dfr = self.client.request('/foo')
        self.assertFailure(request_dfr, error.ConnectError)
        return request_dfr


class TestElisaHttpClientConnectionNoServer(TestCase):

    """
    This test case tests the behaviour of the ElisaHttpClient when there is no
    server to respond.
    """

    def setUp(self):
        self.client = ElisaHttpClient(SERVER_HOST, SERVER_PORT)

    def test_connection_fails(self):
        """
        Test that trying to connect to an inexistent server raises the expected
        exception.
        """
        request_dfr = self.client.request('/foo')
        self.assertFailure(request_dfr, error.ConnectionRefusedError)
        return request_dfr


class TestElisaHttpClientNoPipelining(ClientServerMixin, TestCase):

    """
    This test case tests the ElisaHttpClient class with no pipelining.
    """
    client_constructor_kwargs = dict(pipeline=False)

    def test_inexistent_resource(self):
        """
        Test that requesting a resource that does not exists returns a 404
        error code.
        """
        def request_done(response):
            self.assertEqual(response.code, responsecode.NOT_FOUND)
            return response

        request_dfr = self.client.request('/crap')
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_single_request(self):
        """
        Test a single request and validate the response code.
        """
        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            return response

        request_dfr = self.client.request('/foo')
        request_dfr.addCallback(request_done)
        return request_dfr

# FIXME: understand why this test hangs instead of failing with a TypeError as
#        expected.
#    def test_unicode_fails_for_request(self):
#        """
#        Test that passing a unicode string to the request fails
#        (this is a common mistake in client code)
#        """
#        request_dfr = self.client.request(u'/foo')
#        self.assertFailure(request_dfr, TypeError)
#        return request_dfr

    def test_absolute_uri(self):
        """
        Test a request with an absolute URI (not just a relative path to the
        resource on the server).
        """
        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            return response

        uri = 'http://%s:%s/foo' % (SERVER_HOST, SERVER_PORT)
        request_dfr = self.client.request(uri)
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_uri_with_parameters(self):
        """
        Test a request with a URI that contains parameters.
        """
        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            return response

        uri = '/foo?crap=lots&stuff=none'
        request_dfr = self.client.request(uri)
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_consecutive_same_uri(self):
        """
        Test two consecutive requests on the same URI. This tests that our
        client reconnects correctly.
        """
        uri = '/foo'
        result_dfr = defer.Deferred()

        def second_request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            return response

        def first_request_done(response):
            self.assertEqual(response.code, responsecode.OK)

            # Send the second request
            second_request_dfr = self.client.request(uri)
            second_request_dfr.addCallback(second_request_done)
            second_request_dfr.chainDeferred(result_dfr)

            return response

        # Send the first request
        first_request_dfr = self.client.request(uri)
        first_request_dfr.addCallback(first_request_done)
        first_request_dfr.addErrback(result_dfr.errback)

        return result_dfr

    def test_consecutive_different_uris(self):
        """
        Test two consecutive requests on different URIs. This tests that our
        client reconnects correctly.
        """
        first_uri = '/foo'
        second_uri = '/bar'
        result_dfr = defer.Deferred()

        def second_request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            return response

        def first_request_done(response):
            self.assertEqual(response.code, responsecode.OK)

            # Send the second request
            second_request_dfr = self.client.request(second_uri)
            second_request_dfr.addCallback(second_request_done)
            second_request_dfr.chainDeferred(result_dfr)

            return response

        # Send the first request
        first_request_dfr = self.client.request(first_uri)
        first_request_dfr.addCallback(first_request_done)
        first_request_dfr.addErrback(result_dfr.errback)

        return result_dfr


class TestElisaHttpClientNoPipeliningSSL(TestElisaHttpClientNoPipelining):
    client_constructor_kwargs = dict(pipeline=False,
                                     ssl_context=client_ssl_context())


class TestElisaHttpClientPipelining(ClientServerMixin, TestCase):

    """
    This test case tests the ElisaHttpClient class with pipelining.
    """
    client_constructor_kwargs = dict(pipeline=True)

    def test_single_request(self):
        """
        Test a single request and validate the response code.
        """
        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            return response

        request_dfr = self.client.request('/foo')
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_response_contents(self):
        """
        Test the actual contents of the response returned by the server.
        """
        resource = 'foo'

        def build_response_string(resource):
            content = resource * 10
            response = '<html><head><title>%s</title></head>' % SERVER_TITLE
            response += '<body><h1>%s</h1><p>%s</p></body></html>' % \
                (SERVER_TITLE, content)
            return response

        def response_read(response):
            self.assertEqual(response, build_response_string(resource))
            return response

        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            read_dfr = response.stream.read()
            read_dfr.addCallback(response_read)
            return read_dfr

        request_dfr = self.client.request('/' + resource)
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_large_response_contents(self):
        """
        Test that a 'large' resource is completely read.
        """
        resource = 'fatty'

        def response_read(response, response_size):
            self.assertEqual(len(response), response_size)
            return response

        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            # Read the response's contents
            read_dfr = BufferedStream(response.stream).readExactly()
            read_dfr.addCallback(response_read, response.stream.length)
            return read_dfr

        request_dfr = self.client.request('/' + resource)
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_consecutive_same_uri(self):
        """
        Test two consecutive requests on the same URI.
        """
        uri = '/foo'
        result_dfr = defer.Deferred()

        def second_request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            return response

        def first_request_done(response):
            self.assertEqual(response.code, responsecode.OK)

            # Send the second request
            second_request_dfr = self.client.request(uri)
            second_request_dfr.addCallback(second_request_done)
            second_request_dfr.chainDeferred(result_dfr)

            return response

        # Send the first request
        first_request_dfr = self.client.request(uri)
        first_request_dfr.addCallback(first_request_done)
        first_request_dfr.addErrback(result_dfr.errback)

        return result_dfr

    def test_consecutive_different_uris(self):
        """
        Test two consecutive requests on different URIs.
        """
        first_uri = '/foo'
        second_uri = '/bar'
        result_dfr = defer.Deferred()

        def second_request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            return response

        def first_request_done(response):
            self.assertEqual(response.code, responsecode.OK)

            # Send the second request
            second_request_dfr = self.client.request(second_uri)
            second_request_dfr.addCallback(second_request_done)
            second_request_dfr.chainDeferred(result_dfr)

            return response

        # Send the first request
        first_request_dfr = self.client.request(first_uri)
        first_request_dfr.addCallback(first_request_done)
        first_request_dfr.addErrback(result_dfr.errback)

        return result_dfr

    def test_consecutive_with_close(self):
        """
        Test two consecutive requests on different URIs with the server closing
        the connection after replying to the first request. This tests that
        the client correctly restores the connection if needed.
        """
        first_uri = '/foo_and_close'
        second_uri = '/bar'
        data = "some data"
        result_dfr = defer.Deferred()

        def response_read(response_data):
            self.assertEqual(response_data, data)

        def second_request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            dfr = BufferedStream(response.stream).readExactly()
            dfr.addCallback(response_read)
            return dfr

        def first_request_done(response):
            self.assertEqual(response.code, responsecode.OK)

            # Send the second request
            second_request_dfr = self.client.request(second_uri,
                                                     method='POST',
                                                     stream=MemoryStream(data))
            second_request_dfr.addCallback(second_request_done)
            second_request_dfr.chainDeferred(result_dfr)

            return response

        # Send the first request
        first_request_dfr = self.client.request(first_uri)
        first_request_dfr.addCallback(first_request_done)
        first_request_dfr.addErrback(result_dfr.errback)

        return result_dfr

    def test_pipeline_with_close(self):
        """
        Test the pipelining with three requests. The first request returns OK,
        the second request returns OBJECT MOVED and closes the connection, and
        the third request returns OK.
        This checks that our client correctly reconnects if for some reason the
        server closes the connection in the middle of a pipeline
        """
        result_dfr = defer.Deferred()
        got_first_response = False
        got_second_response = False

        def first_request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            global got_first_response
            got_first_response = True
            return response

        def second_request_done(response):
            global got_second_response
            got_second_response = True
            global got_first_response
            self.failUnless(got_first_response)
            self.assertEqual(response.code, responsecode.MOVED_PERMANENTLY)
            return response

        def third_request_done(response):
            global got_second_response
            self.failUnless(got_second_response)
            self.assertEqual(response.code, responsecode.OK)
            return response

        first_request_dfr = self.client.request('/foo')
        second_request_dfr = self.client.request('/redirect_and_close')
        third_request_dfr = self.client.request('/bar')

        first_request_dfr.addCallback(first_request_done)
        first_request_dfr.addErrback(result_dfr.errback)
        second_request_dfr.addCallback(second_request_done)
        second_request_dfr.addErrback(result_dfr.errback)
        third_request_dfr.addCallback(third_request_done)
        third_request_dfr.chainDeferred(result_dfr)

        return result_dfr

    def test_cancel_request(self):
        """
        Test that cancelling a request works.
        """
        uri = '/foo'
        request_deferreds = []

        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            return response

        def first_request_done(response):
            # Let's cancel the third request after the first one is done
            request_deferreds[2].cancel()
            return request_done(response)

        def second_request_done(response):
            return request_done(response)

        def third_request_done(response):
            self.fail('This should never happen')

        def third_request_cancelled(failure):
            failure.trap(defer.CancelledError)

        def fourth_request_done(response):
            return request_done(response)

        for i in xrange(4):
            request_deferred = self.client.request(uri)
            request_deferreds.append(request_deferred)

        request_deferreds[0].addCallback(first_request_done)
        request_deferreds[1].addCallback(second_request_done)
        request_deferreds[2].addCallbacks(third_request_done,
                third_request_cancelled)
        request_deferreds[3].addCallback(fourth_request_done)

        return defer.DeferredList(request_deferreds, consumeErrors=True)

    def test_cancel_already_cancelled_request(self):
        """
        Test cancelling an already cancelled request.
        """
        uri = '/foo'
        request_deferreds = []

        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            return response

        def first_request_done(response):
            # Let's cancel the fourth request after the first one is done
            request_deferreds[3].cancel()
            return request_done(response)

        def second_request_done(response):
            # Let's try to cancel again the fourth request after the second one
            # is done
            request_deferreds[3].cancel()
            return request_done(response)

        def third_request_done(response):
            return request_done(response)

        def fourth_request_done(response):
            self.fail('This should never happen')

        def fourth_request_cancelled(failure):
            failure.trap(defer.CancelledError)

        for i in xrange(4):
            request_deferred = self.client.request(uri)
            request_deferreds.append(request_deferred)

        request_deferreds[0].addCallback(first_request_done)
        request_deferreds[1].addCallback(second_request_done)
        request_deferreds[2].addCallback(third_request_done)
        request_deferreds[3].addCallback(fourth_request_done)
        request_deferreds[3].addErrback(fourth_request_cancelled)

        return defer.DeferredList(request_deferreds, consumeErrors=True)

class TestElisaHttpClientPipeliningSSL(TestElisaHttpClientPipelining):
    client_constructor_kwargs = dict(pipeline=True,
                                     ssl_context=client_ssl_context())

class TestElisaHttpClientPipeliningErrors(ClientServerMixin, TestCase):

    """
    This test case tests the ElisaHttpClient class with pipelining errors.
    """
    client_constructor_kwargs = dict(pipeline=True)

    def tearDown(self):
        global crashed_server
        crashed_server = None
        return ClientServerMixin.tearDown(self)

    def test_pipeline_with_errors(self):
        """
        Test the pipelining with three requests. For the first request we get a
        response, for the last two requests the server closes the connection
        without writing a response. This checks that the client doesn't get
        stuck retrying requests if the server is down.
        """
        result_dfr = defer.Deferred()
        global got_first_response
        global got_second_failure
        got_first_response = False
        got_second_failure = False

        def first_request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            global got_first_response
            got_first_response = True

        def first_request_failure(failure):
            result_dfr.errback(failure)

        def second_request_done(response):
            raise Exception('this should not be reached')

        def second_request_failure(failure):
            global got_second_failure
            got_second_failure = True
            global got_first_response
            self.failUnless(got_first_response)
            self.client._trap_failure(failure)

        def third_request_done(response):
            raise Exception('this should not be reached')

        def third_request_failure(failure):
            global got_second_failure
            self.failUnless(got_second_failure)
            self.client._trap_failure(failure)

        # The server is going to be voluntarily crashed after the first request
        global crashed_server
        crashed_server = self

        first_request_dfr = self.client.request('/foo_and_crash')
        second_request_dfr = self.client.request('/foo')
        third_request_dfr = self.client.request('/bar')

        first_request_dfr.addCallbacks(first_request_done,
                                       first_request_failure)
        second_request_dfr.addCallbacks(second_request_done,
                                        second_request_failure)
        third_request_dfr.addCallbacks(third_request_done,
                                       third_request_failure)
        third_request_dfr.chainDeferred(result_dfr)
        return result_dfr


class TestElisaHttpClientPipeliningErrorsSSL(TestElisaHttpClientPipeliningErrors):
    client_constructor_kwargs=dict(pipeline=True,
                                   ssl_context=client_ssl_context())

class TestElisaHttpClientMethods(ClientServerMixin, TestCase):

    """
    This test case tests the ElisaHttpClient class on other methods than GET
    (POST, PUT, DELETE).

    What really happens on the server side is not tested here, this test case
    only checks that the server receives the request and answers correctly, and
    it is intended as an example piece of code for how to use methods other
    than GET.
    """

    def test_post(self):
        """
        Test a simple POST request.
        """

        def response_read(data):
            self.assertEqual(data, 'a' * 100)

        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            dfr = BufferedStream(response.stream).readExactly()
            dfr.addCallback(response_read)
            return dfr

        stream = MemoryStream('a' * 100)
        request = ClientRequest('POST', '/bar', {}, stream)
        request_dfr = self.client.request_full(request)
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_put(self):
        """
        Test a simple PUT request.
        """
        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            return response

        stream = MemoryStream('a' * 100)
        request = ClientRequest('PUT', '/foo', {}, stream)
        request_dfr = self.client.request_full(request)
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_delete(self):
        """
        Test a simple DELETE request.
        """
        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            return response

        request = ClientRequest('DELETE', '/foo', {}, None)
        request_dfr = self.client.request_full(request)
        request_dfr.addCallback(request_done)
        return request_dfr

class TestElisaHttpClientMethodsSSL(TestElisaHttpClientMethods):
    client_constructor_kwargs=dict(ssl_context=client_ssl_context())

class TestMultipleElisaHttpClients(TestCase):

    """
    This test case tests the interaction of two instances of ElisaHttpClient.
    """

    def setUpClass(self):
        # Set up two servers that listen on two different ports
        self.ports = []
        self.ports.append(reactor.listenTCP(SERVER_PORT, HTTPFactory(DummyHttpServerRequest)))
        self.ports.append(reactor.listenTCP(SERVER_PORT_2, HTTPFactory(DummyHttpServerRequest)))

    def tearDownClass(self):
        # Stop listening
        stop_defers = [defer.maybeDeferred(port.stopListening) for port in self.ports]
        return defer.DeferredList(stop_defers)

    def setUp(self):
        # Set up two clients
        self.clients = []
        self.clients.append(ElisaHttpClient(SERVER_HOST, SERVER_PORT, pipeline=True))
        self.clients.append(ElisaHttpClient(SERVER_HOST, SERVER_PORT_2, pipeline=True))

    def tearDown(self):
        close_defers = [client.close() for client in self.clients]
        return defer.DeferredList(close_defers)

    def test_two_clients_concurrent_requests(self):
        """
        Test two concurrent requests on two clients, and that closing the
        clients at tearDown time succeeds.
        The server will close the connection after responding, and this should
        not prevent the cleanup to be performed correctly.
        """
        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            return response

        first_request_defer = self.clients[0].request('/foo_and_close')
        second_request_defer = self.clients[1].request('/foo_and_close')

        first_request_defer.addCallback(request_done)
        second_request_defer.addCallback(request_done)

        return defer.DeferredList([first_request_defer, second_request_defer])

class TestElisaAdvancedHttpClient(ClientServerMixin, TestCase):

    """
    This test case tests the ElisaAdvancedHttpClient class on HTTP
    redirections and on reading large responses.
    """
    client_class = ElisaAdvancedHttpClient

    def request_done(self, response):
        self.assertEqual(response.code, responsecode.OK)
        return response

    def test_no_redirection(self):
        """
        Test a simple HTTP request without redirection.
        """
        result_dfr = defer.Deferred()
        request_dfr = self.client.request('/foo_and_close')
        request_dfr.addCallback(self.request_done)
        request_dfr.addErrback(result_dfr.errback)
        request_dfr.chainDeferred(result_dfr)
        return result_dfr

    def test_simple_redirection(self):
        """
        Test a single HTTP redirection.
        """
        result_dfr = defer.Deferred()
        request_dfr = self.client.request('/simple_redirect')
        request_dfr.addCallback(self.request_done)
        request_dfr.addErrback(result_dfr.errback)
        request_dfr.chainDeferred(result_dfr)
        return result_dfr

    def test_copy_headers_on_redirection(self):
        """
        test that the headers are copied for the redirection
        """
        request = ClientRequest('GET', '/simple_redirect', None, None)

        request.headers.setRawHeaders('DummyTestHeader', ['yehaaa'])

        def check_data(result):
            self.assertEquals(result.headers.getRawHeaders('DummyTestHeader'),
                                ['yehaaa'])
            return result
        request_dfr = self.client.request_full(request)
        request_dfr.addCallback(check_data)
        request_dfr.addCallback(self.request_done)
        return request_dfr

    def test_double_redirection(self):
        """
        Test a double HTTP redirection. The first request redirects to a
        resource which in turn redirects to another one.
        """
        result_dfr = defer.Deferred()
        request_dfr = self.client.request('/double_redirect')
        request_dfr.addCallback(self.request_done)
        request_dfr.chainDeferred(result_dfr)
        return result_dfr

    def test_redirection_and_close(self):
        """
        Test a single HTTP redirection when the server closes the connection
        after replying to the first request.
        """
        result_dfr = defer.Deferred()
        request_dfr = self.client.request('/redirect_and_close')
        request_dfr.addCallback(self.request_done)
        request_dfr.chainDeferred(result_dfr)
        return result_dfr

    def test_response_contents(self):
        """
        Test the actual contents of the response returned by the server.
        """
        resource = 'foo'

        def build_response_string(resource):
            content = resource * 10
            response = '<html><head><title>%s</title></head>' % SERVER_TITLE
            response += '<body><h1>%s</h1><p>%s</p></body></html>' % \
                (SERVER_TITLE, content)
            return response

        def response_read(response):
            self.assertEqual(response, build_response_string(resource))
            return response

        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            # Read the contents of the response
            read_dfr = response.stream.read()
            read_dfr.addCallback(response_read)
            return read_dfr

        request_dfr = self.client.request('/' + resource)
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_large_response_contents(self):
        """
        Test that a 'large' resource is completely read.
        """
        def response_read(response, response_size):
            self.assertEqual(response_size, len(response))
            return response

        def request_done(response):
            self.assertEqual(response.code, responsecode.OK)
            # Read the contents of the response
            read_dfr = BufferedStream(response.stream).readExactly()
            read_dfr.addCallback(response_read, response.stream.length)
            return read_dfr

        request_dfr = self.client.request('/fatty')
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_protocol_error(self):
        """ Test a request against a server that returns broken responses.
        A good http response starts with a line like this::

          HTTP/1.X 200 OK

        A broken response is anything not matching that pattern, eg::

          foo bar and something else
        """

        def request_done(response):
            self.fail(Exception("This request should not succeed"))

        def request_failed(failure):
            is_protocol_error = failure.trap(ProtocolError)
            self.failUnless(is_protocol_error)
            self.flushLoggedErrors()

        request_dfr = self.client.request('/broken_server')
        request_dfr.addCallback(request_done)
        request_dfr.addErrback(request_failed)
        return request_dfr

class TestElisaAdvancedHttpClientSSL(TestElisaAdvancedHttpClient):
    client_constructor_kwargs=dict(ssl_context=client_ssl_context())

class TestElisaHttpClientAuthentication(ClientServerMixin, TestCase):

    def test_successful_auth(self):

        def request_done(response):
            self.assertEquals(response.code, responsecode.OK)

        headers = utils.generate_basic_auth_headers('guest', 'guest123')
        request_dfr = self.client.request('/auth', headers=headers)
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_no_auth(self):

        def request_done(response):
            self.assertEquals(response.code, responsecode.UNAUTHORIZED)

        request_dfr = self.client.request('/auth')
        request_dfr.addCallback(request_done)
        return request_dfr

    def test_wrong_credentials(self):

        def request_done(response):
            self.assertEquals(response.code, responsecode.UNAUTHORIZED)

        headers = utils.generate_basic_auth_headers('guest', 'guest321')
        request_dfr = self.client.request('/auth', headers=headers)
        request_dfr.addCallback(request_done)
        return request_dfr

class TestElisaHttpClientAuthenticationSSL(TestElisaHttpClientAuthentication):
    client_constructor_kwargs=dict(ssl_context=client_ssl_context())
