# Copyright (C) 2006-2009, 2011 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

"""Tests for WSGI application"""

from io import BytesIO

from .. import tests
from ..bzr.smart import medium, protocol
from ..transport import chroot, memory
from ..transport.http import wsgi


class WSGITestMixin:

    def build_environ(self, updates=None):
        """Builds an environ dict with all fields required by PEP 333.

        :param updates: a dict to that will be incorporated into the returned
            dict using dict.update(updates).
        """
        environ = {
            # Required CGI variables
            'REQUEST_METHOD': 'GET',
            'SCRIPT_NAME': '/script/name/',
            'PATH_INFO': 'path/info',
            'SERVER_NAME': 'test',
            'SERVER_PORT': '9999',
            'SERVER_PROTOCOL': 'HTTP/1.0',

            # Required WSGI variables
            'wsgi.version': (1, 0),
            'wsgi.url_scheme': 'http',
            'wsgi.input': BytesIO(b''),
            'wsgi.errors': BytesIO(),
            'wsgi.multithread': False,
            'wsgi.multiprocess': False,
            'wsgi.run_once': True,
        }
        if updates is not None:
            environ.update(updates)
        return environ

    def read_response(self, iterable):
        response = b''
        for string in iterable:
            response += string
        return response

    def start_response(self, status, headers):
        self.status = status
        self.headers = headers


