##// END OF EJS Templates
tests: Use only one place to define pr and shadow URL segments.
Martin Bornhold -
r920:c4b5ba79 default
parent child Browse files
Show More
@@ -1,451 +1,461 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 base64
22 22
23 23 import mock
24 24 import pytest
25 25 import webtest.app
26 26
27 27 from rhodecode.lib.caching_query import FromCache
28 28 from rhodecode.lib.hooks_daemon import DummyHooksCallbackDaemon
29 29 from rhodecode.lib.middleware import simplevcs
30 30 from rhodecode.lib.middleware.https_fixup import HttpsFixup
31 31 from rhodecode.lib.middleware.utils import scm_app
32 32 from rhodecode.model.db import User, _hash_key
33 33 from rhodecode.model.meta import Session
34 34 from rhodecode.tests import (
35 35 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
36 36 from rhodecode.tests.lib.middleware import mock_scm_app
37 37 from rhodecode.tests.utils import set_anonymous_access
38 38
39 39
40 40 class StubVCSController(simplevcs.SimpleVCS):
41 41
42 42 SCM = 'hg'
43 43 stub_response_body = tuple()
44 44
45 45 def __init__(self, *args, **kwargs):
46 46 super(StubVCSController, self).__init__(*args, **kwargs)
47 47 self._action = 'pull'
48 48 self._name = HG_REPO
49 49 self.set_repo_names(None)
50 50
51 51 def _get_repository_name(self, environ):
52 52 return self._name
53 53
54 54 def _get_action(self, environ):
55 55 return self._action
56 56
57 57 def _create_wsgi_app(self, repo_path, repo_name, config):
58 58 def fake_app(environ, start_response):
59 59 start_response('200 OK', [])
60 60 return self.stub_response_body
61 61 return fake_app
62 62
63 63 def _create_config(self, extras, repo_name):
64 64 return None
65 65
66 66
67 67 @pytest.fixture
68 68 def vcscontroller(pylonsapp, config_stub):
69 69 config_stub.testing_securitypolicy()
70 70 config_stub.include('rhodecode.authentication')
71 71
72 72 set_anonymous_access(True)
73 73 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
74 74 app = HttpsFixup(controller, pylonsapp.config)
75 75 app = webtest.app.TestApp(app)
76 76
77 77 _remove_default_user_from_query_cache()
78 78
79 79 # Sanity checks that things are set up correctly
80 80 app.get('/' + HG_REPO, status=200)
81 81
82 82 app.controller = controller
83 83 return app
84 84
85 85
86 86 def _remove_default_user_from_query_cache():
87 87 user = User.get_default_user(cache=True)
88 88 query = Session().query(User).filter(User.username == user.username)
89 89 query = query.options(FromCache(
90 90 "sql_cache_short", "get_user_%s" % _hash_key(user.username)))
91 91 query.invalidate()
92 92 Session().expire(user)
93 93
94 94
95 95 @pytest.fixture
96 96 def disable_anonymous_user(request, pylonsapp):
97 97 set_anonymous_access(False)
98 98
99 99 @request.addfinalizer
100 100 def cleanup():
101 101 set_anonymous_access(True)
102 102
103 103
104 104 def test_handles_exceptions_during_permissions_checks(
105 105 vcscontroller, disable_anonymous_user):
106 106 user_and_pass = '%s:%s' % (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
107 107 auth_password = base64.encodestring(user_and_pass).strip()
108 108 extra_environ = {
109 109 'AUTH_TYPE': 'Basic',
110 110 'HTTP_AUTHORIZATION': 'Basic %s' % auth_password,
111 111 'REMOTE_USER': TEST_USER_ADMIN_LOGIN,
112 112 }
113 113
114 114 # Verify that things are hooked up correctly
115 115 vcscontroller.get('/', status=200, extra_environ=extra_environ)
116 116
117 117 # Simulate trouble during permission checks
118 118 with mock.patch('rhodecode.model.db.User.get_by_username',
119 119 side_effect=Exception) as get_user:
120 120 # Verify that a correct 500 is returned and check that the expected
121 121 # code path was hit.
122 122 vcscontroller.get('/', status=500, extra_environ=extra_environ)
123 123 assert get_user.called
124 124
125 125
126 126 def test_returns_forbidden_if_no_anonymous_access(
127 127 vcscontroller, disable_anonymous_user):
128 128 vcscontroller.get('/', status=401)
129 129
130 130
131 131 class StubFailVCSController(simplevcs.SimpleVCS):
132 132 def _handle_request(self, environ, start_response):
133 133 raise Exception("BOOM")
134 134
135 135
136 136 @pytest.fixture(scope='module')
137 137 def fail_controller(pylonsapp):
138 138 controller = StubFailVCSController(pylonsapp, pylonsapp.config, None)
139 139 controller = HttpsFixup(controller, pylonsapp.config)
140 140 controller = webtest.app.TestApp(controller)
141 141 return controller
142 142
143 143
144 144 def test_handles_exceptions_as_internal_server_error(fail_controller):
145 145 fail_controller.get('/', status=500)
146 146
147 147
148 148 def test_provides_traceback_for_appenlight(fail_controller):
149 149 response = fail_controller.get(
150 150 '/', status=500, extra_environ={'appenlight.client': 'fake'})
151 151 assert 'appenlight.__traceback' in response.request.environ
152 152
153 153
154 154 def test_provides_utils_scm_app_as_scm_app_by_default(pylonsapp):
155 155 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
156 156 assert controller.scm_app is scm_app
157 157
158 158
159 159 def test_allows_to_override_scm_app_via_config(pylonsapp):
160 160 config = pylonsapp.config.copy()
161 161 config['vcs.scm_app_implementation'] = (
162 162 'rhodecode.tests.lib.middleware.mock_scm_app')
163 163 controller = StubVCSController(pylonsapp, config, None)
164 164 assert controller.scm_app is mock_scm_app
165 165
166 166
167 167 @pytest.mark.parametrize('query_string, expected', [
168 168 ('cmd=stub_command', True),
169 169 ('cmd=listkeys', False),
170 170 ])
171 171 def test_should_check_locking(query_string, expected):
172 172 result = simplevcs._should_check_locking(query_string)
173 173 assert result == expected
174 174
175 175
176 176 class TestShadowRepoRegularExpression(object):
177 pr_segment = 'pull-request'
178 shadow_segment = 'repository'
179
177 180 @pytest.mark.parametrize('url, expected', [
178 181 # repo with/without groups
179 ('My-Repo/pull-request/1/repository', True),
180 ('Group/My-Repo/pull-request/2/repository', True),
181 ('Group/Sub-Group/My-Repo/pull-request/3/repository', True),
182 ('Group/Sub-Group1/Sub-Group2/My-Repo/pull-request/3/repository', True),
182 ('My-Repo/{pr_segment}/1/{shadow_segment}', True),
183 ('Group/My-Repo/{pr_segment}/2/{shadow_segment}', True),
184 ('Group/Sub-Group/My-Repo/{pr_segment}/3/{shadow_segment}', True),
185 ('Group/Sub-Group1/Sub-Group2/My-Repo/{pr_segment}/3/{shadow_segment}', True),
183 186
184 187 # pull request ID
185 ('MyRepo/pull-request/1/repository', True),
186 ('MyRepo/pull-request/1234567890/repository', True),
187 ('MyRepo/pull-request/-1/repository', False),
188 ('MyRepo/pull-request/invalid/repository', False),
188 ('MyRepo/{pr_segment}/1/{shadow_segment}', True),
189 ('MyRepo/{pr_segment}/1234567890/{shadow_segment}', True),
190 ('MyRepo/{pr_segment}/-1/{shadow_segment}', False),
191 ('MyRepo/{pr_segment}/invalid/{shadow_segment}', False),
189 192
190 193 # unicode
191 (u'Sp€çîál-Repö/pull-request/1/repository', True),
192 (u'Sp€çîál-Gröüp/Sp€çîál-Repö/pull-request/1/repository', True),
194 (u'Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
195 (u'Sp€çîál-Gröüp/Sp€çîál-Repö/{pr_segment}/1/{shadow_segment}', True),
193 196
194 197 # trailing/leading slash
195 ('/My-Repo/pull-request/1/repository', False),
196 ('My-Repo/pull-request/1/repository/', False),
197 ('/My-Repo/pull-request/1/repository/', False),
198 ('/My-Repo/{pr_segment}/1/{shadow_segment}', False),
199 ('My-Repo/{pr_segment}/1/{shadow_segment}/', False),
200 ('/My-Repo/{pr_segment}/1/{shadow_segment}/', False),
198 201
199 202 # misc
200 ('My-Repo/pull-request/1/repository/extra', False),
201 ('My-Repo/pull-request/1/repositoryextra', False),
203 ('My-Repo/{pr_segment}/1/{shadow_segment}/extra', False),
204 ('My-Repo/{pr_segment}/1/{shadow_segment}extra', False),
202 205 ])
203 206 def test_shadow_repo_regular_expression(self, url, expected):
204 207 from rhodecode.lib.middleware.simplevcs import SimpleVCS
208 url = url.format(
209 pr_segment=self.pr_segment,
210 shadow_segment=self.shadow_segment)
205 211 match_obj = SimpleVCS.shadow_repo_re.match(url)
206 212 assert (match_obj is not None) == expected
207 213
208 214
209 215 @pytest.mark.backends('git', 'hg')
210 216 class TestShadowRepoExposure(object):
211 217
212 218 def test_pull_on_shadow_repo_propagates_to_wsgi_app(self, pylonsapp):
213 219 """
214 220 Check that a pull action to a shadow repo is propagated to the
215 221 underlying wsgi app.
216 222 """
217 223 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
218 224 controller._check_ssl = mock.Mock()
219 225 controller.is_shadow_repo = True
220 226 controller._action = 'pull'
221 227 controller.stub_response_body = 'dummy body value'
222 228 environ_stub = {
223 229 'HTTP_HOST': 'test.example.com',
224 230 'REQUEST_METHOD': 'GET',
225 231 'wsgi.url_scheme': 'http',
226 232 }
227 233
228 234 response = controller(environ_stub, mock.Mock())
229 235 response_body = ''.join(response)
230 236
231 237 # Assert that we got the response from the wsgi app.
232 238 assert response_body == controller.stub_response_body
233 239
234 240 def test_push_on_shadow_repo_raises(self, pylonsapp):
235 241 """
236 242 Check that a push action to a shadow repo is aborted.
237 243 """
238 244 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
239 245 controller._check_ssl = mock.Mock()
240 246 controller.is_shadow_repo = True
241 247 controller._action = 'push'
242 248 controller.stub_response_body = 'dummy body value'
243 249 environ_stub = {
244 250 'HTTP_HOST': 'test.example.com',
245 251 'REQUEST_METHOD': 'GET',
246 252 'wsgi.url_scheme': 'http',
247 253 }
248 254
249 255 response = controller(environ_stub, mock.Mock())
250 256 response_body = ''.join(response)
251 257
252 258 assert response_body != controller.stub_response_body
253 259 # Assert that a 406 error is returned.
254 260 assert '406 Not Acceptable' in response_body
255 261
256 262 def test_set_repo_names_no_shadow(self, pylonsapp):
257 263 """
258 264 Check that the set_repo_names method sets all names to the one returned
259 265 by the _get_repository_name method on a request to a non shadow repo.
260 266 """
261 267 environ_stub = {}
262 268 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
263 269 controller._name = 'RepoGroup/MyRepo'
264 270 controller.set_repo_names(environ_stub)
265 271 assert not controller.is_shadow_repo
266 272 assert (controller.url_repo_name ==
267 273 controller.acl_repo_name ==
268 274 controller.vcs_repo_name ==
269 275 controller._get_repository_name(environ_stub))
270 276
271 277 def test_set_repo_names_with_shadow(self, pylonsapp, pr_util):
272 278 """
273 279 Check that the set_repo_names method sets correct names on a request
274 280 to a shadow repo.
275 281 """
276 282 from rhodecode.model.pull_request import PullRequestModel
277 283
278 284 pull_request = pr_util.create_pull_request()
279 shadow_url = '{target}/pull-request/{pr_id}/repository'.format(
285 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
280 286 target=pull_request.target_repo.repo_name,
281 pr_id=pull_request.pull_request_id)
287 pr_id=pull_request.pull_request_id,
288 pr_segment=TestShadowRepoRegularExpression.pr_segment,
289 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
282 290 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
283 291 controller._name = shadow_url
284 292 controller.set_repo_names({})
285 293
286 294 # Get file system path to shadow repo for assertions.
287 295 workspace_id = PullRequestModel()._workspace_id(pull_request)
288 296 target_vcs = pull_request.target_repo.scm_instance()
289 297 vcs_repo_name = target_vcs._get_shadow_repository_path(
290 298 workspace_id)
291 299
292 300 assert controller.vcs_repo_name == vcs_repo_name
293 301 assert controller.url_repo_name == shadow_url
294 302 assert controller.acl_repo_name == pull_request.target_repo.repo_name
295 303 assert controller.is_shadow_repo
296 304
297 305 def test_set_repo_names_with_shadow_but_missing_pr(
298 306 self, pylonsapp, pr_util):
299 307 """
300 308 Checks that the set_repo_names method enforces matching target repos
301 309 and pull request IDs.
302 310 """
303 311 pull_request = pr_util.create_pull_request()
304 shadow_url = '{target}/pull-request/{pr_id}/repository'.format(
312 shadow_url = '{target}/{pr_segment}/{pr_id}/{shadow_segment}'.format(
305 313 target=pull_request.target_repo.repo_name,
306 pr_id=999999999)
314 pr_id=999999999,
315 pr_segment=TestShadowRepoRegularExpression.pr_segment,
316 shadow_segment=TestShadowRepoRegularExpression.shadow_segment)
307 317 controller = StubVCSController(pylonsapp, pylonsapp.config, None)
308 318 controller._name = shadow_url
309 319 controller.set_repo_names({})
310 320
311 321 assert not controller.is_shadow_repo
312 322 assert (controller.url_repo_name ==
313 323 controller.acl_repo_name ==
314 324 controller.vcs_repo_name)
315 325
316 326
317 327 @pytest.mark.usefixtures('db')
318 328 @mock.patch.multiple(
319 329 'Pyro4.config', SERVERTYPE='multiplex', POLLTIMEOUT=0.01)
320 330 class TestGenerateVcsResponse:
321 331
322 332 def test_ensures_that_start_response_is_called_early_enough(self):
323 333 self.call_controller_with_response_body(iter(['a', 'b']))
324 334 assert self.start_response.called
325 335
326 336 def test_invalidates_cache_after_body_is_consumed(self):
327 337 result = self.call_controller_with_response_body(iter(['a', 'b']))
328 338 assert not self.was_cache_invalidated()
329 339 # Consume the result
330 340 list(result)
331 341 assert self.was_cache_invalidated()
332 342
333 343 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
334 344 def test_handles_locking_exception(self, http_locked_rc):
335 345 result = self.call_controller_with_response_body(
336 346 self.raise_result_iter(vcs_kind='repo_locked'))
337 347 assert not http_locked_rc.called
338 348 # Consume the result
339 349 list(result)
340 350 assert http_locked_rc.called
341 351
342 352 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPRequirementError')
343 353 def test_handles_requirement_exception(self, http_requirement):
344 354 result = self.call_controller_with_response_body(
345 355 self.raise_result_iter(vcs_kind='requirement'))
346 356 assert not http_requirement.called
347 357 # Consume the result
348 358 list(result)
349 359 assert http_requirement.called
350 360
351 361 @mock.patch('rhodecode.lib.middleware.simplevcs.HTTPLockedRC')
352 362 def test_handles_locking_exception_in_app_call(self, http_locked_rc):
353 363 app_factory_patcher = mock.patch.object(
354 364 StubVCSController, '_create_wsgi_app')
355 365 with app_factory_patcher as app_factory:
356 366 app_factory().side_effect = self.vcs_exception()
357 367 result = self.call_controller_with_response_body(['a'])
358 368 list(result)
359 369 assert http_locked_rc.called
360 370
361 371 def test_raises_unknown_exceptions(self):
362 372 result = self.call_controller_with_response_body(
363 373 self.raise_result_iter(vcs_kind='unknown'))
364 374 with pytest.raises(Exception):
365 375 list(result)
366 376
367 377 def test_prepare_callback_daemon_is_called(self):
368 378 def side_effect(extras):
369 379 return DummyHooksCallbackDaemon(), extras
370 380
371 381 prepare_patcher = mock.patch.object(
372 382 StubVCSController, '_prepare_callback_daemon')
373 383 with prepare_patcher as prepare_mock:
374 384 prepare_mock.side_effect = side_effect
375 385 self.call_controller_with_response_body(iter(['a', 'b']))
376 386 assert prepare_mock.called
377 387 assert prepare_mock.call_count == 1
378 388
379 389 def call_controller_with_response_body(self, response_body):
380 390 settings = {
381 391 'base_path': 'fake_base_path',
382 392 'vcs.hooks.protocol': 'http',
383 393 'vcs.hooks.direct_calls': False,
384 394 }
385 395 controller = StubVCSController(None, settings, None)
386 396 controller._invalidate_cache = mock.Mock()
387 397 controller.stub_response_body = response_body
388 398 self.start_response = mock.Mock()
389 399 result = controller._generate_vcs_response(
390 400 environ={}, start_response=self.start_response,
391 401 repo_path='fake_repo_path',
392 402 extras={}, action='push')
393 403 self.controller = controller
394 404 return result
395 405
396 406 def raise_result_iter(self, vcs_kind='repo_locked'):
397 407 """
398 408 Simulates an exception due to a vcs raised exception if kind vcs_kind
399 409 """
400 410 raise self.vcs_exception(vcs_kind=vcs_kind)
401 411 yield "never_reached"
402 412
403 413 def vcs_exception(self, vcs_kind='repo_locked'):
404 414 locked_exception = Exception('TEST_MESSAGE')
405 415 locked_exception._vcs_kind = vcs_kind
406 416 return locked_exception
407 417
408 418 def was_cache_invalidated(self):
409 419 return self.controller._invalidate_cache.called
410 420
411 421
412 422 class TestInitializeGenerator:
413 423
414 424 def test_drains_first_element(self):
415 425 gen = self.factory(['__init__', 1, 2])
416 426 result = list(gen)
417 427 assert result == [1, 2]
418 428
419 429 @pytest.mark.parametrize('values', [
420 430 [],
421 431 [1, 2],
422 432 ])
423 433 def test_raises_value_error(self, values):
424 434 with pytest.raises(ValueError):
425 435 self.factory(values)
426 436
427 437 @simplevcs.initialize_generator
428 438 def factory(self, iterable):
429 439 for elem in iterable:
430 440 yield elem
431 441
432 442
433 443 class TestPrepareHooksDaemon(object):
434 444 def test_calls_imported_prepare_callback_daemon(self, app_settings):
435 445 expected_extras = {'extra1': 'value1'}
436 446 daemon = DummyHooksCallbackDaemon()
437 447
438 448 controller = StubVCSController(None, app_settings, None)
439 449 prepare_patcher = mock.patch.object(
440 450 simplevcs, 'prepare_callback_daemon',
441 451 return_value=(daemon, expected_extras))
442 452 with prepare_patcher as prepare_mock:
443 453 callback_daemon, extras = controller._prepare_callback_daemon(
444 454 expected_extras.copy())
445 455 prepare_mock.assert_called_once_with(
446 456 expected_extras,
447 457 protocol=app_settings['vcs.hooks.protocol'],
448 458 use_direct_calls=app_settings['vcs.hooks.direct_calls'])
449 459
450 460 assert callback_daemon == daemon
451 461 assert extras == extras
General Comments 0
You need to be logged in to leave comments. Login now