##// END OF EJS Templates
vcs: Do not pass the backend key into the scm app instances....
Martin Bornhold -
r951:8ff080b9 default
parent child Browse files
Show More
@@ -1,85 +1,85 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleGit middleware for handling git protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25 import re
26 26 import logging
27 27 import urlparse
28 28
29 29 import rhodecode
30 30 from rhodecode.lib import utils2
31 31 from rhodecode.lib.middleware import simplevcs
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 GIT_PROTO_PAT = re.compile(
37 37 r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
38 38
39 39
40 40 class SimpleGit(simplevcs.SimpleVCS):
41 41
42 42 SCM = 'git'
43 43
44 44 def _get_repository_name(self, environ):
45 45 """
46 46 Gets repository name out of PATH_INFO header
47 47
48 48 :param environ: environ where PATH_INFO is stored
49 49 """
50 50 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
51 51 return repo_name
52 52
53 53 _ACTION_MAPPING = {
54 54 'git-receive-pack': 'push',
55 55 'git-upload-pack': 'pull',
56 56 }
57 57
58 58 def _get_action(self, environ):
59 59 """
60 60 Maps git request commands into a pull or push command.
61 61 In case of unknown/unexpected data, it returns 'pull' to be safe.
62 62
63 63 :param environ:
64 64 """
65 65 path = environ['PATH_INFO']
66 66
67 67 if path.endswith('/info/refs'):
68 68 query = urlparse.parse_qs(environ['QUERY_STRING'])
69 69 service_cmd = query.get('service', [''])[0]
70 70 return self._ACTION_MAPPING.get(service_cmd, 'pull')
71 71 elif path.endswith('/git-receive-pack'):
72 72 return 'push'
73 73 elif path.endswith('/git-upload-pack'):
74 74 return 'pull'
75 75
76 76 return 'pull'
77 77
78 78 def _create_wsgi_app(self, repo_path, repo_name, config):
79 79 return self.scm_app.create_git_wsgi_app(
80 repo_path, repo_name, config, self.SCM)
80 repo_path, repo_name, config)
81 81
82 82 def _create_config(self, extras, repo_name):
83 83 extras['git_update_server_info'] = utils2.str2bool(
84 84 rhodecode.CONFIG.get('git_update_server_info'))
85 85 return extras
@@ -1,81 +1,81 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleHG middleware for handling mercurial protocol request
23 23 (push/clone etc.). It's implemented with basic auth function
24 24 """
25 25
26 26 import logging
27 27 import urlparse
28 28
29 29 from rhodecode.lib import utils
30 30 from rhodecode.lib.ext_json import json
31 31 from rhodecode.lib.middleware import simplevcs
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class SimpleHg(simplevcs.SimpleVCS):
37 37
38 38 SCM = 'hg'
39 39
40 40 def _get_repository_name(self, environ):
41 41 """
42 42 Gets repository name out of PATH_INFO header
43 43
44 44 :param environ: environ where PATH_INFO is stored
45 45 """
46 46 return environ['PATH_INFO'].strip('/')
47 47
48 48 _ACTION_MAPPING = {
49 49 'changegroup': 'pull',
50 50 'changegroupsubset': 'pull',
51 51 'getbundle': 'pull',
52 52 'stream_out': 'pull',
53 53 'listkeys': 'pull',
54 54 'unbundle': 'push',
55 55 'pushkey': 'push',
56 56 }
57 57
58 58 def _get_action(self, environ):
59 59 """
60 60 Maps mercurial request commands into a pull or push command.
61 61 In case of unknown/unexpected data, it returns 'pull' to be safe.
62 62
63 63 :param environ:
64 64 """
65 65 query = urlparse.parse_qs(environ['QUERY_STRING'],
66 66 keep_blank_values=True)
67 67 if 'cmd' in query:
68 68 cmd = query['cmd'][0]
69 69 return self._ACTION_MAPPING.get(cmd, 'pull')
70 70
71 71 return 'pull'
72 72
73 73 def _create_wsgi_app(self, repo_path, repo_name, config):
74 74 return self.scm_app.create_hg_wsgi_app(
75 repo_path, repo_name, config, self.SCM)
75 repo_path, repo_name, config)
76 76
77 77 def _create_config(self, extras, repo_name):
78 78 config = utils.make_db_config(repo=repo_name)
79 79 config.set('rhodecode', 'RC_SCM_DATA', json.dumps(extras))
80 80
81 81 return config.serialize()
@@ -1,63 +1,63 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Equivalent of rhodecode.lib.middleware.scm_app but using remote apps.
23 23 """
24 24
25 25 import logging
26 26
27 27 from rhodecode.lib.middleware.utils import wsgi_app_caller_client
28 28
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 HG_REMOTE_WSGI = None
34 34 GIT_REMOTE_WSGI = None
35 35
36 36
37 def create_git_wsgi_app(repo_path, repo_name, config, backend):
37 def create_git_wsgi_app(repo_path, repo_name, config):
38 38 """
39 39 Return a WSGI app backed by a remote app to handle Git.
40 40
41 41 config is a dictionary holding the extras.
42 42 """
43 43 factory = GIT_REMOTE_WSGI
44 44 if not factory:
45 45 log.error('Pyro server has not been initialized yet')
46 46
47 47 return wsgi_app_caller_client.RemoteAppCaller(
48 factory, backend, repo_path, repo_name, config)
48 factory, repo_path, repo_name, config)
49 49
50 50
51 def create_hg_wsgi_app(repo_path, repo_name, config, backend):
51 def create_hg_wsgi_app(repo_path, repo_name, config):
52 52 """
53 53 Return a WSGI app backed by a remote app to handle Mercurial.
54 54
55 55 config is a list of 3-item tuples representing a ConfigObject (it is the
56 56 serialized version of the config object).
57 57 """
58 58 factory = HG_REMOTE_WSGI
59 59 if not factory:
60 60 log.error('Pyro server has not been initialized yet')
61 61
62 62 return wsgi_app_caller_client.RemoteAppCaller(
63 factory, backend, repo_path, repo_name, config)
63 factory, repo_path, repo_name, config)
@@ -1,136 +1,135 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Implementation of the scm_app interface using raw HTTP communication.
23 23 """
24 24
25 25 import base64
26 26 import logging
27 27 import urlparse
28 28 import wsgiref.util
29 29
30 30 import msgpack
31 31 import requests
32 32 import webob.request
33 33
34 34 import rhodecode
35 35
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 def create_git_wsgi_app(repo_path, repo_name, config, backend):
40 def create_git_wsgi_app(repo_path, repo_name, config):
41 41 url = _vcs_streaming_url() + 'git/'
42 return VcsHttpProxy(url, repo_path, repo_name, config, backend)
42 return VcsHttpProxy(url, repo_path, repo_name, config)
43 43
44 44
45 def create_hg_wsgi_app(repo_path, repo_name, config, backend):
45 def create_hg_wsgi_app(repo_path, repo_name, config):
46 46 url = _vcs_streaming_url() + 'hg/'
47 return VcsHttpProxy(url, repo_path, repo_name, config, backend)
47 return VcsHttpProxy(url, repo_path, repo_name, config)
48 48
49 49
50 50 def _vcs_streaming_url():
51 51 template = 'http://{}/stream/'
52 52 return template.format(rhodecode.CONFIG['vcs.server'])
53 53
54 54
55 55 # TODO: johbo: Avoid the global.
56 56 session = requests.Session()
57 57 # Requests speedup, avoid reading .netrc and similar
58 58 session.trust_env = False
59 59
60 60
61 61 class VcsHttpProxy(object):
62 62 """
63 63 A WSGI application which proxies vcs requests.
64 64
65 65 The goal is to shuffle the data around without touching it. The only
66 66 exception is the extra data from the config object which we send to the
67 67 server as well.
68 68 """
69 69
70 def __init__(self, url, repo_path, repo_name, config, backend):
70 def __init__(self, url, repo_path, repo_name, config):
71 71 """
72 72 :param str url: The URL of the VCSServer to call.
73 73 """
74 74 self._url = url
75 75 self._repo_name = repo_name
76 76 self._repo_path = repo_path
77 77 self._config = config
78 self._backend = backend
79 78 log.debug(
80 79 "Creating VcsHttpProxy for repo %s, url %s",
81 80 repo_name, url)
82 81
83 82 def __call__(self, environ, start_response):
84 83 status = '200 OK'
85 84
86 85 config = msgpack.packb(self._config)
87 86 request = webob.request.Request(environ)
88 87 request_headers = request.headers
89 88 request_headers.update({
90 89 # TODO: johbo: Remove this, rely on URL path only
91 90 'X-RC-Repo-Name': self._repo_name,
92 91 'X-RC-Repo-Path': self._repo_path,
93 92 'X-RC-Path-Info': environ['PATH_INFO'],
94 93 # TODO: johbo: Avoid encoding and put this into payload?
95 94 'X-RC-Repo-Config': base64.b64encode(config),
96 95 })
97 96
98 97 data = environ['wsgi.input'].read()
99 98 method = environ['REQUEST_METHOD']
100 99
101 100 # Preserve the query string
102 101 url = self._url
103 102 url = urlparse.urljoin(url, self._repo_name)
104 103 if environ.get('QUERY_STRING'):
105 104 url += '?' + environ['QUERY_STRING']
106 105
107 106 response = session.request(
108 107 method, url,
109 108 data=data,
110 109 headers=request_headers,
111 110 stream=True)
112 111
113 112 # Preserve the headers of the response, except hop_by_hop ones
114 113 response_headers = [
115 114 (h, v) for h, v in response.headers.items()
116 115 if not wsgiref.util.is_hop_by_hop(h)
117 116 ]
118 117
119 118 # TODO: johbo: Better way to get the status including text?
120 119 status = str(response.status_code)
121 120 start_response(status, response_headers)
122 121 return _maybe_stream(response)
123 122
124 123
125 124 def _maybe_stream(response):
126 125 """
127 126 Try to generate chunks from the response if it is chunked.
128 127 """
129 128 if _is_chunked(response):
130 129 return response.raw.read_chunked()
131 130 else:
132 131 return [response.content]
133 132
134 133
135 134 def _is_chunked(response):
136 135 return response.headers.get('Transfer-Encoding', '') == 'chunked'
@@ -1,100 +1,98 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Utility to call a WSGI app wrapped in a WSGIAppCaller object.
23 23 """
24 24
25 25 import logging
26 26
27 27 from Pyro4.errors import ConnectionClosedError
28 28
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 def _get_clean_environ(environ):
34 34 """Return a copy of the WSGI environment without wsgi.* keys.
35 35
36 36 It also omits any non-string values.
37 37
38 38 :param environ: WSGI environment to clean
39 39 :type environ: dict
40 40
41 41 :returns: WSGI environment to pass to WSGIAppCaller.handle.
42 42 :rtype: dict
43 43 """
44 44 clean_environ = dict(
45 45 (k, v) for k, v in environ.iteritems()
46 46 if type(v) == str and type(k) == str and not k.startswith('wsgi.')
47 47 )
48 48
49 49 return clean_environ
50 50
51 51
52 52 # pylint: disable=too-few-public-methods
53 53 class RemoteAppCaller(object):
54 54 """Create and calls a remote WSGI app using the given factory.
55 55
56 56 It first cleans the environment, so as to reduce the data transferred.
57 57 """
58 58
59 def __init__(self, remote_wsgi, backend, *args, **kwargs):
59 def __init__(self, remote_wsgi, *args, **kwargs):
60 60 """
61 61 :param remote_wsgi: The remote wsgi object that creates a
62 62 WSGIAppCaller. This object
63 63 has to have a handle method, with the signature:
64 64 handle(environ, start_response, *args, **kwargs)
65 :param backend: Key (str) of the SCM backend that is in use.
66 65 :param args: args to be passed to the app creation
67 66 :param kwargs: kwargs to be passed to the app creation
68 67 """
69 68 self._remote_wsgi = remote_wsgi
70 self._backend = backend
71 69 self._args = args
72 70 self._kwargs = kwargs
73 71
74 72 def __call__(self, environ, start_response):
75 73 """
76 74 :param environ: WSGI environment with which the app will be run
77 75 :type environ: dict
78 76 :param start_response: callable of WSGI protocol
79 77 :type start_response: callable
80 78
81 79 :returns: an iterable with the data returned by the app
82 80 :rtype: iterable<str>
83 81 """
84 82 log.debug("Forwarding WSGI request via proxy %s", self._remote_wsgi)
85 83 input_data = environ['wsgi.input'].read()
86 84 clean_environ = _get_clean_environ(environ)
87 85
88 86 try:
89 87 data, status, headers = self._remote_wsgi.handle(
90 88 clean_environ, input_data, *self._args, **self._kwargs)
91 89 except ConnectionClosedError:
92 90 log.debug('Remote Pyro Server ConnectionClosedError')
93 91 self._remote_wsgi._pyroReconnect(tries=15)
94 92 data, status, headers = self._remote_wsgi.handle(
95 93 clean_environ, input_data, *self._args, **self._kwargs)
96 94
97 95 log.debug("Got result from proxy, returning to WSGI container")
98 96 start_response(status, headers)
99 97
100 98 return data
@@ -1,41 +1,41 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Provides a mock implementation of the scm_app module.
23 23
24 24 It resembles the same API as :mod:`rhodecode.lib.middleware.utils.scm_app`
25 25 for testing purposes.
26 26 """
27 27 import mock
28 28
29 29
30 def create_git_wsgi_app(repo_path, repo_name, config, backend):
30 def create_git_wsgi_app(repo_path, repo_name, config):
31 31 return mock_git_wsgi
32 32
33 33
34 def create_hg_wsgi_app(repo_path, repo_name, config, backend):
34 def create_hg_wsgi_app(repo_path, repo_name, config):
35 35 return mock_hg_wsgi
36 36
37 37
38 38 # Utilities to ease testing
39 39
40 40 mock_hg_wsgi = mock.Mock()
41 41 mock_git_wsgi = mock.Mock()
@@ -1,131 +1,130 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import Pyro4
23 23 import pytest
24 24 import webtest
25 25
26 26 from rhodecode.lib.middleware.utils import scm_app_http, scm_app
27 from rhodecode.lib.middleware.simplegit import SimpleGit
28 27 from rhodecode.lib.vcs.conf import settings
29 28
30 29
31 30 def vcs_http_app(vcsserver_http_echo_app):
32 31 """
33 32 VcsHttpProxy wrapped in WebTest.
34 33 """
35 34 git_url = vcsserver_http_echo_app.http_url + 'stream/git/'
36 35 vcs_http_proxy = scm_app_http.VcsHttpProxy(
37 git_url, 'stub_path', 'stub_name', None, 'stub_backend')
36 git_url, 'stub_path', 'stub_name', None)
38 37 app = webtest.TestApp(vcs_http_proxy)
39 38 return app
40 39
41 40
42 41 @pytest.fixture(scope='module')
43 42 def vcsserver_http_echo_app(request, vcsserver_factory):
44 43 """
45 44 A running VCSServer with the EchoApp activated via HTTP.
46 45 """
47 46 vcsserver = vcsserver_factory(
48 47 request=request,
49 48 use_http=True,
50 49 overrides=[{'app:main': {'dev.use_echo_app': 'true'}}])
51 50 return vcsserver
52 51
53 52
54 53 @pytest.fixture(scope='session')
55 54 def data():
56 55 one_kb = "x" * 1024
57 56 return one_kb * 1024 * 10
58 57
59 58
60 59 def test_reuse_app_no_data(repeat, vcsserver_http_echo_app):
61 60 app = vcs_http_app(vcsserver_http_echo_app)
62 61 for x in xrange(repeat / 10):
63 62 response = app.post('/')
64 63 assert response.status_code == 200
65 64
66 65
67 66 def test_reuse_app_with_data(data, repeat, vcsserver_http_echo_app):
68 67 app = vcs_http_app(vcsserver_http_echo_app)
69 68 for x in xrange(repeat / 10):
70 69 response = app.post('/', params=data)
71 70 assert response.status_code == 200
72 71
73 72
74 73 def test_create_app_per_request_no_data(repeat, vcsserver_http_echo_app):
75 74 for x in xrange(repeat / 10):
76 75 app = vcs_http_app(vcsserver_http_echo_app)
77 76 response = app.post('/')
78 77 assert response.status_code == 200
79 78
80 79
81 80 def test_create_app_per_request_with_data(
82 81 data, repeat, vcsserver_http_echo_app):
83 82 for x in xrange(repeat / 10):
84 83 app = vcs_http_app(vcsserver_http_echo_app)
85 84 response = app.post('/', params=data)
86 85 assert response.status_code == 200
87 86
88 87
89 88 @pytest.fixture(scope='module')
90 89 def vcsserver_pyro_echo_app(request, vcsserver_factory):
91 90 """
92 91 A running VCSServer with the EchoApp activated via Pyro4.
93 92 """
94 93 vcsserver = vcsserver_factory(
95 94 request=request,
96 95 use_http=False,
97 96 overrides=[{'DEFAULT': {'dev.use_echo_app': 'true'}}])
98 97 return vcsserver
99 98
100 99
101 100 def vcs_pyro4_app(vcsserver_pyro_echo_app):
102 101 """
103 102 Pyro4 based Vcs proxy wrapped in WebTest
104 103 """
105 104 stub_config = {
106 105 'git_update_server_info': 'stub',
107 106 }
108 107 server_and_port = vcsserver_pyro_echo_app.server_and_port
109 108 GIT_REMOTE_WSGI = Pyro4.Proxy(
110 109 settings.pyro_remote(
111 110 settings.PYRO_GIT_REMOTE_WSGI, server_and_port))
112 111 with mock.patch('rhodecode.lib.middleware.utils.scm_app.GIT_REMOTE_WSGI',
113 112 GIT_REMOTE_WSGI):
114 113 pyro4_app = scm_app.create_git_wsgi_app(
115 'stub_path', 'stub_name', stub_config, SimpleGit.SCM)
114 'stub_path', 'stub_name', stub_config)
116 115 app = webtest.TestApp(pyro4_app)
117 116 return app
118 117
119 118
120 119 def test_pyro4_no_data(repeat, pylonsapp, vcsserver_pyro_echo_app):
121 120 for x in xrange(repeat / 10):
122 121 app = vcs_pyro4_app(vcsserver_pyro_echo_app)
123 122 response = app.post('/')
124 123 assert response.status_code == 200
125 124
126 125
127 126 def test_pyro4_with_data(repeat, pylonsapp, vcsserver_pyro_echo_app, data):
128 127 for x in xrange(repeat / 10):
129 128 app = vcs_pyro4_app(vcsserver_pyro_echo_app)
130 129 response = app.post('/', params=data)
131 130 assert response.status_code == 200
@@ -1,136 +1,136 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Checking the chunked data transfer via HTTP
23 23 """
24 24
25 25 import os
26 26 import time
27 27 import subprocess
28 28
29 29 import pytest
30 30 import requests
31 31
32 32 from rhodecode.lib.middleware.utils import scm_app_http
33 33 from rhodecode.tests.utils import wait_for_url
34 34
35 35
36 36 def test_does_chunked_end_to_end_transfer(scm_app):
37 37 response = requests.post(scm_app, data='', stream=True)
38 38 assert response.headers['Transfer-Encoding'] == 'chunked'
39 39 times = [time.time() for chunk in response.raw.read_chunked()]
40 40 assert times[1] - times[0] > 0.1, "Chunks arrived at the same time"
41 41
42 42
43 43 @pytest.fixture
44 44 def echo_app_chunking(request, available_port_factory):
45 45 """
46 46 Run the EchoApp via Waitress in a subprocess.
47 47
48 48 Return the URL endpoint to reach the app.
49 49 """
50 50 port = available_port_factory()
51 51 command = (
52 52 'waitress-serve --send-bytes 1 --port {port} --call '
53 53 'rhodecode.tests.lib.middleware.utils.test_scm_app_http_chunking'
54 54 ':create_echo_app')
55 55 command = command.format(port=port)
56 56 proc = subprocess.Popen(command.split(' '), bufsize=0)
57 57 echo_app_url = 'http://localhost:' + str(port)
58 58
59 59 @request.addfinalizer
60 60 def stop_echo_app():
61 61 proc.kill()
62 62
63 63 return echo_app_url
64 64
65 65
66 66 @pytest.fixture
67 67 def scm_app(request, available_port_factory, echo_app_chunking):
68 68 """
69 69 Run the scm_app in Waitress.
70 70
71 71 Returns the URL endpoint where this app can be reached.
72 72 """
73 73 port = available_port_factory()
74 74 command = (
75 75 'waitress-serve --send-bytes 1 --port {port} --call '
76 76 'rhodecode.tests.lib.middleware.utils.test_scm_app_http_chunking'
77 77 ':create_scm_app')
78 78 command = command.format(port=port)
79 79 env = os.environ.copy()
80 80 env["RC_ECHO_URL"] = echo_app_chunking
81 81 proc = subprocess.Popen(command.split(' '), bufsize=0, env=env)
82 82 scm_app_url = 'http://localhost:' + str(port)
83 83 wait_for_url(scm_app_url)
84 84
85 85 @request.addfinalizer
86 86 def stop_echo_app():
87 87 proc.kill()
88 88
89 89 return scm_app_url
90 90
91 91
92 92 class EchoApp(object):
93 93 """
94 94 Stub WSGI application which returns a chunked response to every request.
95 95 """
96 96
97 97 def __init__(self, repo_path, repo_name, config):
98 98 self._repo_path = repo_path
99 99
100 100 def __call__(self, environ, start_response):
101 101 environ['wsgi.input'].read()
102 102 status = '200 OK'
103 103 headers = []
104 104 start_response(status, headers)
105 105 return result_generator()
106 106
107 107
108 108 def result_generator():
109 109 """
110 110 Simulate chunked results.
111 111
112 112 The intended usage is to simulate a chunked response as we would get it
113 113 out of a vcs operation during a call to "hg clone".
114 114 """
115 115 yield 'waiting 2 seconds'
116 116 # Wait long enough so that the first chunk can go out
117 117 time.sleep(2)
118 118 yield 'final chunk'
119 119 # Another small wait, otherwise they go together
120 120 time.sleep(0.1)
121 121
122 122
123 123 def create_echo_app():
124 124 """
125 125 Create EchoApp filled with stub data.
126 126 """
127 127 return EchoApp('stub_path', 'repo_name', {})
128 128
129 129
130 130 def create_scm_app():
131 131 """
132 132 Create a scm_app hooked up to speak to EchoApp.
133 133 """
134 134 echo_app_url = os.environ["RC_ECHO_URL"]
135 135 return scm_app_http.VcsHttpProxy(
136 echo_app_url, 'stub_path', 'stub_name', None, 'stub_backend')
136 echo_app_url, 'stub_path', 'stub_name', None)
@@ -1,102 +1,100 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import webtest
22 22
23 23 from rhodecode.lib.middleware.utils import wsgi_app_caller_client
24 24
25 25 # pylint: disable=protected-access,too-many-public-methods
26 26
27 27
28 28 BASE_ENVIRON = {
29 29 'REQUEST_METHOD': 'GET',
30 30 'SERVER_NAME': 'localhost',
31 31 'SERVER_PORT': '80',
32 32 'SCRIPT_NAME': '',
33 33 'PATH_INFO': '/',
34 34 'QUERY_STRING': '',
35 35 'foo.bool_var': True,
36 36 'foo.str_var': 'True',
37 37 'wsgi.foo': True,
38 38 # Some non string values. The validator expects to get an iterable as
39 39 # value.
40 40 (42,): '42',
41 41 (True,): 'False',
42 42 }
43 43
44 44
45 45 def assert_all_values_are_str(environ):
46 46 """Checks that all values of a dict are str."""
47 47 for key, value in environ.iteritems():
48 48 assert isinstance(value, str), (
49 49 "Value for key %s: has type %s but 'str' was expected. Value: %s" %
50 50 (key, type(value), repr(value)))
51 51
52 52
53 53 def assert_all_keys_are_str(environ):
54 54 """Checks that all keys of a dict are str."""
55 55 for key, value in environ.iteritems():
56 56 assert isinstance(value, str), (
57 57 "Key %s: has type %s but 'str' was expected. " %
58 58 (repr(key), type(key)))
59 59
60 60
61 61 def assert_no_prefix_in_keys(environ, prefix):
62 62 """Checks that no key of the dict starts with the prefix."""
63 63 for key in environ:
64 64 assert not key.startswith(prefix), 'Key %s should not be present' % key
65 65
66 66
67 67 def test_get_environ():
68 68 clean_environ = wsgi_app_caller_client._get_clean_environ(BASE_ENVIRON)
69 69
70 70 assert len(clean_environ) == 7
71 71 assert_no_prefix_in_keys(clean_environ, 'wsgi.')
72 72 assert_all_keys_are_str(clean_environ)
73 73 assert_all_values_are_str(clean_environ)
74 74
75 75
76 76 def test_remote_app_caller():
77 77
78 78 class RemoteAppCallerMock(object):
79 79
80 80 def handle(self, environ, input_data, arg1, arg2,
81 81 arg3=None, arg4=None, arg5=None):
82 82 assert ((arg1, arg2, arg3, arg4, arg5) ==
83 83 ('a1', 'a2', 'a3', 'a4', None))
84 84 # Note: RemoteAppCaller is expected to return a tuple like the
85 85 # following one
86 86 return (['content'], '200 OK', [('Content-Type', 'text/plain')])
87 87
88 88 wrapper_app = wsgi_app_caller_client.RemoteAppCaller(
89 RemoteAppCallerMock(), 'dummy-backend',
90 'a1', 'a2', arg3='a3', arg4='a4')
89 RemoteAppCallerMock(), 'a1', 'a2', arg3='a3', arg4='a4')
91 90
92 91 test_app = webtest.TestApp(wrapper_app)
93 92
94 93 response = test_app.get('/path')
95 94
96 95 assert response.status == '200 OK'
97 96 assert sorted(response.headers.items()) == sorted([
98 ('X-RhodeCode-Backend', 'dummy-backend'),
99 97 ('Content-Type', 'text/plain'),
100 98 ('Content-Length', '7'),
101 99 ])
102 100 assert response.body == 'content'
General Comments 0
You need to be logged in to leave comments. Login now