class TestWSGI(tests.TestCaseInTempDir, WSGITestMixin):

    def setUp(self):
        super().setUp()
        self.status = None
        self.headers = None

    def test_construct(self):
        app = wsgi.SmartWSGIApp(FakeTransport())
        self.assertIsInstance(
            app.backing_transport, chroot.ChrootTransport)

    def test_http_get_rejected(self):
        # GET requests are rejected.
        app = wsgi.SmartWSGIApp(FakeTransport())
        environ = self.build_environ({'REQUEST_METHOD': 'GET'})
        iterable = app(environ, self.start_response)
        self.read_response(iterable)
        self.assertEqual('405 Method not allowed', self.status)
        self.assertTrue(('Allow', 'POST') in self.headers)

    def _fake_make_request(self, transport, write_func, bytes, rcp):
        request = FakeRequest(transport, write_func)
        request.accept_bytes(bytes)
        self.request = request
        return request

    def test_smart_wsgi_app_uses_given_relpath(self):
        # The SmartWSGIApp should use the "breezy.relpath" field from the
        # WSGI environ to clone from its backing transport to get a specific
        # transport for this request.
        transport = FakeTransport()
        wsgi_app = wsgi.SmartWSGIApp(transport)
        wsgi_app.backing_transport = transport
        wsgi_app.make_request = self._fake_make_request
        fake_input = BytesIO(b'fake request')
        environ = self.build_environ({
            'REQUEST_METHOD': 'POST',
            'CONTENT_LENGTH': len(fake_input.getvalue()),
            'wsgi.input': fake_input,
            'breezy.relpath': 'foo/bar',
        })
        iterable = wsgi_app(environ, self.start_response)
        response = self.read_response(iterable)
        self.assertEqual([('clone', 'foo/bar/')], transport.calls)

    def test_smart_wsgi_app_request_and_response(self):
        # SmartWSGIApp reads the smart request from the 'wsgi.input' file-like
        # object in the environ dict, and returns the response via the iterable
        # returned to the WSGI handler.
        transport = memory.MemoryTransport()
        transport.put_bytes('foo', b'some bytes')
        wsgi_app = wsgi.SmartWSGIApp(transport)
        wsgi_app.make_request = self._fake_make_request
        fake_input = BytesIO(b'fake request')
        environ = self.build_environ({
            'REQUEST_METHOD': 'POST',
            'CONTENT_LENGTH': len(fake_input.getvalue()),
            'wsgi.input': fake_input,
            'breezy.relpath': 'foo',
        })
        iterable = wsgi_app(environ, self.start_response)
        response = self.read_response(iterable)
        self.assertEqual('200 OK', self.status)
        self.assertEqual(b'got bytes: fake request', response)

    def test_relpath_setter(self):
        # wsgi.RelpathSetter is WSGI "middleware" to set the 'breezy.relpath'
        # variable.
        calls = []

        def fake_app(environ, start_response):
            calls.append(environ['breezy.relpath'])
        wrapped_app = wsgi.RelpathSetter(
            fake_app, prefix='/abc/', path_var='FOO')
        wrapped_app({'FOO': '/abc/xyz/.bzr/smart'}, None)
        self.assertEqual(['xyz'], calls)

    def test_relpath_setter_bad_path_prefix(self):
        # wsgi.RelpathSetter will reject paths with that don't match the prefix
        # with a 404.  This is probably a sign of misconfiguration; a server
        # shouldn't ever be invoking our WSGI application with bad paths.
        def fake_app(environ, start_response):
            self.fail('The app should never be called when the path is wrong')
        wrapped_app = wsgi.RelpathSetter(
            fake_app, prefix='/abc/', path_var='FOO')
        iterable = wrapped_app(
            {'FOO': 'AAA/abc/xyz/.bzr/smart'}, self.start_response)
        self.read_response(iterable)
        self.assertTrue(self.status.startswith('404'))

    def test_relpath_setter_bad_path_suffix(self):
        # Similar to test_relpath_setter_bad_path_prefix: wsgi.RelpathSetter
        # will reject paths with that don't match the suffix '.bzr/smart' with a
        # 404 as well.  Again, this shouldn't be seen by our WSGI application if
        # the server is configured correctly.
        def fake_app(environ, start_response):
            self.fail('The app should never be called when the path is wrong')
        wrapped_app = wsgi.RelpathSetter(
            fake_app, prefix='/abc/', path_var='FOO')
        iterable = wrapped_app(
            {'FOO': '/abc/xyz/.bzr/AAA'}, self.start_response)
        self.read_response(iterable)
        self.assertTrue(self.status.startswith('404'))

    def test_make_app(self):
        # The make_app helper constructs a SmartWSGIApp wrapped in a
        # RelpathSetter.
        app = wsgi.make_app(
            root='a root',
            prefix='a prefix',
            path_var='a path_var')
        self.assertIsInstance(app, wsgi.RelpathSetter)
        self.assertIsInstance(app.app, wsgi.SmartWSGIApp)
        self.assertStartsWith(app.app.backing_transport.base, 'chroot-')
        backing_transport = app.app.backing_transport
        chroot_backing_transport = backing_transport.server.backing_transport
        self.assertEndsWith(chroot_backing_transport.base, 'a%20root/')
        self.assertEqual(app.app.root_client_path, 'a prefix')
        self.assertEqual(app.path_var, 'a path_var')

    def test_incomplete_request(self):
        transport = FakeTransport()
        wsgi_app = wsgi.SmartWSGIApp(transport)

        def make_request(transport, write_func, bytes, root_client_path):
            request = IncompleteRequest(transport, write_func)
            request.accept_bytes(bytes)
            self.request = request
            return request
        wsgi_app.make_request = make_request

        fake_input = BytesIO(b'incomplete request')
        environ = self.build_environ({
            'REQUEST_METHOD': 'POST',
            'CONTENT_LENGTH': len(fake_input.getvalue()),
            'wsgi.input': fake_input,
            'breezy.relpath': 'foo/bar',
        })
        iterable = wsgi_app(environ, self.start_response)
        response = self.read_response(iterable)
        self.assertEqual('200 OK', self.status)
        self.assertEqual(b'error\x01incomplete request\n', response)

    def test_protocol_version_detection_one(self):
        # SmartWSGIApp detects requests that don't start with
        # REQUEST_VERSION_TWO as version one.
        transport = memory.MemoryTransport()
        wsgi_app = wsgi.SmartWSGIApp(transport)
        fake_input = BytesIO(b'hello\n')
        environ = self.build_environ({
            'REQUEST_METHOD': 'POST',
            'CONTENT_LENGTH': len(fake_input.getvalue()),
            'wsgi.input': fake_input,
            'breezy.relpath': 'foo',
        })
        iterable = wsgi_app(environ, self.start_response)
        response = self.read_response(iterable)
        self.assertEqual('200 OK', self.status)
        # Expect a version 1-encoded response.
        self.assertEqual(b'ok\x012\n', response)

    def test_protocol_version_detection_two(self):
        # SmartWSGIApp detects requests that start with REQUEST_VERSION_TWO
        # as version two.
        transport = memory.MemoryTransport()
        wsgi_app = wsgi.SmartWSGIApp(transport)
        fake_input = BytesIO(protocol.REQUEST_VERSION_TWO + b'hello\n')
        environ = self.build_environ({
            'REQUEST_METHOD': 'POST',
            'CONTENT_LENGTH': len(fake_input.getvalue()),
            'wsgi.input': fake_input,
            'breezy.relpath': 'foo',
        })
        iterable = wsgi_app(environ, self.start_response)
        response = self.read_response(iterable)
        self.assertEqual('200 OK', self.status)
        # Expect a version 2-encoded response.
        self.assertEqual(
            protocol.RESPONSE_VERSION_TWO + b'success\nok\x012\n', response)


class TestWSGIJail(tests.TestCaseWithMemoryTransport, WSGITestMixin):

    def make_hpss_wsgi_request(self, wsgi_relpath, *args):
        write_buf = BytesIO()
        request_medium = medium.SmartSimplePipesClientMedium(
            None, write_buf, 'fake:' + wsgi_relpath)
        request_encoder = protocol.ProtocolThreeRequester(
            request_medium.get_request())
        request_encoder.call(*args)
        write_buf.seek(0)
        environ = self.build_environ({
            'REQUEST_METHOD': 'POST',
            'CONTENT_LENGTH': len(write_buf.getvalue()),
            'wsgi.input': write_buf,
            'breezy.relpath': wsgi_relpath,
        })
        return environ

    def test_jail_root(self):
        """The WSGI HPSS glue allows access to the whole WSGI backing
        transport, regardless of which HTTP path the request was delivered
        to.
        """
        # make a branch in a shared repo
        self.make_repository('repo', shared=True)
        branch = self.make_controldir('repo/branch').create_branch()
        # serve the repo via bzr+http WSGI
        wsgi_app = wsgi.SmartWSGIApp(self.get_transport())
        # send a request to /repo/branch that will have to access /repo.
        environ = self.make_hpss_wsgi_request(
            '/repo/branch', b'BzrDir.open_branchV2', b'.')
        iterable = wsgi_app(environ, self.start_response)
        response_bytes = self.read_response(iterable)
        self.assertEqual('200 OK', self.status)
        # expect a successful response, rather than a jail break error
        from breezy.bzr.tests.test_smart_transport import LoggingMessageHandler
        message_handler = LoggingMessageHandler()
        decoder = protocol.ProtocolThreeDecoder(
            message_handler, expect_version_marker=True)
        decoder.accept_bytes(response_bytes)
        self.assertTrue(
            ('structure', (b'branch', branch._format.network_name()))
            in message_handler.event_log)


class FakeRequest:

    def __init__(self, transport, write_func):
        self.transport = transport
        self.write_func = write_func
        self.accepted_bytes = b''

    def accept_bytes(self, bytes):
        self.accepted_bytes = bytes
        self.write_func(b'got bytes: ' + bytes)

    def next_read_size(self):
        return 0


class FakeTransport:

    def __init__(self):
        self.calls = []
        self.base = 'fake:///'

    def abspath(self, relpath):
        return 'fake:///' + relpath

    def clone(self, relpath):
        self.calls.append(('clone', relpath))
        return self


class IncompleteRequest(FakeRequest):
    """A request-like object that always expects to read more bytes."""

    def next_read_size(self):
        # this request always asks for more
        return 1
