tests: fixed test suite for celery adoption

This commit is contained in:
RhodeCode Admin 2024-11-08 16:17:57 +01:00
parent 840634c16f
commit 1d5d2c0155
209 changed files with 5248 additions and 5275 deletions

View file

@ -27,8 +27,11 @@ from rhodecode.tests.conftest_common import ( # noqa
pytest_plugins = [ pytest_plugins = [
"rhodecode.tests.fixture_mods.fixture_pyramid", "rhodecode.tests.fixtures.fixture_pyramid",
"rhodecode.tests.fixture_mods.fixture_utils", "rhodecode.tests.fixtures.fixture_utils",
"rhodecode.tests.fixtures.function_scoped_baseapp",
"rhodecode.tests.fixtures.module_scoped_baseapp",
"rhodecode.tests.fixtures.rcextensions_fixtures",
] ]

View file

@ -65,8 +65,7 @@ dependencies = {file = ["requirements.txt"]}
optional-dependencies.tests = {file = ["requirements_test.txt"]} optional-dependencies.tests = {file = ["requirements_test.txt"]}
[tool.ruff] [tool.ruff]
lint.select = [
select = [
# Pyflakes # Pyflakes
"F", "F",
# Pycodestyle # Pycodestyle
@ -75,16 +74,13 @@ select = [
# isort # isort
"I001" "I001"
] ]
lint.ignore = [
ignore = [
"E501", # line too long, handled by black "E501", # line too long, handled by black
] ]
# Same as Black. # Same as Black.
line-length = 120 line-length = 120
[tool.ruff.isort] [tool.ruff.lint.isort]
known-first-party = ["rhodecode"] known-first-party = ["rhodecode"]
[tool.ruff.format] [tool.ruff.format]

View file

@ -4,8 +4,10 @@ norecursedirs = rhodecode/public rhodecode/templates tests/scripts
cache_dir = /tmp/.pytest_cache cache_dir = /tmp/.pytest_cache
pyramid_config = rhodecode/tests/rhodecode.ini pyramid_config = rhodecode/tests/rhodecode.ini
vcsserver_protocol = http
vcsserver_config_http = rhodecode/tests/vcsserver_http.ini vcsserver_config = rhodecode/tests/vcsserver_http.ini
rhodecode_config = rhodecode/tests/rhodecode.ini
celery_config = rhodecode/tests/rhodecode.ini
addopts = addopts =
--pdbcls=IPython.terminal.debugger:TerminalPdb --pdbcls=IPython.terminal.debugger:TerminalPdb

View file

@ -1,5 +1,4 @@
# Copyright (C) 2010-2024 RhodeCode GmbH
# Copyright (C) 2010-2023 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3 # it under the terms of the GNU Affero General Public License, version 3

View file

@ -24,7 +24,7 @@ from rhodecode.model.db import Gist
from rhodecode.model.gist import GistModel from rhodecode.model.gist import GistModel
from rhodecode.api.tests.utils import ( from rhodecode.api.tests.utils import (
build_data, api_call, assert_error, assert_ok, crash) build_data, api_call, assert_error, assert_ok, crash)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
@pytest.mark.usefixtures("testuser_api", "app") @pytest.mark.usefixtures("testuser_api", "app")

View file

@ -27,7 +27,7 @@ from rhodecode.model.user import UserModel
from rhodecode.tests import TEST_USER_ADMIN_LOGIN from rhodecode.tests import TEST_USER_ADMIN_LOGIN
from rhodecode.api.tests.utils import ( from rhodecode.api.tests.utils import (
build_data, api_call, assert_ok, assert_error, crash) build_data, api_call, assert_ok, assert_error, crash)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.lib.ext_json import json from rhodecode.lib.ext_json import json
from rhodecode.lib.str_utils import safe_str from rhodecode.lib.str_utils import safe_str

View file

@ -26,7 +26,7 @@ from rhodecode.model.user import UserModel
from rhodecode.tests import TEST_USER_ADMIN_LOGIN from rhodecode.tests import TEST_USER_ADMIN_LOGIN
from rhodecode.api.tests.utils import ( from rhodecode.api.tests.utils import (
build_data, api_call, assert_ok, assert_error, crash) build_data, api_call, assert_ok, assert_error, crash)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
fixture = Fixture() fixture = Fixture()

View file

@ -26,7 +26,7 @@ from rhodecode.tests import (
TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL) TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL)
from rhodecode.api.tests.utils import ( from rhodecode.api.tests.utils import (
build_data, api_call, assert_ok, assert_error, jsonify, crash) build_data, api_call, assert_ok, assert_error, jsonify, crash)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.model.db import RepoGroup from rhodecode.model.db import RepoGroup

View file

@ -25,7 +25,7 @@ from rhodecode.model.user import UserModel
from rhodecode.model.user_group import UserGroupModel from rhodecode.model.user_group import UserGroupModel
from rhodecode.api.tests.utils import ( from rhodecode.api.tests.utils import (
build_data, api_call, assert_error, assert_ok, crash, jsonify) build_data, api_call, assert_error, assert_ok, crash, jsonify)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
@pytest.mark.usefixtures("testuser_api", "app") @pytest.mark.usefixtures("testuser_api", "app")

View file

@ -28,7 +28,7 @@ from rhodecode.model.user import UserModel
from rhodecode.tests import TEST_USER_ADMIN_LOGIN from rhodecode.tests import TEST_USER_ADMIN_LOGIN
from rhodecode.api.tests.utils import ( from rhodecode.api.tests.utils import (
build_data, api_call, assert_error, assert_ok, crash) build_data, api_call, assert_error, assert_ok, crash)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
fixture = Fixture() fixture = Fixture()

View file

@ -25,8 +25,8 @@ from rhodecode.model.scm import ScmModel
from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
from rhodecode.api.tests.utils import ( from rhodecode.api.tests.utils import (
build_data, api_call, assert_error, assert_ok, crash, jsonify) build_data, api_call, assert_error, assert_ok, crash, jsonify)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.fixture_mods.fixture_utils import plain_http_host_only_stub from rhodecode.tests.fixtures.fixture_utils import plain_http_host_only_stub
fixture = Fixture() fixture = Fixture()

View file

@ -26,7 +26,7 @@ import pytest
from rhodecode.lib.str_utils import safe_str from rhodecode.lib.str_utils import safe_str
from rhodecode.tests import * from rhodecode.tests import *
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
from rhodecode.tests.fixture import FIXTURES from rhodecode.tests.fixtures.rc_fixture import FIXTURES
from rhodecode.model.db import UserLog from rhodecode.model.db import UserLog
from rhodecode.model.meta import Session from rhodecode.model.meta import Session

View file

@ -20,7 +20,7 @@
import pytest import pytest
from rhodecode.tests import TestController from rhodecode.tests import TestController
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -37,7 +37,7 @@ from rhodecode.model.user import UserModel
from rhodecode.tests import ( from rhodecode.tests import (
login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN, login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
from rhodecode.tests.fixture import Fixture, error_function from rhodecode.tests.fixtures.rc_fixture import Fixture, error_function
from rhodecode.tests.utils import repo_on_filesystem from rhodecode.tests.utils import repo_on_filesystem
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path

View file

@ -27,7 +27,7 @@ from rhodecode.model.meta import Session
from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.repo_group import RepoGroupModel
from rhodecode.tests import ( from rhodecode.tests import (
assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH) assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path

View file

@ -24,7 +24,7 @@ from rhodecode.model.meta import Session
from rhodecode.tests import ( from rhodecode.tests import (
TestController, assert_session_flash) TestController, assert_session_flash)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -28,7 +28,7 @@ from rhodecode.model.user import UserModel
from rhodecode.tests import ( from rhodecode.tests import (
TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash) TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -22,7 +22,7 @@ import pytest
from rhodecode.model.db import User, UserSshKeys from rhodecode.model.db import User, UserSshKeys
from rhodecode.tests import TestController, assert_session_flash from rhodecode.tests import TestController, assert_session_flash
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -27,7 +27,7 @@ from rhodecode.model.repo_group import RepoGroupModel
from rhodecode.model.db import Session, Repository, RepoGroup from rhodecode.model.db import Session, Repository, RepoGroup
from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -22,7 +22,7 @@ from rhodecode.model.db import Repository
from rhodecode.lib.ext_json import json from rhodecode.lib.ext_json import json
from rhodecode.tests import TestController from rhodecode.tests import TestController
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -20,7 +20,7 @@ import pytest
from rhodecode.lib.ext_json import json from rhodecode.lib.ext_json import json
from rhodecode.tests import TestController from rhodecode.tests import TestController
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -40,7 +40,7 @@ import pytest
from rhodecode.lib.ext_json import json from rhodecode.lib.ext_json import json
from rhodecode.tests import TestController from rhodecode.tests import TestController
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -24,7 +24,7 @@ from rhodecode.model.db import Repository, RepoGroup, User
from rhodecode.model.meta import Session from rhodecode.model.meta import Session
from rhodecode.model.settings import SettingsModel from rhodecode.model.settings import SettingsModel
from rhodecode.tests import TestController from rhodecode.tests import TestController
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path

View file

@ -3,7 +3,7 @@ import mock
from rhodecode.lib.type_utils import AttributeDict from rhodecode.lib.type_utils import AttributeDict
from rhodecode.model.meta import Session from rhodecode.model.meta import Session
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
from rhodecode.model.settings import SettingsModel from rhodecode.model.settings import SettingsModel

View file

@ -31,7 +31,7 @@ from rhodecode.model.meta import Session
from rhodecode.tests import ( from rhodecode.tests import (
assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN, assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
no_newline_id_generator) no_newline_id_generator)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -22,7 +22,7 @@ from rhodecode.lib import helpers as h
from rhodecode.tests import ( from rhodecode.tests import (
TestController, clear_cache_regions, TestController, clear_cache_regions,
TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS) TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.utils import AssertResponse from rhodecode.tests.utils import AssertResponse
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path

View file

@ -22,7 +22,7 @@ from rhodecode.apps._base import ADMIN_PREFIX
from rhodecode.model.db import User from rhodecode.model.db import User
from rhodecode.tests import ( from rhodecode.tests import (
TestController, assert_session_flash) TestController, assert_session_flash)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path

View file

@ -23,7 +23,7 @@ from rhodecode.model.db import User, UserEmailMap
from rhodecode.tests import ( from rhodecode.tests import (
TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL, TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
assert_session_flash, TEST_USER_REGULAR_PASS) assert_session_flash, TEST_USER_REGULAR_PASS)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path

View file

@ -21,7 +21,7 @@ import pytest
from rhodecode.tests import ( from rhodecode.tests import (
TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS) TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
from rhodecode.model.db import Notification, User from rhodecode.model.db import Notification, User

View file

@ -24,7 +24,7 @@ from rhodecode.lib.auth import check_password
from rhodecode.model.meta import Session from rhodecode.model.meta import Session
from rhodecode.model.user import UserModel from rhodecode.model.user import UserModel
from rhodecode.tests import assert_session_flash, TestController from rhodecode.tests import assert_session_flash, TestController
from rhodecode.tests.fixture import Fixture, error_function from rhodecode.tests.fixtures.rc_fixture import Fixture, error_function
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -20,7 +20,7 @@
from rhodecode.tests import ( from rhodecode.tests import (
TestController, TEST_USER_ADMIN_LOGIN, TestController, TEST_USER_ADMIN_LOGIN,
TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -19,7 +19,7 @@
from rhodecode.model.db import User, Repository, UserFollowing from rhodecode.model.db import User, Repository, UserFollowing
from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -21,7 +21,7 @@
from rhodecode.model.db import User, UserSshKeys from rhodecode.model.db import User, UserSshKeys
from rhodecode.tests import TestController, assert_session_flash from rhodecode.tests import TestController, assert_session_flash
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -22,7 +22,7 @@ import pytest
from rhodecode.apps.repository.tests.test_repo_compare import ComparePage from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
from rhodecode.lib.vcs import nodes from rhodecode.lib.vcs import nodes
from rhodecode.lib.vcs.backends.base import EmptyCommit from rhodecode.lib.vcs.backends.base import EmptyCommit
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.utils import commit_change from rhodecode.tests.utils import commit_change
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
@ -166,14 +166,15 @@ class TestSideBySideDiff(object):
response.mustcontain('Collapse 2 commits') response.mustcontain('Collapse 2 commits')
response.mustcontain('123 file changed') response.mustcontain('123 file changed')
response.mustcontain( response.mustcontain(f'r{commit1.idx}:{commit1.short_id}...r{commit2.idx}:{commit2.short_id}')
'r%s:%s...r%s:%s' % (
commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
response.mustcontain(f_path) response.mustcontain(f_path)
@pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)') #@pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub): def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
if backend.alias == 'git':
pytest.skip('GIT does not handle empty commit compare correct (missing 1 commit)')
f_path = b'test_sidebyside_file.py' f_path = b'test_sidebyside_file.py'
commit1_content = b'content-25d7e49c18b159446c\n' commit1_content = b'content-25d7e49c18b159446c\n'
commit2_content = b'content-603d6c72c46d953420\n' commit2_content = b'content-603d6c72c46d953420\n'
@ -200,9 +201,7 @@ class TestSideBySideDiff(object):
response.mustcontain('Collapse 2 commits') response.mustcontain('Collapse 2 commits')
response.mustcontain('1 file changed') response.mustcontain('1 file changed')
response.mustcontain( response.mustcontain(f'r{commit1.idx}:{commit1.short_id}...r{commit2.idx}:{commit2.short_id}')
'r%s:%s...r%s:%s' % (
commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
response.mustcontain(f_path) response.mustcontain(f_path)

View file

@ -33,7 +33,7 @@ from rhodecode.lib.vcs.conf import settings
from rhodecode.model.db import Session, Repository from rhodecode.model.db import Session, Repository
from rhodecode.tests import assert_session_flash from rhodecode.tests import assert_session_flash
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path

View file

@ -21,7 +21,7 @@ import pytest
from rhodecode.tests import TestController, assert_session_flash, HG_FORK, GIT_FORK from rhodecode.tests import TestController, assert_session_flash, HG_FORK, GIT_FORK
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.lib import helpers as h from rhodecode.lib import helpers as h
from rhodecode.model.db import Repository from rhodecode.model.db import Repository

View file

@ -21,7 +21,7 @@ import pytest
from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -15,6 +15,9 @@
# This program is dual-licensed. If you wish to learn more about the # This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services, # RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/ # and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
import os
import mock import mock
import pytest import pytest
@ -41,7 +44,7 @@ from rhodecode.tests import (
TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_LOGIN,
TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_LOGIN,
) )
from rhodecode.tests.fixture_mods.fixture_utils import PRTestUtility from rhodecode.tests.fixtures.fixture_utils import PRTestUtility
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
@ -1050,7 +1053,6 @@ class TestPullrequestsView(object):
) )
assert len(notifications.all()) == 2 assert len(notifications.all()) == 2
@pytest.mark.xfail(reason="unable to fix this test after python3 migration")
def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token): def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
commits = [ commits = [
{ {
@ -1125,20 +1127,38 @@ class TestPullrequestsView(object):
response.mustcontain(no=["content_of_ancestor-child"]) response.mustcontain(no=["content_of_ancestor-child"])
response.mustcontain("content_of_change") response.mustcontain("content_of_change")
def test_merge_pull_request_enabled(self, pr_util, csrf_token): def test_merge_pull_request_enabled(self, pr_util, csrf_token, rcextensions_modification):
# Clear any previous calls to rcextensions
rhodecode.EXTENSIONS.calls.clear()
pull_request = pr_util.create_pull_request(approved=True, mergeable=True) pull_request = pr_util.create_pull_request(approved=True, mergeable=True)
pull_request_id = pull_request.pull_request_id pull_request_id = pull_request.pull_request_id
repo_name = (pull_request.target_repo.scm_instance().name,) repo_name = pull_request.target_repo.scm_instance().name
url = route_path( url = route_path(
"pullrequest_merge", "pullrequest_merge",
repo_name=str(repo_name[0]), repo_name=repo_name,
pull_request_id=pull_request_id, pull_request_id=pull_request_id,
) )
response = self.app.post(url, params={"csrf_token": csrf_token}).follow()
rcstack_location = os.path.dirname(self.app._pyramid_registry.settings['__file__'])
rc_ext_location = os.path.join(rcstack_location, 'rcextension-output.txt')
mods = [
('_push_hook',
f"""
import os
action = kwargs['action']
commit_ids = kwargs['commit_ids']
with open('{rc_ext_location}', 'w') as f:
f.write('test-execution'+os.linesep)
f.write(f'{{action}}'+os.linesep)
f.write(f'{{commit_ids}}'+os.linesep)
return HookResponse(0, 'HOOK_TEST')
""")
]
# Add the hook
with rcextensions_modification(rcstack_location, mods, create_if_missing=True, force_create=True):
response = self.app.post(url, params={"csrf_token": csrf_token}).follow()
pull_request = PullRequest.get(pull_request_id) pull_request = PullRequest.get(pull_request_id)
@ -1162,12 +1182,39 @@ class TestPullrequestsView(object):
assert actions[-1].action == "user.push" assert actions[-1].action == "user.push"
assert actions[-1].action_data["commit_ids"] == pr_commit_ids assert actions[-1].action_data["commit_ids"] == pr_commit_ids
# Check post_push rcextension was really executed with open(rc_ext_location) as f:
push_calls = rhodecode.EXTENSIONS.calls["_push_hook"] f_data = f.read()
assert len(push_calls) == 1 assert 'test-execution' in f_data
unused_last_call_args, last_call_kwargs = push_calls[0] for commit_id in pr_commit_ids:
assert last_call_kwargs["action"] == "push" assert f'{commit_id}' in f_data
assert last_call_kwargs["commit_ids"] == pr_commit_ids
def test_merge_pull_request_forbidden_by_pre_push_hook(self, pr_util, csrf_token, rcextensions_modification, caplog):
caplog.set_level(logging.WARNING, logger="rhodecode.model.pull_request")
pull_request = pr_util.create_pull_request(approved=True, mergeable=True)
pull_request_id = pull_request.pull_request_id
repo_name = pull_request.target_repo.scm_instance().name
url = route_path(
"pullrequest_merge",
repo_name=repo_name,
pull_request_id=pull_request_id,
)
rcstack_location = os.path.dirname(self.app._pyramid_registry.settings['__file__'])
mods = [
('_pre_push_hook',
f"""
return HookResponse(1, 'HOOK_TEST_FORBIDDEN')
""")
]
# Add the hook
with rcextensions_modification(rcstack_location, mods, create_if_missing=True, force_create=True):
self.app.post(url, params={"csrf_token": csrf_token})
assert 'Merge failed, not updating the pull request.' in [r[2] for r in caplog.record_tuples]
def test_merge_pull_request_disabled(self, pr_util, csrf_token): def test_merge_pull_request_disabled(self, pr_util, csrf_token):
pull_request = pr_util.create_pull_request(mergeable=False) pull_request = pr_util.create_pull_request(mergeable=False)
@ -1523,7 +1570,6 @@ class TestPullrequestsView(object):
assert pull_request.revisions == [commit_ids["change-rebased"]] assert pull_request.revisions == [commit_ids["change-rebased"]]
def test_remove_pull_request_branch(self, backend_git, csrf_token): def test_remove_pull_request_branch(self, backend_git, csrf_token):
branch_name = "development" branch_name = "development"
commits = [ commits = [

View file

@ -26,7 +26,7 @@ from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
from rhodecode.model.meta import Session from rhodecode.model.meta import Session
from rhodecode.tests import ( from rhodecode.tests import (
TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash) TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -24,7 +24,7 @@ from rhodecode.model.db import Repository
from rhodecode.model.repo import RepoModel from rhodecode.model.repo import RepoModel
from rhodecode.tests import ( from rhodecode.tests import (
HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator) HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.utils import repo_on_filesystem from rhodecode.tests.utils import repo_on_filesystem
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path

View file

@ -31,7 +31,7 @@ from rhodecode.model.meta import Session
from rhodecode.model.repo import RepoModel from rhodecode.model.repo import RepoModel
from rhodecode.model.scm import ScmModel from rhodecode.model.scm import ScmModel
from rhodecode.tests import assert_session_flash from rhodecode.tests import assert_session_flash
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.utils import AssertResponse, repo_on_filesystem from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path

View file

@ -30,7 +30,7 @@ from rhodecode.model.user import UserModel
from rhodecode.tests import ( from rhodecode.tests import (
login_user_session, logout_user_session, login_user_session, logout_user_session,
TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.utils import AssertResponse from rhodecode.tests.utils import AssertResponse
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path

View file

@ -32,16 +32,13 @@ class TestAdminRepoVcsSettings(object):
@pytest.mark.parametrize('setting_name, setting_backends', [ @pytest.mark.parametrize('setting_name, setting_backends', [
('hg_use_rebase_for_merging', ['hg']), ('hg_use_rebase_for_merging', ['hg']),
]) ])
def test_labs_settings_visible_if_enabled( def test_labs_settings_visible_if_enabled(self, setting_name, setting_backends, backend):
self, setting_name, setting_backends, backend):
if backend.alias not in setting_backends: if backend.alias not in setting_backends:
pytest.skip('Setting not available for backend {}'.format(backend)) pytest.skip('Setting not available for backend {}'.format(backend))
vcs_settings_url = route_path( vcs_settings_url = route_path('edit_repo_vcs', repo_name=backend.repo.repo_name)
'edit_repo_vcs', repo_name=backend.repo.repo_name)
with mock.patch.dict( with mock.patch.dict(rhodecode.CONFIG, {'labs_settings_active': 'true'}):
rhodecode.CONFIG, {'labs_settings_active': 'true'}):
response = self.app.get(vcs_settings_url) response = self.app.get(vcs_settings_url)
assertr = response.assert_response() assertr = response.assert_response()

View file

@ -20,7 +20,7 @@ import os
import sys import sys
import logging import logging
from rhodecode.lib.hook_daemon.base import prepare_callback_daemon from rhodecode.lib.hook_daemon.utils import prepare_callback_daemon
from rhodecode.lib.ext_json import sjson as json from rhodecode.lib.ext_json import sjson as json
from rhodecode.lib.vcs.conf import settings as vcs_settings from rhodecode.lib.vcs.conf import settings as vcs_settings
from rhodecode.lib.api_utils import call_service_api from rhodecode.lib.api_utils import call_service_api
@ -162,9 +162,7 @@ class SshVcsServer(object):
extras = {} extras = {}
extras.update(tunnel_extras) extras.update(tunnel_extras)
callback_daemon, extras = prepare_callback_daemon( callback_daemon, extras = prepare_callback_daemon(extras, protocol=self.hooks_protocol)
extras, protocol=self.hooks_protocol,
host=vcs_settings.HOOKS_HOST)
with callback_daemon: with callback_daemon:
try: try:

View file

@ -33,19 +33,24 @@ class GitServerCreator(object):
'app:main': { 'app:main': {
'ssh.executable.git': git_path, 'ssh.executable.git': git_path,
'vcs.hooks.protocol.v2': 'celery', 'vcs.hooks.protocol.v2': 'celery',
'app.service_api.host': 'http://localhost',
'app.service_api.token': 'secret4',
'rhodecode.api.url': '/_admin/api',
} }
} }
repo_name = 'test_git' repo_name = 'test_git'
repo_mode = 'receive-pack' repo_mode = 'receive-pack'
user = plain_dummy_user() user = plain_dummy_user()
def __init__(self): def __init__(self, service_api_url, ini_file):
pass self.service_api_url = service_api_url
self.ini_file = ini_file
def create(self, **kwargs): def create(self, **kwargs):
self.config_data['app:main']['app.service_api.host'] = self.service_api_url
parameters = { parameters = {
'store': self.root, 'store': self.root,
'ini_path': '', 'ini_path': self.ini_file,
'user': self.user, 'user': self.user,
'repo_name': self.repo_name, 'repo_name': self.repo_name,
'repo_mode': self.repo_mode, 'repo_mode': self.repo_mode,
@ -60,12 +65,30 @@ class GitServerCreator(object):
return server return server
@pytest.fixture() @pytest.fixture(scope='module')
def git_server(app): def git_server(request, module_app, rhodecode_factory, available_port_factory):
return GitServerCreator() ini_file = module_app._pyramid_settings['__file__']
vcsserver_host = module_app._pyramid_settings['vcs.server']
store_dir = os.path.dirname(ini_file)
# start rhodecode for service API
rc = rhodecode_factory(
request,
store_dir=store_dir,
port=available_port_factory(),
overrides=(
{'handler_console': {'level': 'DEBUG'}},
{'app:main': {'vcs.server': vcsserver_host}},
{'app:main': {'repo_store.path': store_dir}}
))
service_api_url = f'http://{rc.bind_addr}'
return GitServerCreator(service_api_url, ini_file)
class TestGitServer(object): class TestGitServer:
def test_command(self, git_server): def test_command(self, git_server):
server = git_server.create() server = git_server.create()
@ -102,14 +125,14 @@ class TestGitServer(object):
assert result is value assert result is value
def test_run_returns_executes_command(self, git_server): def test_run_returns_executes_command(self, git_server):
server = git_server.create()
from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper
server = git_server.create()
os.environ['SSH_CLIENT'] = '127.0.0.1' os.environ['SSH_CLIENT'] = '127.0.0.1'
with mock.patch.object(GitTunnelWrapper, 'create_hooks_env') as _patch: with mock.patch.object(GitTunnelWrapper, 'create_hooks_env') as _patch:
_patch.return_value = 0 _patch.return_value = 0
with mock.patch.object(GitTunnelWrapper, 'command', return_value='date'): with mock.patch.object(GitTunnelWrapper, 'command', return_value='date'):
exit_code = server.run() exit_code = server.run(tunnel_extras={'config': server.ini_path})
assert exit_code == (0, False) assert exit_code == (0, False)
@ -135,7 +158,7 @@ class TestGitServer(object):
'action': action, 'action': action,
'ip': '10.10.10.10', 'ip': '10.10.10.10',
'locked_by': [None, None], 'locked_by': [None, None],
'config': '', 'config': git_server.ini_file,
'repo_store': store, 'repo_store': store,
'server_url': None, 'server_url': None,
'hooks': ['push', 'pull'], 'hooks': ['push', 'pull'],

View file

@ -17,6 +17,7 @@
# and proprietary license terms, please see https://rhodecode.com/licenses/ # and proprietary license terms, please see https://rhodecode.com/licenses/
import os import os
import mock import mock
import pytest import pytest
@ -32,22 +33,27 @@ class MercurialServerCreator(object):
'app:main': { 'app:main': {
'ssh.executable.hg': hg_path, 'ssh.executable.hg': hg_path,
'vcs.hooks.protocol.v2': 'celery', 'vcs.hooks.protocol.v2': 'celery',
'app.service_api.host': 'http://localhost',
'app.service_api.token': 'secret4',
'rhodecode.api.url': '/_admin/api',
} }
} }
repo_name = 'test_hg' repo_name = 'test_hg'
user = plain_dummy_user() user = plain_dummy_user()
def __init__(self): def __init__(self, service_api_url, ini_file):
pass self.service_api_url = service_api_url
self.ini_file = ini_file
def create(self, **kwargs): def create(self, **kwargs):
self.config_data['app:main']['app.service_api.host'] = self.service_api_url
parameters = { parameters = {
'store': self.root, 'store': self.root,
'ini_path': '', 'ini_path': self.ini_file,
'user': self.user, 'user': self.user,
'repo_name': self.repo_name, 'repo_name': self.repo_name,
'user_permissions': { 'user_permissions': {
'test_hg': 'repository.admin' self.repo_name: 'repository.admin'
}, },
'settings': self.config_data['app:main'], 'settings': self.config_data['app:main'],
'env': plain_dummy_env() 'env': plain_dummy_env()
@ -57,12 +63,30 @@ class MercurialServerCreator(object):
return server return server
@pytest.fixture() @pytest.fixture(scope='module')
def hg_server(app): def hg_server(request, module_app, rhodecode_factory, available_port_factory):
return MercurialServerCreator() ini_file = module_app._pyramid_settings['__file__']
vcsserver_host = module_app._pyramid_settings['vcs.server']
store_dir = os.path.dirname(ini_file)
# start rhodecode for service API
rc = rhodecode_factory(
request,
store_dir=store_dir,
port=available_port_factory(),
overrides=(
{'handler_console': {'level': 'DEBUG'}},
{'app:main': {'vcs.server': vcsserver_host}},
{'app:main': {'repo_store.path': store_dir}}
))
service_api_url = f'http://{rc.bind_addr}'
return MercurialServerCreator(service_api_url, ini_file)
class TestMercurialServer(object): class TestMercurialServer:
def test_command(self, hg_server, tmpdir): def test_command(self, hg_server, tmpdir):
server = hg_server.create() server = hg_server.create()
@ -107,7 +131,7 @@ class TestMercurialServer(object):
with mock.patch.object(MercurialTunnelWrapper, 'create_hooks_env') as _patch: with mock.patch.object(MercurialTunnelWrapper, 'create_hooks_env') as _patch:
_patch.return_value = 0 _patch.return_value = 0
with mock.patch.object(MercurialTunnelWrapper, 'command', return_value='date'): with mock.patch.object(MercurialTunnelWrapper, 'command', return_value='date'):
exit_code = server.run() exit_code = server.run(tunnel_extras={'config': server.ini_path})
assert exit_code == (0, False) assert exit_code == (0, False)

View file

@ -15,7 +15,9 @@
# This program is dual-licensed. If you wish to learn more about the # This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services, # RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/ # and proprietary license terms, please see https://rhodecode.com/licenses/
import os import os
import mock import mock
import pytest import pytest
@ -26,39 +28,62 @@ from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dum
class SubversionServerCreator(object): class SubversionServerCreator(object):
root = '/tmp/repo/path/' root = '/tmp/repo/path/'
svn_path = '/usr/local/bin/svnserve' svn_path = '/usr/local/bin/svnserve'
config_data = { config_data = {
'app:main': { 'app:main': {
'ssh.executable.svn': svn_path, 'ssh.executable.svn': svn_path,
'vcs.hooks.protocol.v2': 'celery', 'vcs.hooks.protocol.v2': 'celery',
'app.service_api.host': 'http://localhost',
'app.service_api.token': 'secret4',
'rhodecode.api.url': '/_admin/api',
} }
} }
repo_name = 'test-svn' repo_name = 'test-svn'
user = plain_dummy_user() user = plain_dummy_user()
def __init__(self): def __init__(self, service_api_url, ini_file):
pass self.service_api_url = service_api_url
self.ini_file = ini_file
def create(self, **kwargs): def create(self, **kwargs):
self.config_data['app:main']['app.service_api.host'] = self.service_api_url
parameters = { parameters = {
'store': self.root, 'store': self.root,
'repo_name': self.repo_name, 'ini_path': self.ini_file,
'ini_path': '',
'user': self.user, 'user': self.user,
'repo_name': self.repo_name,
'user_permissions': { 'user_permissions': {
self.repo_name: 'repository.admin' self.repo_name: 'repository.admin'
}, },
'settings': self.config_data['app:main'], 'settings': self.config_data['app:main'],
'env': plain_dummy_env() 'env': plain_dummy_env()
} }
parameters.update(kwargs) parameters.update(kwargs)
server = SubversionServer(**parameters) server = SubversionServer(**parameters)
return server return server
@pytest.fixture() @pytest.fixture(scope='module')
def svn_server(app): def svn_server(request, module_app, rhodecode_factory, available_port_factory):
return SubversionServerCreator() ini_file = module_app._pyramid_settings['__file__']
vcsserver_host = module_app._pyramid_settings['vcs.server']
store_dir = os.path.dirname(ini_file)
# start rhodecode for service API
rc = rhodecode_factory(
request,
store_dir=store_dir,
port=available_port_factory(),
overrides=(
{'handler_console': {'level': 'DEBUG'}},
{'app:main': {'vcs.server': vcsserver_host}},
{'app:main': {'repo_store.path': store_dir}}
))
service_api_url = f'http://{rc.bind_addr}'
return SubversionServerCreator(service_api_url, ini_file)
class TestSubversionServer(object): class TestSubversionServer(object):
@ -168,8 +193,9 @@ class TestSubversionServer(object):
assert repo_name == expected_match assert repo_name == expected_match
def test_run_returns_executes_command(self, svn_server): def test_run_returns_executes_command(self, svn_server):
server = svn_server.create()
from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
server = svn_server.create()
os.environ['SSH_CLIENT'] = '127.0.0.1' os.environ['SSH_CLIENT'] = '127.0.0.1'
with mock.patch.object( with mock.patch.object(
SubversionTunnelWrapper, 'get_first_client_response', SubversionTunnelWrapper, 'get_first_client_response',
@ -184,20 +210,18 @@ class TestSubversionServer(object):
SubversionTunnelWrapper, 'command', SubversionTunnelWrapper, 'command',
return_value=['date']): return_value=['date']):
exit_code = server.run() exit_code = server.run(tunnel_extras={'config': server.ini_path})
# SVN has this differently configured, and we get in our mock env # SVN has this differently configured, and we get in our mock env
# None as return code # None as return code
assert exit_code == (None, False) assert exit_code == (None, False)
def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server): def test_run_returns_executes_command_that_cannot_extract_repo_name(self, svn_server):
server = svn_server.create()
from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper
with mock.patch.object(
SubversionTunnelWrapper, 'command', server = svn_server.create()
return_value=['date']): with mock.patch.object(SubversionTunnelWrapper, 'command', return_value=['date']):
with mock.patch.object( with mock.patch.object(SubversionTunnelWrapper, 'get_first_client_response',
SubversionTunnelWrapper, 'get_first_client_response',
return_value=None): return_value=None):
exit_code = server.run() exit_code = server.run(tunnel_extras={'config': server.ini_path})
assert exit_code == (1, False) assert exit_code == (1, False)

View file

@ -22,7 +22,7 @@ from rhodecode.tests import (
TestController, assert_session_flash, TEST_USER_ADMIN_LOGIN) TestController, assert_session_flash, TEST_USER_ADMIN_LOGIN)
from rhodecode.model.db import UserGroup from rhodecode.model.db import UserGroup
from rhodecode.model.meta import Session from rhodecode.model.meta import Session
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -18,7 +18,7 @@
from rhodecode.model.user_group import UserGroupModel from rhodecode.model.user_group import UserGroupModel
from rhodecode.tests import ( from rhodecode.tests import (
TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path
fixture = Fixture() fixture = Fixture()

View file

@ -22,7 +22,7 @@ from rhodecode.model.db import User
from rhodecode.tests import ( from rhodecode.tests import (
TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS, TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
from rhodecode.tests.fixture import Fixture from rhodecode.tests.fixtures.rc_fixture import Fixture
from rhodecode.tests.utils import AssertResponse from rhodecode.tests.utils import AssertResponse
from rhodecode.tests.routes import route_path from rhodecode.tests.routes import route_path

View file

@ -30,7 +30,7 @@ from rhodecode.lib.vcs import connect_vcs
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def propagate_rhodecode_config(global_config, settings, config): def propagate_rhodecode_config(global_config, settings, config, full=True):
# Store the settings to make them available to other modules. # Store the settings to make them available to other modules.
settings_merged = global_config.copy() settings_merged = global_config.copy()
settings_merged.update(settings) settings_merged.update(settings)
@ -40,7 +40,7 @@ def propagate_rhodecode_config(global_config, settings, config):
rhodecode.PYRAMID_SETTINGS = settings_merged rhodecode.PYRAMID_SETTINGS = settings_merged
rhodecode.CONFIG = settings_merged rhodecode.CONFIG = settings_merged
if 'default_user_id' not in rhodecode.CONFIG: if full and 'default_user_id' not in rhodecode.CONFIG:
rhodecode.CONFIG['default_user_id'] = utils.get_default_user_id() rhodecode.CONFIG['default_user_id'] = utils.get_default_user_id()
log.debug('set rhodecode.CONFIG data') log.debug('set rhodecode.CONFIG data')
@ -93,6 +93,7 @@ def load_pyramid_environment(global_config, settings):
# first run, to store data... # first run, to store data...
propagate_rhodecode_config(global_config, settings, {}) propagate_rhodecode_config(global_config, settings, {})
if vcs_server_enabled: if vcs_server_enabled:
connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings)) connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings))
else: else:

View file

@ -101,6 +101,9 @@ def make_pyramid_app(global_config, **settings):
patches.inspect_getargspec() patches.inspect_getargspec()
patches.repoze_sendmail_lf_fix() patches.repoze_sendmail_lf_fix()
# first init, so load_pyramid_enviroment, can access some critical data, like __file__
propagate_rhodecode_config(global_config, {}, {}, full=False)
load_pyramid_environment(global_config, settings) load_pyramid_environment(global_config, settings)
# Static file view comes first # Static file view comes first

View file

@ -17,7 +17,7 @@
# and proprietary license terms, please see https://rhodecode.com/licenses/ # and proprietary license terms, please see https://rhodecode.com/licenses/
""" """
rcextensions module, please edit `hooks.py` to over write hooks logic rcextensions module, please edit `hooks.py` to over-write hooks logic
""" """
from .hooks import ( from .hooks import (

View file

@ -85,7 +85,7 @@ def _pre_push_hook(*args, **kwargs):
# check files names # check files names
if forbidden_files: if forbidden_files:
reason = 'File {} is forbidden to be pushed'.format(file_name) reason = f'File {file_name} is forbidden to be pushed'
for forbidden_pattern in forbid_files: for forbidden_pattern in forbid_files:
# here we can also filter for operation, e.g if check for only ADDED files # here we can also filter for operation, e.g if check for only ADDED files
# if operation == 'A': # if operation == 'A':

View file

@ -1,4 +1,3 @@
# Copyright (C) 2016-2023 RhodeCode GmbH # Copyright (C) 2016-2023 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@ -55,7 +54,7 @@ def run(*args, **kwargs):
return fields return fields
class _Undefined(object): class _Undefined:
pass pass
@ -67,7 +66,7 @@ def get_field(extra_fields_data, key, default=_Undefined(), convert_type=True):
if key not in extra_fields_data: if key not in extra_fields_data:
if isinstance(default, _Undefined): if isinstance(default, _Undefined):
raise ValueError('key {} not present in extra_fields'.format(key)) raise ValueError(f'key {key} not present in extra_fields')
return default return default
# NOTE(dan): from metadata we get field_label, field_value, field_desc, field_type # NOTE(dan): from metadata we get field_label, field_value, field_desc, field_type

View file

@ -1,4 +1,3 @@
# Copyright (C) 2016-2023 RhodeCode GmbH # Copyright (C) 2016-2023 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify

View file

@ -1,4 +1,3 @@
# Copyright (C) 2016-2023 RhodeCode GmbH # Copyright (C) 2016-2023 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@ -52,7 +51,7 @@ def get_git_commits(repo, refs):
cmd = [ cmd = [
'log', 'log',
'--pretty=format:{"commit_id": "%H", "author": "%aN <%aE>", "date": "%ad", "message": "%s"}', '--pretty=format:{"commit_id": "%H", "author": "%aN <%aE>", "date": "%ad", "message": "%s"}',
'{}...{}'.format(old_rev, new_rev) f'{old_rev}...{new_rev}'
] ]
stdout, stderr = repo.run_git_command(cmd, extra_env=git_env) stdout, stderr = repo.run_git_command(cmd, extra_env=git_env)
@ -80,12 +79,12 @@ def run(*args, **kwargs):
if vcs_type == 'git': if vcs_type == 'git':
for rev_data in kwargs['commit_ids']: for rev_data in kwargs['commit_ids']:
new_environ = dict((k, v) for k, v in rev_data['git_env']) new_environ = {k: v for k, v in rev_data['git_env']}
commits = get_git_commits(vcs_repo, kwargs['commit_ids']) commits = get_git_commits(vcs_repo, kwargs['commit_ids'])
if vcs_type == 'hg': if vcs_type == 'hg':
for rev_data in kwargs['commit_ids']: for rev_data in kwargs['commit_ids']:
new_environ = dict((k, v) for k, v in rev_data['hg_env']) new_environ = {k: v for k, v in rev_data['hg_env']}
commits = get_hg_commits(vcs_repo, kwargs['commit_ids']) commits = get_hg_commits(vcs_repo, kwargs['commit_ids'])
return commits return commits

View file

@ -1,4 +1,3 @@
# Copyright (C) 2016-2023 RhodeCode GmbH # Copyright (C) 2016-2023 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@ -133,12 +132,12 @@ def run(*args, **kwargs):
if vcs_type == 'git': if vcs_type == 'git':
for rev_data in kwargs['commit_ids']: for rev_data in kwargs['commit_ids']:
new_environ = dict((k, v) for k, v in rev_data['git_env']) new_environ = {k: v for k, v in rev_data['git_env']}
files = get_git_files(repo, vcs_repo, kwargs['commit_ids']) files = get_git_files(repo, vcs_repo, kwargs['commit_ids'])
if vcs_type == 'hg': if vcs_type == 'hg':
for rev_data in kwargs['commit_ids']: for rev_data in kwargs['commit_ids']:
new_environ = dict((k, v) for k, v in rev_data['hg_env']) new_environ = {k: v for k, v in rev_data['hg_env']}
files = get_hg_files(repo, vcs_repo, kwargs['commit_ids']) files = get_hg_files(repo, vcs_repo, kwargs['commit_ids'])
if vcs_type == 'svn': if vcs_type == 'svn':

View file

@ -1,4 +1,3 @@
# Copyright (C) 2016-2023 RhodeCode GmbH # Copyright (C) 2016-2023 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify

View file

@ -28,7 +28,7 @@ import urllib.error
log = logging.getLogger('rhodecode.' + __name__) log = logging.getLogger('rhodecode.' + __name__)
class HookResponse(object): class HookResponse:
def __init__(self, status, output): def __init__(self, status, output):
self.status = status self.status = status
self.output = output self.output = output
@ -44,6 +44,11 @@ class HookResponse(object):
def __bool__(self): def __bool__(self):
return self.status == 0 return self.status == 0
def to_json(self):
return {'status': self.status, 'output': self.output}
def __repr__(self):
return self.to_json().__repr__()
class DotDict(dict): class DotDict(dict):
@ -91,8 +96,8 @@ class DotDict(dict):
def __repr__(self): def __repr__(self):
keys = list(self.keys()) keys = list(self.keys())
keys.sort() keys.sort()
args = ', '.join(['%s=%r' % (key, self[key]) for key in keys]) args = ', '.join(['{}={!r}'.format(key, self[key]) for key in keys])
return '%s(%s)' % (self.__class__.__name__, args) return '{}({})'.format(self.__class__.__name__, args)
@staticmethod @staticmethod
def fromDict(d): def fromDict(d):
@ -110,7 +115,7 @@ def serialize(x):
def unserialize(x): def unserialize(x):
if isinstance(x, dict): if isinstance(x, dict):
return dict((k, unserialize(v)) for k, v in x.items()) return {k: unserialize(v) for k, v in x.items()}
elif isinstance(x, (list, tuple)): elif isinstance(x, (list, tuple)):
return type(x)(unserialize(v) for v in x) return type(x)(unserialize(v) for v in x)
else: else:
@ -161,7 +166,8 @@ def str2bool(_str) -> bool:
string into boolean string into boolean
:param _str: string value to translate into boolean :param _str: string value to translate into boolean
:returns: bool from given string :rtype: boolean
:returns: boolean from given string
""" """
if _str is None: if _str is None:
return False return False

View file

@ -49,22 +49,22 @@ link_config = [
{ {
"name": "enterprise_docs", "name": "enterprise_docs",
"target": "https://rhodecode.com/r1/enterprise/docs/", "target": "https://rhodecode.com/r1/enterprise/docs/",
"external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/", "external_target": "https://docs.rhodecode.com/4.x/rce/index.html",
}, },
{ {
"name": "enterprise_log_file_locations", "name": "enterprise_log_file_locations",
"target": "https://rhodecode.com/r1/enterprise/docs/admin-system-overview/", "target": "https://rhodecode.com/r1/enterprise/docs/admin-system-overview/",
"external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/system-overview.html#log-files", "external_target": "https://docs.rhodecode.com/4.x/rce/admin/system-overview.html#log-files",
}, },
{ {
"name": "enterprise_issue_tracker_settings", "name": "enterprise_issue_tracker_settings",
"target": "https://rhodecode.com/r1/enterprise/docs/issue-trackers-overview/", "target": "https://rhodecode.com/r1/enterprise/docs/issue-trackers-overview/",
"external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/issue-trackers/issue-trackers.html", "external_target": "https://docs.rhodecode.com/4.x/rce/issue-trackers/issue-trackers.html",
}, },
{ {
"name": "enterprise_svn_setup", "name": "enterprise_svn_setup",
"target": "https://rhodecode.com/r1/enterprise/docs/svn-setup/", "target": "https://rhodecode.com/r1/enterprise/docs/svn-setup/",
"external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/svn-http.html", "external_target": "https://docs.rhodecode.com/4.x/rce/admin/svn-http.html",
}, },
{ {
"name": "enterprise_license_convert_from_old", "name": "enterprise_license_convert_from_old",

View file

@ -19,6 +19,8 @@
import os import os
import platform import platform
from rhodecode.lib.type_utils import str2bool
DEFAULT_USER = 'default' DEFAULT_USER = 'default'
@ -48,28 +50,23 @@ def initialize_database(config):
engine = engine_from_config(config, 'sqlalchemy.db1.') engine = engine_from_config(config, 'sqlalchemy.db1.')
init_model(engine, encryption_key=get_encryption_key(config)) init_model(engine, encryption_key=get_encryption_key(config))
def initialize_test_environment(settings):
skip_test_env = str2bool(os.environ.get('RC_NO_TEST_ENV'))
if skip_test_env:
return
def initialize_test_environment(settings, test_env=None): repo_store_path = os.environ.get('RC_TEST_ENV_REPO_STORE') or settings['repo_store.path']
if test_env is None:
test_env = not int(os.environ.get('RC_NO_TMP_PATH', 0))
from rhodecode.lib.utils import ( from rhodecode.lib.utils import (
create_test_directory, create_test_database, create_test_repositories, create_test_directory, create_test_database, create_test_repositories,
create_test_index) create_test_index)
from rhodecode.tests import TESTS_TMP_PATH
from rhodecode.lib.vcs.backends.hg import largefiles_store
from rhodecode.lib.vcs.backends.git import lfs_store
create_test_directory(repo_store_path)
create_test_database(repo_store_path, settings)
# test repos # test repos
if test_env: create_test_repositories(repo_store_path, settings)
create_test_directory(TESTS_TMP_PATH) create_test_index(repo_store_path, settings)
# large object stores
create_test_directory(largefiles_store(TESTS_TMP_PATH))
create_test_directory(lfs_store(TESTS_TMP_PATH))
create_test_database(TESTS_TMP_PATH, settings)
create_test_repositories(TESTS_TMP_PATH, settings)
create_test_index(TESTS_TMP_PATH, settings)
def get_vcs_server_protocol(config): def get_vcs_server_protocol(config):

View file

@ -20,8 +20,7 @@
Set of custom exceptions used in RhodeCode Set of custom exceptions used in RhodeCode
""" """
from webob.exc import HTTPClientError from pyramid.httpexceptions import HTTPBadGateway, HTTPClientError
from pyramid.httpexceptions import HTTPBadGateway
class LdapUsernameError(Exception): class LdapUsernameError(Exception):
@ -102,12 +101,7 @@ class HTTPRequirementError(HTTPClientError):
self.args = (message, ) self.args = (message, )
class ClientNotSupportedError(HTTPRequirementError): class HTTPLockedRepo(HTTPClientError):
title = explanation = 'Client Not Supported'
reason = None
class HTTPLockedRC(HTTPClientError):
""" """
Special Exception For locked Repos in RhodeCode, the return code can Special Exception For locked Repos in RhodeCode, the return code can
be overwritten by _code keyword argument passed into constructors be overwritten by _code keyword argument passed into constructors
@ -131,14 +125,13 @@ class HTTPBranchProtected(HTTPClientError):
Special Exception For Indicating that branch is protected in RhodeCode, the Special Exception For Indicating that branch is protected in RhodeCode, the
return code can be overwritten by _code keyword argument passed into constructors return code can be overwritten by _code keyword argument passed into constructors
""" """
code = 403
title = explanation = 'Branch Protected' title = explanation = 'Branch Protected'
reason = None reason = None
def __init__(self, message, *args, **kwargs):
self.title = self.explanation = message class ClientNotSupported(HTTPRequirementError):
super().__init__(*args, **kwargs) title = explanation = 'Client Not Supported'
self.args = (message, ) reason = None
class IMCCommitError(Exception): class IMCCommitError(Exception):

View file

@ -1,4 +1,4 @@
# Copyright (C) 2010-2023 RhodeCode GmbH # Copyright (C) 2010-2024 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3 # it under the terms of the GNU Affero General Public License, version 3
@ -16,13 +16,14 @@
# RhodeCode Enterprise Edition, including its added features, Support services, # RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/ # and proprietary license terms, please see https://rhodecode.com/licenses/
import os
import time
import logging import logging
import traceback
from rhodecode.lib.config_utils import get_app_config_lightweight from rhodecode.model import meta
from rhodecode.lib import hooks_base
from rhodecode.lib.utils2 import AttributeDict
from rhodecode.lib.exceptions import HTTPLockedRepo, HTTPBranchProtected
from rhodecode.lib.svn_txn_utils import get_txn_id_from_store
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -42,53 +43,82 @@ class BaseHooksCallbackDaemon:
log.debug('Exiting `%s` callback daemon', self.__class__.__name__) log.debug('Exiting `%s` callback daemon', self.__class__.__name__)
class HooksModuleCallbackDaemon(BaseHooksCallbackDaemon): class Hooks(object):
"""
Exposes the hooks module for calling them using the local HooksModuleCallbackDaemon
"""
def __init__(self, request=None, log_prefix=''):
self.log_prefix = log_prefix
self.request = request
def __init__(self, module): def repo_size(self, extras):
super().__init__() log.debug("%sCalled repo_size of %s object", self.log_prefix, self)
self.hooks_module = module return self._call_hook(hooks_base.repo_size, extras)
def __repr__(self): def pre_pull(self, extras):
return f'HooksModuleCallbackDaemon(hooks_module={self.hooks_module})' log.debug("%sCalled pre_pull of %s object", self.log_prefix, self)
return self._call_hook(hooks_base.pre_pull, extras)
def post_pull(self, extras):
log.debug("%sCalled post_pull of %s object", self.log_prefix, self)
return self._call_hook(hooks_base.post_pull, extras)
def prepare_callback_daemon(extras, protocol, host, txn_id=None): def pre_push(self, extras):
log.debug("%sCalled pre_push of %s object", self.log_prefix, self)
return self._call_hook(hooks_base.pre_push, extras)
match protocol: def post_push(self, extras):
case 'http': log.debug("%sCalled post_push of %s object", self.log_prefix, self)
from rhodecode.lib.hook_daemon.http_hooks_deamon import HttpHooksCallbackDaemon return self._call_hook(hooks_base.post_push, extras)
port = 0
if txn_id:
# read txn-id to re-use the PORT for callback daemon
repo_path = os.path.join(extras['repo_store'], extras['repository'])
txn_details = get_txn_id_from_store(repo_path, txn_id)
port = txn_details.get('port', 0)
callback_daemon = HttpHooksCallbackDaemon( def _call_hook(self, hook, extras):
txn_id=txn_id, host=host, port=port) extras = AttributeDict(extras)
case 'celery': _server_url = extras['server_url']
from rhodecode.lib.hook_daemon.celery_hooks_deamon import CeleryHooksCallbackDaemon
config = get_app_config_lightweight(extras['config']) extras.request = self.request
task_queue = config.get('celery.broker_url') try:
task_backend = config.get('celery.result_backend') result = hook(extras)
if result is None:
raise Exception(f'Failed to obtain hook result from func: {hook}')
except HTTPBranchProtected as error:
# Those special cases don't need error reporting. It's a case of
# locked repo or protected branch
result = AttributeDict({
'status': error.code,
'output': error.explanation
})
except HTTPLockedRepo as error:
# Those special cases don't need error reporting. It's a case of
# locked repo or protected branch
result = AttributeDict({
'status': error.code,
'output': error.explanation
})
except Exception as error:
# locked needs different handling since we need to also
# handle PULL operations
log.exception('%sException when handling hook %s', self.log_prefix, hook)
exc_tb = traceback.format_exc()
error_args = error.args
return {
'status': 128,
'output': '',
'exception': type(error).__name__,
'exception_traceback': exc_tb,
'exception_args': error_args,
}
finally:
meta.Session.remove()
callback_daemon = CeleryHooksCallbackDaemon(task_queue, task_backend) log.debug('%sGot hook call response %s', self.log_prefix, result)
case 'local': return {
from rhodecode.lib.hook_daemon.hook_module import Hooks 'status': result.status,
callback_daemon = HooksModuleCallbackDaemon(Hooks.__module__) 'output': result.output,
case _: }
log.error('Unsupported callback daemon protocol "%s"', protocol)
raise Exception('Unsupported callback daemon protocol.')
extras['hooks_uri'] = getattr(callback_daemon, 'hooks_uri', '') def __enter__(self):
extras['task_queue'] = getattr(callback_daemon, 'task_queue', '') return self
extras['task_backend'] = getattr(callback_daemon, 'task_backend', '')
extras['hooks_protocol'] = protocol def __exit__(self, exc_type, exc_val, exc_tb):
extras['time'] = time.time() pass
# register txn_id
extras['txn_id'] = txn_id
log.debug('Prepared a callback daemon: %s',
callback_daemon.__class__.__name__)
return callback_daemon, extras

View file

@ -22,14 +22,16 @@ from rhodecode.lib.hook_daemon.base import BaseHooksCallbackDaemon
class CeleryHooksCallbackDaemon(BaseHooksCallbackDaemon): class CeleryHooksCallbackDaemon(BaseHooksCallbackDaemon):
""" """
Context manger for achieving a compatibility with celery backend Context manger for achieving a compatibility with celery backend
It is calling a call to vcsserver, where it uses HooksCeleryClient to actually call a task from
f'rhodecode.lib.celerylib.tasks.{method}'
""" """
def __init__(self, task_queue, task_backend): def __init__(self, broker_url, result_backend):
self.task_queue = task_queue super().__init__()
self.task_backend = task_backend self.broker_url = broker_url
self.result_backend = result_backend
def __repr__(self): def __repr__(self):
return f'CeleryHooksCallbackDaemon(task_queue={self.task_queue}, task_backend={self.task_backend})' return f'CeleryHooksCallbackDaemon(broker_url={self.broker_url}, result_backend={self.result_backend})'
def __repr__(self):
return f'CeleryHooksCallbackDaemon(task_queue={self.task_queue}, task_backend={self.task_backend})'

View file

@ -17,88 +17,18 @@
# and proprietary license terms, please see https://rhodecode.com/licenses/ # and proprietary license terms, please see https://rhodecode.com/licenses/
import logging import logging
import traceback
from rhodecode.model import meta from rhodecode.lib.hook_daemon.base import BaseHooksCallbackDaemon
from rhodecode.lib import hooks_base
from rhodecode.lib.exceptions import HTTPLockedRC, HTTPBranchProtected
from rhodecode.lib.utils2 import AttributeDict
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Hooks(object): class HooksModuleCallbackDaemon(BaseHooksCallbackDaemon):
"""
Exposes the hooks for remote callbacks
"""
def __init__(self, request=None, log_prefix=''):
self.log_prefix = log_prefix
self.request = request
def repo_size(self, extras): def __init__(self, module):
log.debug("%sCalled repo_size of %s object", self.log_prefix, self) super().__init__()
return self._call_hook(hooks_base.repo_size, extras) self.hooks_module = module
def pre_pull(self, extras): def __repr__(self):
log.debug("%sCalled pre_pull of %s object", self.log_prefix, self) return f'HooksModuleCallbackDaemon(hooks_module={self.hooks_module})'
return self._call_hook(hooks_base.pre_pull, extras)
def post_pull(self, extras):
log.debug("%sCalled post_pull of %s object", self.log_prefix, self)
return self._call_hook(hooks_base.post_pull, extras)
def pre_push(self, extras):
log.debug("%sCalled pre_push of %s object", self.log_prefix, self)
return self._call_hook(hooks_base.pre_push, extras)
def post_push(self, extras):
log.debug("%sCalled post_push of %s object", self.log_prefix, self)
return self._call_hook(hooks_base.post_push, extras)
def _call_hook(self, hook, extras):
extras = AttributeDict(extras)
_server_url = extras['server_url']
extras.request = self.request
try:
result = hook(extras)
if result is None:
raise Exception(f'Failed to obtain hook result from func: {hook}')
except HTTPBranchProtected as error:
# Those special cases don't need error reporting. It's a case of
# locked repo or protected branch
result = AttributeDict({
'status': error.code,
'output': error.explanation
})
except (HTTPLockedRC, Exception) as error:
# locked needs different handling since we need to also
# handle PULL operations
exc_tb = ''
if not isinstance(error, HTTPLockedRC):
exc_tb = traceback.format_exc()
log.exception('%sException when handling hook %s', self.log_prefix, hook)
error_args = error.args
return {
'status': 128,
'output': '',
'exception': type(error).__name__,
'exception_traceback': exc_tb,
'exception_args': error_args,
}
finally:
meta.Session.remove()
log.debug('%sGot hook call response %s', self.log_prefix, result)
return {
'status': result.status,
'output': result.output,
}
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
pass

View file

@ -1,287 +0,0 @@
# Copyright (C) 2010-2023 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# 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 Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import os
import logging
import traceback
import threading
import socket
import msgpack
import gevent
from http.server import BaseHTTPRequestHandler
from socketserver import TCPServer
from rhodecode.model import meta
from rhodecode.lib.ext_json import json
from rhodecode.lib import rc_cache
from rhodecode.lib.svn_txn_utils import get_txn_id_data_key
from rhodecode.lib.hook_daemon.hook_module import Hooks
log = logging.getLogger(__name__)
class HooksHttpHandler(BaseHTTPRequestHandler):
JSON_HOOKS_PROTO = 'json.v1'
MSGPACK_HOOKS_PROTO = 'msgpack.v1'
# starting with RhodeCode 5.0.0 MsgPack is the default, prior it used json
DEFAULT_HOOKS_PROTO = MSGPACK_HOOKS_PROTO
@classmethod
def serialize_data(cls, data, proto=DEFAULT_HOOKS_PROTO):
if proto == cls.MSGPACK_HOOKS_PROTO:
return msgpack.packb(data)
return json.dumps(data)
@classmethod
def deserialize_data(cls, data, proto=DEFAULT_HOOKS_PROTO):
if proto == cls.MSGPACK_HOOKS_PROTO:
return msgpack.unpackb(data)
return json.loads(data)
def do_POST(self):
hooks_proto, method, extras = self._read_request()
log.debug('Handling HooksHttpHandler %s with %s proto', method, hooks_proto)
txn_id = getattr(self.server, 'txn_id', None)
if txn_id:
log.debug('Computing TXN_ID based on `%s`:`%s`',
extras['repository'], extras['txn_id'])
computed_txn_id = rc_cache.utils.compute_key_from_params(
extras['repository'], extras['txn_id'])
if txn_id != computed_txn_id:
raise Exception(
'TXN ID fail: expected {} got {} instead'.format(
txn_id, computed_txn_id))
request = getattr(self.server, 'request', None)
try:
hooks = Hooks(request=request, log_prefix='HOOKS: {} '.format(self.server.server_address))
result = self._call_hook_method(hooks, method, extras)
except Exception as e:
exc_tb = traceback.format_exc()
result = {
'exception': e.__class__.__name__,
'exception_traceback': exc_tb,
'exception_args': e.args
}
self._write_response(hooks_proto, result)
def _read_request(self):
length = int(self.headers['Content-Length'])
# respect sent headers, fallback to OLD proto for compatability
hooks_proto = self.headers.get('rc-hooks-protocol') or self.JSON_HOOKS_PROTO
if hooks_proto == self.MSGPACK_HOOKS_PROTO:
# support for new vcsserver msgpack based protocol hooks
body = self.rfile.read(length)
data = self.deserialize_data(body)
else:
body = self.rfile.read(length)
data = self.deserialize_data(body)
return hooks_proto, data['method'], data['extras']
def _write_response(self, hooks_proto, result):
self.send_response(200)
if hooks_proto == self.MSGPACK_HOOKS_PROTO:
self.send_header("Content-type", "application/msgpack")
self.end_headers()
data = self.serialize_data(result)
self.wfile.write(data)
else:
self.send_header("Content-type", "text/json")
self.end_headers()
data = self.serialize_data(result)
self.wfile.write(data)
def _call_hook_method(self, hooks, method, extras):
try:
result = getattr(hooks, method)(extras)
finally:
meta.Session.remove()
return result
def log_message(self, format, *args):
"""
This is an overridden method of BaseHTTPRequestHandler which logs using
a logging library instead of writing directly to stderr.
"""
message = format % args
log.debug(
"HOOKS: client=%s - - [%s] %s", self.client_address,
self.log_date_time_string(), message)
class ThreadedHookCallbackDaemon(object):
_callback_thread = None
_daemon = None
_done = False
use_gevent = False
def __init__(self, txn_id=None, host=None, port=None):
self._prepare(txn_id=txn_id, host=host, port=port)
if self.use_gevent:
self._run_func = self._run_gevent
self._stop_func = self._stop_gevent
else:
self._run_func = self._run
self._stop_func = self._stop
def __enter__(self):
log.debug('Running `%s` callback daemon', self.__class__.__name__)
self._run_func()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
log.debug('Exiting `%s` callback daemon', self.__class__.__name__)
self._stop_func()
def _prepare(self, txn_id=None, host=None, port=None):
raise NotImplementedError()
def _run(self):
raise NotImplementedError()
def _stop(self):
raise NotImplementedError()
def _run_gevent(self):
raise NotImplementedError()
def _stop_gevent(self):
raise NotImplementedError()
class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
"""
Context manager which will run a callback daemon in a background thread.
"""
hooks_uri = None
# From Python docs: Polling reduces our responsiveness to a shutdown
# request and wastes cpu at all other times.
POLL_INTERVAL = 0.01
use_gevent = False
def __repr__(self):
return f'HttpHooksCallbackDaemon(hooks_uri={self.hooks_uri})'
@property
def _hook_prefix(self):
return f'HOOKS: {self.hooks_uri} '
def get_hostname(self):
return socket.gethostname() or '127.0.0.1'
def get_available_port(self, min_port=20000, max_port=65535):
from rhodecode.lib.utils2 import get_available_port as _get_port
return _get_port(min_port, max_port)
def _prepare(self, txn_id=None, host=None, port=None):
from pyramid.threadlocal import get_current_request
if not host or host == "*":
host = self.get_hostname()
if not port:
port = self.get_available_port()
server_address = (host, port)
self.hooks_uri = f'{host}:{port}'
self.txn_id = txn_id
self._done = False
log.debug(
"%s Preparing HTTP callback daemon registering hook object: %s",
self._hook_prefix, HooksHttpHandler)
self._daemon = TCPServer(server_address, HooksHttpHandler)
# inject transaction_id for later verification
self._daemon.txn_id = self.txn_id
# pass the WEB app request into daemon
self._daemon.request = get_current_request()
def _run(self):
log.debug("Running thread-based loop of callback daemon in background")
callback_thread = threading.Thread(
target=self._daemon.serve_forever,
kwargs={'poll_interval': self.POLL_INTERVAL})
callback_thread.daemon = True
callback_thread.start()
self._callback_thread = callback_thread
def _run_gevent(self):
log.debug("Running gevent-based loop of callback daemon in background")
# create a new greenlet for the daemon's serve_forever method
callback_greenlet = gevent.spawn(
self._daemon.serve_forever,
poll_interval=self.POLL_INTERVAL)
# store reference to greenlet
self._callback_greenlet = callback_greenlet
# switch to this greenlet
gevent.sleep(0.01)
def _stop(self):
log.debug("Waiting for background thread to finish.")
self._daemon.shutdown()
self._callback_thread.join()
self._daemon = None
self._callback_thread = None
if self.txn_id:
#TODO: figure out the repo_path...
repo_path = ''
txn_id_file = get_txn_id_data_key(repo_path, self.txn_id)
log.debug('Cleaning up TXN ID %s', txn_id_file)
if os.path.isfile(txn_id_file):
os.remove(txn_id_file)
log.debug("Background thread done.")
def _stop_gevent(self):
log.debug("Waiting for background greenlet to finish.")
# if greenlet exists and is running
if self._callback_greenlet and not self._callback_greenlet.dead:
# shutdown daemon if it exists
if self._daemon:
self._daemon.shutdown()
# kill the greenlet
self._callback_greenlet.kill()
self._daemon = None
self._callback_greenlet = None
if self.txn_id:
#TODO: figure out the repo_path...
repo_path = ''
txn_id_file = get_txn_id_data_key(repo_path, self.txn_id)
log.debug('Cleaning up TXN ID %s', txn_id_file)
if os.path.isfile(txn_id_file):
os.remove(txn_id_file)
log.debug("Background greenlet done.")

View file

@ -0,0 +1,61 @@
# Copyright (C) 2010-2024 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# 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 Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import time
import logging
from rhodecode.lib.config_utils import get_app_config_lightweight
from rhodecode.lib.hook_daemon.base import Hooks
from rhodecode.lib.hook_daemon.hook_module import HooksModuleCallbackDaemon
from rhodecode.lib.hook_daemon.celery_hooks_deamon import CeleryHooksCallbackDaemon
from rhodecode.lib.type_utils import str2bool
log = logging.getLogger(__name__)
def prepare_callback_daemon(extras, protocol: str, txn_id=None):
hooks_config = {}
match protocol:
case 'celery':
config = get_app_config_lightweight(extras['config'])
broker_url = config.get('celery.broker_url')
result_backend = config.get('celery.result_backend')
hooks_config = {
'broker_url': broker_url,
'result_backend': result_backend,
}
callback_daemon = CeleryHooksCallbackDaemon(broker_url, result_backend)
case 'local':
callback_daemon = HooksModuleCallbackDaemon(Hooks.__module__)
case _:
log.error('Unsupported callback daemon protocol "%s"', protocol)
raise Exception('Unsupported callback daemon protocol.')
extras['hooks_config'] = hooks_config
extras['hooks_protocol'] = protocol
extras['time'] = time.time()
# register txn_id
extras['txn_id'] = txn_id
log.debug('Prepared a callback daemon: %s', callback_daemon.__class__.__name__)
return callback_daemon, extras

View file

@ -30,14 +30,14 @@ from rhodecode.lib import helpers as h
from rhodecode.lib import audit_logger from rhodecode.lib import audit_logger
from rhodecode.lib.utils2 import safe_str, user_agent_normalizer from rhodecode.lib.utils2 import safe_str, user_agent_normalizer
from rhodecode.lib.exceptions import ( from rhodecode.lib.exceptions import (
HTTPLockedRC, HTTPBranchProtected, UserCreationError, ClientNotSupportedError) HTTPLockedRepo, HTTPBranchProtected, UserCreationError, ClientNotSupported)
from rhodecode.model.db import Repository, User from rhodecode.model.db import Repository, User
from rhodecode.lib.statsd_client import StatsdClient from rhodecode.lib.statsd_client import StatsdClient
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class HookResponse(object): class HookResponse:
def __init__(self, status, output): def __init__(self, status, output):
self.status = status self.status = status
self.output = output self.output = output
@ -56,6 +56,8 @@ class HookResponse(object):
def to_json(self): def to_json(self):
return {'status': self.status, 'output': self.output} return {'status': self.status, 'output': self.output}
def __repr__(self):
return self.to_json().__repr__()
def is_shadow_repo(extras): def is_shadow_repo(extras):
""" """
@ -73,8 +75,69 @@ def check_vcs_client(extras):
except ModuleNotFoundError: except ModuleNotFoundError:
is_vcs_client_whitelisted = lambda *x: True is_vcs_client_whitelisted = lambda *x: True
backend = extras.get('scm') backend = extras.get('scm')
if not is_vcs_client_whitelisted(extras.get('user_agent'), backend): user_agent = extras.get('user_agent')
raise ClientNotSupportedError(f"Your {backend} client is forbidden") if not is_vcs_client_whitelisted(user_agent, backend):
raise ClientNotSupported(f"Your {backend} client (version={user_agent}) is forbidden by security rules")
def check_locked_repo(extras, check_same_user=True):
user = User.get_by_username(extras.username)
output = ''
if extras.locked_by[0] and (not check_same_user or user.user_id != extras.locked_by[0]):
locked_by = User.get(extras.locked_by[0]).username
reason = extras.locked_by[2]
# this exception is interpreted in git/hg middlewares and based
# on that proper return code is server to client
_http_ret = HTTPLockedRepo(_locked_by_explanation(extras.repository, locked_by, reason))
if str(_http_ret.code).startswith('2'):
# 2xx Codes don't raise exceptions
output = _http_ret.title
else:
raise _http_ret
return output
def check_branch_protected(extras):
if extras.commit_ids and extras.check_branch_perms:
user = User.get_by_username(extras.username)
auth_user = user.AuthUser()
repo = Repository.get_by_repo_name(extras.repository)
if not repo:
raise ValueError(f'Repo for {extras.repository} not found')
affected_branches = []
if repo.repo_type == 'hg':
for entry in extras.commit_ids:
if entry['type'] == 'branch':
is_forced = bool(entry['multiple_heads'])
affected_branches.append([entry['name'], is_forced])
elif repo.repo_type == 'git':
for entry in extras.commit_ids:
if entry['type'] == 'heads':
is_forced = bool(entry['pruned_sha'])
affected_branches.append([entry['name'], is_forced])
for branch_name, is_forced in affected_branches:
rule, branch_perm = auth_user.get_rule_and_branch_permission(extras.repository, branch_name)
if not branch_perm:
# no branch permission found for this branch, just keep checking
continue
if branch_perm == 'branch.push_force':
continue
elif branch_perm == 'branch.push' and is_forced is False:
continue
elif branch_perm == 'branch.push' and is_forced is True:
halt_message = f'Branch `{branch_name}` changes rejected by rule {rule}. ' \
f'FORCE PUSH FORBIDDEN.'
else:
halt_message = f'Branch `{branch_name}` changes rejected by rule {rule}.'
if halt_message:
_http_ret = HTTPBranchProtected(halt_message)
raise _http_ret
def _get_scm_size(alias, root_path): def _get_scm_size(alias, root_path):
@ -109,116 +172,30 @@ def repo_size(extras):
repo = Repository.get_by_repo_name(extras.repository) repo = Repository.get_by_repo_name(extras.repository)
vcs_part = f'.{repo.repo_type}' vcs_part = f'.{repo.repo_type}'
size_vcs, size_root, size_total = _get_scm_size(vcs_part, repo.repo_full_path) size_vcs, size_root, size_total = _get_scm_size(vcs_part, repo.repo_full_path)
msg = (f'RhodeCode: `{repo.repo_name}` size summary {vcs_part}:{size_vcs} repo:{size_root} total:{size_total}\n') msg = f'RhodeCode: `{repo.repo_name}` size summary {vcs_part}:{size_vcs} repo:{size_root} total:{size_total}\n'
return HookResponse(0, msg) return HookResponse(0, msg)
def pre_push(extras):
"""
Hook executed before pushing code.
It bans pushing when the repository is locked.
"""
check_vcs_client(extras)
user = User.get_by_username(extras.username)
output = ''
if extras.locked_by[0] and user.user_id != int(extras.locked_by[0]):
locked_by = User.get(extras.locked_by[0]).username
reason = extras.locked_by[2]
# this exception is interpreted in git/hg middlewares and based
# on that proper return code is server to client
_http_ret = HTTPLockedRC(
_locked_by_explanation(extras.repository, locked_by, reason))
if str(_http_ret.code).startswith('2'):
# 2xx Codes don't raise exceptions
output = _http_ret.title
else:
raise _http_ret
hook_response = ''
if not is_shadow_repo(extras):
if extras.commit_ids and extras.check_branch_perms:
auth_user = user.AuthUser()
repo = Repository.get_by_repo_name(extras.repository)
if not repo:
raise ValueError(f'Repo for {extras.repository} not found')
affected_branches = []
if repo.repo_type == 'hg':
for entry in extras.commit_ids:
if entry['type'] == 'branch':
is_forced = bool(entry['multiple_heads'])
affected_branches.append([entry['name'], is_forced])
elif repo.repo_type == 'git':
for entry in extras.commit_ids:
if entry['type'] == 'heads':
is_forced = bool(entry['pruned_sha'])
affected_branches.append([entry['name'], is_forced])
for branch_name, is_forced in affected_branches:
rule, branch_perm = auth_user.get_rule_and_branch_permission(
extras.repository, branch_name)
if not branch_perm:
# no branch permission found for this branch, just keep checking
continue
if branch_perm == 'branch.push_force':
continue
elif branch_perm == 'branch.push' and is_forced is False:
continue
elif branch_perm == 'branch.push' and is_forced is True:
halt_message = f'Branch `{branch_name}` changes rejected by rule {rule}. ' \
f'FORCE PUSH FORBIDDEN.'
else:
halt_message = f'Branch `{branch_name}` changes rejected by rule {rule}.'
if halt_message:
_http_ret = HTTPBranchProtected(halt_message)
raise _http_ret
# Propagate to external components. This is done after checking the
# lock, for consistent behavior.
hook_response = pre_push_extension(
repo_store_path=Repository.base_path(), **extras)
events.trigger(events.RepoPrePushEvent(
repo_name=extras.repository, extras=extras))
return HookResponse(0, output) + hook_response
def pre_pull(extras): def pre_pull(extras):
""" """
Hook executed before pulling the code. Hook executed before pulling the code.
It bans pulling when the repository is locked. It bans pulling when the repository is locked.
It bans pulling when incorrect client is used.
""" """
check_vcs_client(extras)
output = '' output = ''
if extras.locked_by[0]: check_vcs_client(extras)
locked_by = User.get(extras.locked_by[0]).username
reason = extras.locked_by[2] # locking repo can, but not have to stop the operation it can also just produce output
# this exception is interpreted in git/hg middlewares and based output += check_locked_repo(extras, check_same_user=False)
# on that proper return code is server to client
_http_ret = HTTPLockedRC(
_locked_by_explanation(extras.repository, locked_by, reason))
if str(_http_ret.code).startswith('2'):
# 2xx Codes don't raise exceptions
output = _http_ret.title
else:
raise _http_ret
# Propagate to external components. This is done after checking the # Propagate to external components. This is done after checking the
# lock, for consistent behavior. # lock, for consistent behavior.
hook_response = '' hook_response = ''
if not is_shadow_repo(extras): if not is_shadow_repo(extras):
extras.hook_type = extras.hook_type or 'pre_pull' extras.hook_type = extras.hook_type or 'pre_pull'
hook_response = pre_pull_extension( hook_response = pre_pull_extension(repo_store_path=Repository.base_path(), **extras)
repo_store_path=Repository.base_path(), **extras) events.trigger(events.RepoPrePullEvent(repo_name=extras.repository, extras=extras))
events.trigger(events.RepoPrePullEvent(
repo_name=extras.repository, extras=extras))
return HookResponse(0, output) + hook_response return HookResponse(0, output) + hook_response
@ -239,6 +216,7 @@ def post_pull(extras):
statsd.incr('rhodecode_pull_total', tags=[ statsd.incr('rhodecode_pull_total', tags=[
f'user-agent:{user_agent_normalizer(extras.user_agent)}', f'user-agent:{user_agent_normalizer(extras.user_agent)}',
]) ])
output = '' output = ''
# make lock is a tri state False, True, None. We only make lock on True # make lock is a tri state False, True, None. We only make lock on True
if extras.make_lock is True and not is_shadow_repo(extras): if extras.make_lock is True and not is_shadow_repo(extras):
@ -246,18 +224,9 @@ def post_pull(extras):
Repository.lock(Repository.get_by_repo_name(extras.repository), Repository.lock(Repository.get_by_repo_name(extras.repository),
user.user_id, user.user_id,
lock_reason=Repository.LOCK_PULL) lock_reason=Repository.LOCK_PULL)
msg = 'Made lock on repo `{}`'.format(extras.repository) msg = f'Made lock on repo `{extras.repository}`'
output += msg output += msg
if extras.locked_by[0]:
locked_by = User.get(extras.locked_by[0]).username
reason = extras.locked_by[2]
_http_ret = HTTPLockedRC(
_locked_by_explanation(extras.repository, locked_by, reason))
if str(_http_ret.code).startswith('2'):
# 2xx Codes don't raise exceptions
output += _http_ret.title
# Propagate to external components. # Propagate to external components.
hook_response = '' hook_response = ''
if not is_shadow_repo(extras): if not is_shadow_repo(extras):
@ -270,6 +239,33 @@ def post_pull(extras):
return HookResponse(0, output) + hook_response return HookResponse(0, output) + hook_response
def pre_push(extras):
"""
Hook executed before pushing code.
It bans pushing when the repository is locked.
It banks pushing when incorrect client is used.
It also checks for Branch protection
"""
output = ''
check_vcs_client(extras)
# locking repo can, but not have to stop the operation it can also just produce output
output += check_locked_repo(extras)
hook_response = ''
if not is_shadow_repo(extras):
check_branch_protected(extras)
# Propagate to external components. This is done after checking the
# lock, for consistent behavior.
hook_response = pre_push_extension(repo_store_path=Repository.base_path(), **extras)
events.trigger(events.RepoPrePushEvent(repo_name=extras.repository, extras=extras))
return HookResponse(0, output) + hook_response
def post_push(extras): def post_push(extras):
"""Hook executed after user pushes to the repository.""" """Hook executed after user pushes to the repository."""
commit_ids = extras.commit_ids commit_ids = extras.commit_ids
@ -292,22 +288,13 @@ def post_push(extras):
# Propagate to external components. # Propagate to external components.
output = '' output = ''
# make lock is a tri state False, True, None. We only release lock on False # make lock is a tri state False, True, None. We only release lock on False
if extras.make_lock is False and not is_shadow_repo(extras): if extras.make_lock is False and not is_shadow_repo(extras):
Repository.unlock(Repository.get_by_repo_name(extras.repository)) Repository.unlock(Repository.get_by_repo_name(extras.repository))
msg = f'Released lock on repo `{extras.repository}`\n' msg = f'Released lock on repo `{extras.repository}`\n'
output += msg output += msg
if extras.locked_by[0]:
locked_by = User.get(extras.locked_by[0]).username
reason = extras.locked_by[2]
_http_ret = HTTPLockedRC(
_locked_by_explanation(extras.repository, locked_by, reason))
# TODO: johbo: if not?
if str(_http_ret.code).startswith('2'):
# 2xx Codes don't raise exceptions
output += _http_ret.title
if extras.new_refs: if extras.new_refs:
tmpl = '{}/{}/pull-request/new?{{ref_type}}={{ref_name}}'.format( tmpl = '{}/{}/pull-request/new?{{ref_type}}={{ref_name}}'.format(
safe_str(extras.server_url), safe_str(extras.repository)) safe_str(extras.server_url), safe_str(extras.repository))
@ -322,11 +309,8 @@ def post_push(extras):
hook_response = '' hook_response = ''
if not is_shadow_repo(extras): if not is_shadow_repo(extras):
hook_response = post_push_extension( hook_response = post_push_extension(repo_store_path=Repository.base_path(), **extras)
repo_store_path=Repository.base_path(), events.trigger(events.RepoPushEvent(repo_name=extras.repository, pushed_commit_ids=commit_ids, extras=extras))
**extras)
events.trigger(events.RepoPushEvent(
repo_name=extras.repository, pushed_commit_ids=commit_ids, extras=extras))
output += 'RhodeCode: push completed\n' output += 'RhodeCode: push completed\n'
return HookResponse(0, output) + hook_response return HookResponse(0, output) + hook_response
@ -380,12 +364,20 @@ class ExtensionCallback(object):
# with older rcextensions that require api_key present # with older rcextensions that require api_key present
if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']: if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']:
kwargs_to_pass['api_key'] = '_DEPRECATED_' kwargs_to_pass['api_key'] = '_DEPRECATED_'
return callback(**kwargs_to_pass) result = callback(**kwargs_to_pass)
log.debug('got rcextensions result: %s', result)
return result
def is_active(self): def is_active(self):
return hasattr(rhodecode.EXTENSIONS, self._hook_name) return hasattr(rhodecode.EXTENSIONS, self._hook_name)
def _get_callback(self): def _get_callback(self):
if rhodecode.is_test:
log.debug('In test mode, reloading rcextensions...')
# NOTE: for test re-load rcextensions always so we can dynamically change them for testing purposes
from rhodecode.lib.utils import load_rcextensions
load_rcextensions(root_path=os.path.dirname(rhodecode.CONFIG['__file__']))
return getattr(rhodecode.EXTENSIONS, self._hook_name, None)
return getattr(rhodecode.EXTENSIONS, self._hook_name, None) return getattr(rhodecode.EXTENSIONS, self._hook_name, None)

View file

@ -40,16 +40,6 @@ GIT_PROTO_PAT = re.compile(
GIT_LFS_PROTO_PAT = re.compile(r'^/(.+)/(info/lfs/(.+))') GIT_LFS_PROTO_PAT = re.compile(r'^/(.+)/(info/lfs/(.+))')
def default_lfs_store():
"""
Default lfs store location, it's consistent with Mercurials large file
store which is in .cache/largefiles
"""
from rhodecode.lib.vcs.backends.git import lfs_store
user_home = os.path.expanduser("~")
return lfs_store(user_home)
class SimpleGit(simplevcs.SimpleVCS): class SimpleGit(simplevcs.SimpleVCS):
SCM = 'git' SCM = 'git'
@ -151,6 +141,6 @@ class SimpleGit(simplevcs.SimpleVCS):
extras['git_lfs_enabled'] = utils2.str2bool( extras['git_lfs_enabled'] = utils2.str2bool(
config.get('vcs_git_lfs', 'enabled')) config.get('vcs_git_lfs', 'enabled'))
extras['git_lfs_store_path'] = custom_store or default_lfs_store() extras['git_lfs_store_path'] = custom_store
extras['git_lfs_http_scheme'] = scheme extras['git_lfs_http_scheme'] = scheme
return extras return extras

View file

@ -1,5 +1,3 @@
# Copyright (C) 2014-2023 RhodeCode GmbH # Copyright (C) 2014-2023 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@ -32,8 +30,7 @@ from functools import wraps
import time import time
from paste.httpheaders import REMOTE_USER, AUTH_TYPE from paste.httpheaders import REMOTE_USER, AUTH_TYPE
from pyramid.httpexceptions import ( from pyramid.httpexceptions import HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError
HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
from zope.cachedescriptors.property import Lazy as LazyProperty from zope.cachedescriptors.property import Lazy as LazyProperty
import rhodecode import rhodecode
@ -41,10 +38,9 @@ from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
from rhodecode.lib import rc_cache from rhodecode.lib import rc_cache
from rhodecode.lib.svn_txn_utils import store_txn_id_data from rhodecode.lib.svn_txn_utils import store_txn_id_data
from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
from rhodecode.lib.base import ( from rhodecode.lib.base import BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context
BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context) from rhodecode.lib.exceptions import UserCreationError, NotAllowedToCreateUserError
from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError) from rhodecode.lib.hook_daemon.utils import prepare_callback_daemon
from rhodecode.lib.hook_daemon.base import prepare_callback_daemon
from rhodecode.lib.middleware import appenlight from rhodecode.lib.middleware import appenlight
from rhodecode.lib.middleware.utils import scm_app_http from rhodecode.lib.middleware.utils import scm_app_http
from rhodecode.lib.str_utils import safe_bytes, safe_int from rhodecode.lib.str_utils import safe_bytes, safe_int
@ -78,17 +74,18 @@ def initialize_generator(factory):
try: try:
init = next(gen) init = next(gen)
except StopIteration: except StopIteration:
raise ValueError('Generator must yield at least one element.') raise ValueError("Generator must yield at least one element.")
if init != "__init__": if init != "__init__":
raise ValueError('First yielded element must be "__init__".') raise ValueError('First yielded element must be "__init__".')
return gen return gen
return wrapper return wrapper
class SimpleVCS(object): class SimpleVCS(object):
"""Common functionality for SCM HTTP handlers.""" """Common functionality for SCM HTTP handlers."""
SCM = 'unknown' SCM = "unknown"
acl_repo_name = None acl_repo_name = None
url_repo_name = None url_repo_name = None
@ -100,11 +97,11 @@ class SimpleVCS(object):
# we use this regex which will match only on URLs pointing to shadow # we use this regex which will match only on URLs pointing to shadow
# repositories. # repositories.
shadow_repo_re = re.compile( shadow_repo_re = re.compile(
'(?P<groups>(?:{slug_pat}/)*)' # repo groups "(?P<groups>(?:{slug_pat}/)*)" # repo groups
'(?P<target>{slug_pat})/' # target repo "(?P<target>{slug_pat})/" # target repo
'pull-request/(?P<pr_id>\\d+)/' # pull request "pull-request/(?P<pr_id>\\d+)/" # pull request
'repository$' # shadow repo "repository$".format(slug_pat=SLUG_RE.pattern) # shadow repo
.format(slug_pat=SLUG_RE.pattern)) )
def __init__(self, config, registry): def __init__(self, config, registry):
self.registry = registry self.registry = registry
@ -113,15 +110,14 @@ class SimpleVCS(object):
self.repo_vcs_config = base.Config() self.repo_vcs_config = base.Config()
rc_settings = SettingsModel().get_all_settings(cache=True, from_request=False) rc_settings = SettingsModel().get_all_settings(cache=True, from_request=False)
realm = rc_settings.get('rhodecode_realm') or 'RhodeCode AUTH' realm = rc_settings.get("rhodecode_realm") or "RhodeCode AUTH"
# authenticate this VCS request using authfunc # authenticate this VCS request using authfunc
auth_ret_code_detection = \ auth_ret_code_detection = str2bool(self.config.get("auth_ret_code_detection", False))
str2bool(self.config.get('auth_ret_code_detection', False))
self.authenticate = BasicAuth( self.authenticate = BasicAuth(
'', authenticate, registry, config.get('auth_ret_code'), "", authenticate, registry, config.get("auth_ret_code"), auth_ret_code_detection, rc_realm=realm
auth_ret_code_detection, rc_realm=realm) )
self.ip_addr = '0.0.0.0' self.ip_addr = "0.0.0.0"
@LazyProperty @LazyProperty
def global_vcs_config(self): def global_vcs_config(self):
@ -132,10 +128,10 @@ class SimpleVCS(object):
@property @property
def base_path(self): def base_path(self):
settings_path = self.config.get('repo_store.path') settings_path = self.config.get("repo_store.path")
if not settings_path: if not settings_path:
raise ValueError('FATAL: repo_store.path is empty') raise ValueError("FATAL: repo_store.path is empty")
return settings_path return settings_path
def set_repo_names(self, environ): def set_repo_names(self, environ):
@ -164,17 +160,16 @@ class SimpleVCS(object):
match_dict = match.groupdict() match_dict = match.groupdict()
# Build acl repo name from regex match. # Build acl repo name from regex match.
acl_repo_name = safe_str('{groups}{target}'.format( acl_repo_name = safe_str(
groups=match_dict['groups'] or '', "{groups}{target}".format(groups=match_dict["groups"] or "", target=match_dict["target"])
target=match_dict['target'])) )
# Retrieve pull request instance by ID from regex match. # Retrieve pull request instance by ID from regex match.
pull_request = PullRequest.get(match_dict['pr_id']) pull_request = PullRequest.get(match_dict["pr_id"])
# Only proceed if we got a pull request and if acl repo name from # Only proceed if we got a pull request and if acl repo name from
# URL equals the target repo name of the pull request. # URL equals the target repo name of the pull request.
if pull_request and (acl_repo_name == pull_request.target_repo.repo_name): if pull_request and (acl_repo_name == pull_request.target_repo.repo_name):
# Get file system path to shadow repository. # Get file system path to shadow repository.
workspace_id = PullRequestModel()._workspace_id(pull_request) workspace_id = PullRequestModel()._workspace_id(pull_request)
vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id) vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
@ -184,21 +179,23 @@ class SimpleVCS(object):
self.acl_repo_name = acl_repo_name self.acl_repo_name = acl_repo_name
self.is_shadow_repo = True self.is_shadow_repo = True
log.debug('Setting all VCS repository names: %s', { log.debug(
'acl_repo_name': self.acl_repo_name, "Setting all VCS repository names: %s",
'url_repo_name': self.url_repo_name, {
'vcs_repo_name': self.vcs_repo_name, "acl_repo_name": self.acl_repo_name,
}) "url_repo_name": self.url_repo_name,
"vcs_repo_name": self.vcs_repo_name,
},
)
@property @property
def scm_app(self): def scm_app(self):
custom_implementation = self.config['vcs.scm_app_implementation'] custom_implementation = self.config["vcs.scm_app_implementation"]
if custom_implementation == 'http': if custom_implementation == "http":
log.debug('Using HTTP implementation of scm app.') log.debug("Using HTTP implementation of scm app.")
scm_app_impl = scm_app_http scm_app_impl = scm_app_http
else: else:
log.debug('Using custom implementation of scm_app: "{}"'.format( log.debug('Using custom implementation of scm_app: "{}"'.format(custom_implementation))
custom_implementation))
scm_app_impl = importlib.import_module(custom_implementation) scm_app_impl = importlib.import_module(custom_implementation)
return scm_app_impl return scm_app_impl
@ -208,17 +205,18 @@ class SimpleVCS(object):
with a repository_name for support of _<ID> non changeable urls with a repository_name for support of _<ID> non changeable urls
""" """
data = repo_name.split('/') data = repo_name.split("/")
if len(data) >= 2: if len(data) >= 2:
from rhodecode.model.repo import RepoModel from rhodecode.model.repo import RepoModel
by_id_match = RepoModel().get_repo_by_id(repo_name) by_id_match = RepoModel().get_repo_by_id(repo_name)
if by_id_match: if by_id_match:
data[1] = by_id_match.repo_name data[1] = by_id_match.repo_name
# Because PEP-3333-WSGI uses bytes-tunneled-in-latin-1 as PATH_INFO # Because PEP-3333-WSGI uses bytes-tunneled-in-latin-1 as PATH_INFO
# and we use this data # and we use this data
maybe_new_path = '/'.join(data) maybe_new_path = "/".join(data)
return safe_bytes(maybe_new_path).decode('latin1') return safe_bytes(maybe_new_path).decode("latin1")
def _invalidate_cache(self, repo_name): def _invalidate_cache(self, repo_name):
""" """
@ -231,21 +229,18 @@ class SimpleVCS(object):
def is_valid_and_existing_repo(self, repo_name, base_path, scm_type): def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
db_repo = Repository.get_by_repo_name(repo_name) db_repo = Repository.get_by_repo_name(repo_name)
if not db_repo: if not db_repo:
log.debug('Repository `%s` not found inside the database.', log.debug("Repository `%s` not found inside the database.", repo_name)
repo_name)
return False return False
if db_repo.repo_type != scm_type: if db_repo.repo_type != scm_type:
log.warning( log.warning(
'Repository `%s` have incorrect scm_type, expected %s got %s', "Repository `%s` have incorrect scm_type, expected %s got %s", repo_name, db_repo.repo_type, scm_type
repo_name, db_repo.repo_type, scm_type) )
return False return False
config = db_repo._config config = db_repo._config
config.set('extensions', 'largefiles', '') config.set("extensions", "largefiles", "")
return is_valid_repo( return is_valid_repo(repo_name, base_path, explicit_scm=scm_type, expect_scm=scm_type, config=config)
repo_name, base_path,
explicit_scm=scm_type, expect_scm=scm_type, config=config)
def valid_and_active_user(self, user): def valid_and_active_user(self, user):
""" """
@ -267,8 +262,9 @@ class SimpleVCS(object):
def is_shadow_repo_dir(self): def is_shadow_repo_dir(self):
return os.path.isdir(self.vcs_repo_name) return os.path.isdir(self.vcs_repo_name)
def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None, def _check_permission(
plugin_id='', plugin_cache_active=False, cache_ttl=0): self, action, user, auth_user, repo_name, ip_addr=None, plugin_id="", plugin_cache_active=False, cache_ttl=0
):
""" """
Checks permissions using action (push/pull) user and repository Checks permissions using action (push/pull) user and repository
name. If plugin_cache and ttl is set it will use the plugin which name. If plugin_cache and ttl is set it will use the plugin which
@ -280,71 +276,67 @@ class SimpleVCS(object):
:param repo_name: repository name :param repo_name: repository name
""" """
log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)', log.debug("AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)", plugin_id, plugin_cache_active, cache_ttl)
plugin_id, plugin_cache_active, cache_ttl)
user_id = user.user_id user_id = user.user_id
cache_namespace_uid = f'cache_user_auth.{rc_cache.PERMISSIONS_CACHE_VER}.{user_id}' cache_namespace_uid = f"cache_user_auth.{rc_cache.PERMISSIONS_CACHE_VER}.{user_id}"
region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid) region = rc_cache.get_or_create_region("cache_perms", cache_namespace_uid)
@region.conditional_cache_on_arguments(namespace=cache_namespace_uid, @region.conditional_cache_on_arguments(
expiration_time=cache_ttl, namespace=cache_namespace_uid, expiration_time=cache_ttl, condition=plugin_cache_active
condition=plugin_cache_active) )
def compute_perm_vcs( def compute_perm_vcs(cache_name, plugin_id, action, user_id, repo_name, ip_addr):
cache_name, plugin_id, action, user_id, repo_name, ip_addr): log.debug("auth: calculating permission access now for vcs operation: %s", action)
log.debug('auth: calculating permission access now for vcs operation: %s', action)
# check IP # check IP
inherit = user.inherit_default_permissions inherit = user.inherit_default_permissions
ip_allowed = AuthUser.check_ip_allowed( ip_allowed = AuthUser.check_ip_allowed(user_id, ip_addr, inherit_from_default=inherit)
user_id, ip_addr, inherit_from_default=inherit)
if ip_allowed: if ip_allowed:
log.info('Access for IP:%s allowed', ip_addr) log.info("Access for IP:%s allowed", ip_addr)
else: else:
return False return False
if action == 'push': if action == "push":
perms = ('repository.write', 'repository.admin') perms = ("repository.write", "repository.admin")
if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name): if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
return False return False
else: else:
# any other action need at least read permission # any other action need at least read permission
perms = ( perms = ("repository.read", "repository.write", "repository.admin")
'repository.read', 'repository.write', 'repository.admin')
if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name): if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
return False return False
return True return True
start = time.time() start = time.time()
log.debug('Running plugin `%s` permissions check', plugin_id) log.debug("Running plugin `%s` permissions check", plugin_id)
# for environ based auth, password can be empty, but then the validation is # for environ based auth, password can be empty, but then the validation is
# on the server that fills in the env data needed for authentication # on the server that fills in the env data needed for authentication
perm_result = compute_perm_vcs( perm_result = compute_perm_vcs("vcs_permissions", plugin_id, action, user.user_id, repo_name, ip_addr)
'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
auth_time = time.time() - start auth_time = time.time() - start
log.debug('Permissions for plugin `%s` completed in %.4fs, ' log.debug(
'expiration time of fetched cache %.1fs.', "Permissions for plugin `%s` completed in %.4fs, " "expiration time of fetched cache %.1fs.",
plugin_id, auth_time, cache_ttl) plugin_id,
auth_time,
cache_ttl,
)
return perm_result return perm_result
def _get_http_scheme(self, environ): def _get_http_scheme(self, environ):
try: try:
return environ['wsgi.url_scheme'] return environ["wsgi.url_scheme"]
except Exception: except Exception:
log.exception('Failed to read http scheme') log.exception("Failed to read http scheme")
return 'http' return "http"
def _get_default_cache_ttl(self): def _get_default_cache_ttl(self):
# take AUTH_CACHE_TTL from the `rhodecode` auth plugin # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode') plugin = loadplugin("egg:rhodecode-enterprise-ce#rhodecode")
plugin_settings = plugin.get_settings() plugin_settings = plugin.get_settings()
plugin_cache_active, cache_ttl = plugin.get_ttl_cache( plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings) or (False, 0)
plugin_settings) or (False, 0)
return plugin_cache_active, cache_ttl return plugin_cache_active, cache_ttl
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
@ -359,17 +351,17 @@ class SimpleVCS(object):
def _handle_request(self, environ, start_response): def _handle_request(self, environ, start_response):
if not self.url_repo_name: if not self.url_repo_name:
log.warning('Repository name is empty: %s', self.url_repo_name) log.warning("Repository name is empty: %s", self.url_repo_name)
# failed to get repo name, we fail now # failed to get repo name, we fail now
return HTTPNotFound()(environ, start_response) return HTTPNotFound()(environ, start_response)
log.debug('Extracted repo name is %s', self.url_repo_name) log.debug("Extracted repo name is %s", self.url_repo_name)
ip_addr = get_ip_addr(environ) ip_addr = get_ip_addr(environ)
user_agent = get_user_agent(environ) user_agent = get_user_agent(environ)
username = None username = None
# skip passing error to error controller # skip passing error to error controller
environ['pylons.status_code_redirect'] = True environ["pylons.status_code_redirect"] = True
# ====================================================================== # ======================================================================
# GET ACTION PULL or PUSH # GET ACTION PULL or PUSH
@ -380,17 +372,15 @@ class SimpleVCS(object):
# Check if this is a request to a shadow repository of a pull request. # Check if this is a request to a shadow repository of a pull request.
# In this case only pull action is allowed. # In this case only pull action is allowed.
# ====================================================================== # ======================================================================
if self.is_shadow_repo and action != 'pull': if self.is_shadow_repo and action != "pull":
reason = 'Only pull action is allowed for shadow repositories.' reason = "Only pull action is allowed for shadow repositories."
log.debug('User not allowed to proceed, %s', reason) log.debug("User not allowed to proceed, %s", reason)
return HTTPNotAcceptable(reason)(environ, start_response) return HTTPNotAcceptable(reason)(environ, start_response)
# Check if the shadow repo actually exists, in case someone refers # Check if the shadow repo actually exists, in case someone refers
# to it, and it has been deleted because of successful merge. # to it, and it has been deleted because of successful merge.
if self.is_shadow_repo and not self.is_shadow_repo_dir: if self.is_shadow_repo and not self.is_shadow_repo_dir:
log.debug( log.debug("Shadow repo detected, and shadow repo dir `%s` is missing", self.is_shadow_repo_dir)
'Shadow repo detected, and shadow repo dir `%s` is missing',
self.is_shadow_repo_dir)
return HTTPNotFound()(environ, start_response) return HTTPNotFound()(environ, start_response)
# ====================================================================== # ======================================================================
@ -398,7 +388,7 @@ class SimpleVCS(object):
# ====================================================================== # ======================================================================
detect_force_push = False detect_force_push = False
check_branch_perms = False check_branch_perms = False
if action in ['pull', 'push']: if action in ["pull", "push"]:
user_obj = anonymous_user = User.get_default_user() user_obj = anonymous_user = User.get_default_user()
auth_user = user_obj.AuthUser() auth_user = user_obj.AuthUser()
username = anonymous_user.username username = anonymous_user.username
@ -406,8 +396,12 @@ class SimpleVCS(object):
plugin_cache_active, cache_ttl = self._get_default_cache_ttl() plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
# ONLY check permissions if the user is activated # ONLY check permissions if the user is activated
anonymous_perm = self._check_permission( anonymous_perm = self._check_permission(
action, anonymous_user, auth_user, self.acl_repo_name, ip_addr, action,
plugin_id='anonymous_access', anonymous_user,
auth_user,
self.acl_repo_name,
ip_addr,
plugin_id="anonymous_access",
plugin_cache_active=plugin_cache_active, plugin_cache_active=plugin_cache_active,
cache_ttl=cache_ttl, cache_ttl=cache_ttl,
) )
@ -416,12 +410,13 @@ class SimpleVCS(object):
if not anonymous_user.active or not anonymous_perm: if not anonymous_user.active or not anonymous_perm:
if not anonymous_user.active: if not anonymous_user.active:
log.debug('Anonymous access is disabled, running ' log.debug("Anonymous access is disabled, running " "authentication")
'authentication')
if not anonymous_perm: if not anonymous_perm:
log.debug('Not enough credentials to access repo: `%s` ' log.debug(
'repository as anonymous user', self.acl_repo_name) "Not enough credentials to access repo: `%s` " "repository as anonymous user",
self.acl_repo_name,
)
username = None username = None
# ============================================================== # ==============================================================
@ -430,19 +425,18 @@ class SimpleVCS(object):
# ============================================================== # ==============================================================
# try to auth based on environ, container auth methods # try to auth based on environ, container auth methods
log.debug('Running PRE-AUTH for container|headers based authentication') log.debug("Running PRE-AUTH for container|headers based authentication")
# headers auth, by just reading special headers and bypass the auth with user/passwd # headers auth, by just reading special headers and bypass the auth with user/passwd
pre_auth = authenticate( pre_auth = authenticate(
'', '', environ, VCS_TYPE, registry=self.registry, "", "", environ, VCS_TYPE, registry=self.registry, acl_repo_name=self.acl_repo_name
acl_repo_name=self.acl_repo_name) )
if pre_auth and pre_auth.get('username'): if pre_auth and pre_auth.get("username"):
username = pre_auth['username'] username = pre_auth["username"]
log.debug('PRE-AUTH got `%s` as username', username) log.debug("PRE-AUTH got `%s` as username", username)
if pre_auth: if pre_auth:
log.debug('PRE-AUTH successful from %s', log.debug("PRE-AUTH successful from %s", pre_auth.get("auth_data", {}).get("_plugin"))
pre_auth.get('auth_data', {}).get('_plugin'))
# If not authenticated by the container, running basic auth # If not authenticated by the container, running basic auth
# before inject the calling repo_name for special scope checks # before inject the calling repo_name for special scope checks
@ -463,16 +457,16 @@ class SimpleVCS(object):
return HTTPNotAcceptable(reason)(environ, start_response) return HTTPNotAcceptable(reason)(environ, start_response)
if isinstance(auth_result, dict): if isinstance(auth_result, dict):
AUTH_TYPE.update(environ, 'basic') AUTH_TYPE.update(environ, "basic")
REMOTE_USER.update(environ, auth_result['username']) REMOTE_USER.update(environ, auth_result["username"])
username = auth_result['username'] username = auth_result["username"]
plugin = auth_result.get('auth_data', {}).get('_plugin') plugin = auth_result.get("auth_data", {}).get("_plugin")
log.info( log.info("MAIN-AUTH successful for user `%s` from %s plugin", username, plugin)
'MAIN-AUTH successful for user `%s` from %s plugin',
username, plugin)
plugin_cache_active, cache_ttl = auth_result.get( plugin_cache_active, cache_ttl = auth_result.get("auth_data", {}).get("_ttl_cache") or (
'auth_data', {}).get('_ttl_cache') or (False, 0) False,
0,
)
else: else:
return auth_result.wsgi_application(environ, start_response) return auth_result.wsgi_application(environ, start_response)
@ -488,21 +482,24 @@ class SimpleVCS(object):
# check user attributes for password change flag # check user attributes for password change flag
user_obj = user user_obj = user
auth_user = user_obj.AuthUser() auth_user = user_obj.AuthUser()
if user_obj and user_obj.username != User.DEFAULT_USER and \ if (
user_obj.user_data.get('force_password_change'): user_obj
reason = 'password change required' and user_obj.username != User.DEFAULT_USER
log.debug('User not allowed to authenticate, %s', reason) and user_obj.user_data.get("force_password_change")
):
reason = "password change required"
log.debug("User not allowed to authenticate, %s", reason)
return HTTPNotAcceptable(reason)(environ, start_response) return HTTPNotAcceptable(reason)(environ, start_response)
# check permissions for this repository # check permissions for this repository
perm = self._check_permission( perm = self._check_permission(
action, user, auth_user, self.acl_repo_name, ip_addr, action, user, auth_user, self.acl_repo_name, ip_addr, plugin, plugin_cache_active, cache_ttl
plugin, plugin_cache_active, cache_ttl) )
if not perm: if not perm:
return HTTPForbidden()(environ, start_response) return HTTPForbidden()(environ, start_response)
environ['rc_auth_user_id'] = str(user_id) environ["rc_auth_user_id"] = str(user_id)
if action == 'push': if action == "push":
perms = auth_user.get_branch_permissions(self.acl_repo_name) perms = auth_user.get_branch_permissions(self.acl_repo_name)
if perms: if perms:
check_branch_perms = True check_branch_perms = True
@ -510,41 +507,48 @@ class SimpleVCS(object):
# extras are injected into UI object and later available # extras are injected into UI object and later available
# in hooks executed by RhodeCode # in hooks executed by RhodeCode
check_locking = _should_check_locking(environ.get('QUERY_STRING')) check_locking = _should_check_locking(environ.get("QUERY_STRING"))
extras = vcs_operation_context( extras = vcs_operation_context(
environ, repo_name=self.acl_repo_name, username=username, environ,
action=action, scm=self.SCM, check_locking=check_locking, repo_name=self.acl_repo_name,
is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms, username=username,
detect_force_push=detect_force_push action=action,
scm=self.SCM,
check_locking=check_locking,
is_shadow_repo=self.is_shadow_repo,
check_branch_perms=check_branch_perms,
detect_force_push=detect_force_push,
) )
# ====================================================================== # ======================================================================
# REQUEST HANDLING # REQUEST HANDLING
# ====================================================================== # ======================================================================
repo_path = os.path.join( repo_path = os.path.join(safe_str(self.base_path), safe_str(self.vcs_repo_name))
safe_str(self.base_path), safe_str(self.vcs_repo_name)) log.debug("Repository path is %s", repo_path)
log.debug('Repository path is %s', repo_path)
fix_PATH() fix_PATH()
log.info( log.info(
'%s action on %s repo "%s" by "%s" from %s %s', '%s action on %s repo "%s" by "%s" from %s %s',
action, self.SCM, safe_str(self.url_repo_name), action,
safe_str(username), ip_addr, user_agent) self.SCM,
safe_str(self.url_repo_name),
safe_str(username),
ip_addr,
user_agent,
)
return self._generate_vcs_response( return self._generate_vcs_response(environ, start_response, repo_path, extras, action)
environ, start_response, repo_path, extras, action)
def _get_txn_id(self, environ): def _get_txn_id(self, environ):
for k in ["RAW_URI", "HTTP_DESTINATION"]:
for k in ['RAW_URI', 'HTTP_DESTINATION']:
url = environ.get(k) url = environ.get(k)
if not url: if not url:
continue continue
# regex to search for svn-txn-id # regex to search for svn-txn-id
pattern = r'/!svn/txr/([^/]+)/' pattern = r"/!svn/txr/([^/]+)/"
# Search for the pattern in the URL # Search for the pattern in the URL
match = re.search(pattern, url) match = re.search(pattern, url)
@ -555,8 +559,7 @@ class SimpleVCS(object):
return txn_id return txn_id
@initialize_generator @initialize_generator
def _generate_vcs_response( def _generate_vcs_response(self, environ, start_response, repo_path, extras, action):
self, environ, start_response, repo_path, extras, action):
""" """
Returns a generator for the response content. Returns a generator for the response content.
@ -565,24 +568,20 @@ class SimpleVCS(object):
also handles the locking exceptions which will be triggered when also handles the locking exceptions which will be triggered when
the first chunk is produced by the underlying WSGI application. the first chunk is produced by the underlying WSGI application.
""" """
svn_txn_id = '' svn_txn_id = ""
if action == 'push': if action == "push":
svn_txn_id = self._get_txn_id(environ) svn_txn_id = self._get_txn_id(environ)
callback_daemon, extras = self._prepare_callback_daemon( callback_daemon, extras = self._prepare_callback_daemon(extras, environ, action, txn_id=svn_txn_id)
extras, environ, action, txn_id=svn_txn_id)
if svn_txn_id: if svn_txn_id:
port = safe_int(extras['hooks_uri'].split(':')[-1])
txn_id_data = extras.copy() txn_id_data = extras.copy()
txn_id_data.update({'port': port}) txn_id_data.update({"req_method": environ["REQUEST_METHOD"]})
txn_id_data.update({'req_method': environ['REQUEST_METHOD']})
full_repo_path = repo_path full_repo_path = repo_path
store_txn_id_data(full_repo_path, svn_txn_id, txn_id_data) store_txn_id_data(full_repo_path, svn_txn_id, txn_id_data)
log.debug('HOOKS extras is %s', extras) log.debug("HOOKS extras is %s", extras)
http_scheme = self._get_http_scheme(environ) http_scheme = self._get_http_scheme(environ)
@ -609,7 +608,7 @@ class SimpleVCS(object):
try: try:
# invalidate cache on push # invalidate cache on push
if action == 'push': if action == "push":
self._invalidate_cache(self.url_repo_name) self._invalidate_cache(self.url_repo_name)
finally: finally:
meta.Session.remove() meta.Session.remove()
@ -632,12 +631,12 @@ class SimpleVCS(object):
"""Return the WSGI app that will finally handle the request.""" """Return the WSGI app that will finally handle the request."""
raise NotImplementedError() raise NotImplementedError()
def _create_config(self, extras, repo_name, scheme='http'): def _create_config(self, extras, repo_name, scheme="http"):
"""Create a safe config representation.""" """Create a safe config representation."""
raise NotImplementedError() raise NotImplementedError()
def _should_use_callback_daemon(self, extras, environ, action): def _should_use_callback_daemon(self, extras, environ, action):
if extras.get('is_shadow_repo'): if extras.get("is_shadow_repo"):
# we don't want to execute hooks, and callback daemon for shadow repos # we don't want to execute hooks, and callback daemon for shadow repos
return False return False
return True return True
@ -647,11 +646,9 @@ class SimpleVCS(object):
if not self._should_use_callback_daemon(extras, environ, action): if not self._should_use_callback_daemon(extras, environ, action):
# disable callback daemon for actions that don't require it # disable callback daemon for actions that don't require it
protocol = 'local' protocol = "local"
return prepare_callback_daemon( return prepare_callback_daemon(extras, protocol=protocol, txn_id=txn_id)
extras, protocol=protocol,
host=vcs_settings.HOOKS_HOST, txn_id=txn_id)
def _should_check_locking(query_string): def _should_check_locking(query_string):
@ -659,4 +656,4 @@ def _should_check_locking(query_string):
# server see all operation on commit; bookmarks, phases and # server see all operation on commit; bookmarks, phases and
# obsolescence marker in different transaction, we don't want to check # obsolescence marker in different transaction, we don't want to check
# locking on those # locking on those
return query_string not in ['cmd=listkeys'] return query_string not in ["cmd=listkeys"]

View file

@ -21,6 +21,7 @@ Utilities library for RhodeCode
""" """
import datetime import datetime
import importlib
import decorator import decorator
import logging import logging
@ -42,8 +43,9 @@ from webhelpers2.text import collapse, strip_tags, convert_accented_entities, co
from mako import exceptions from mako import exceptions
import rhodecode
from rhodecode import ConfigGet from rhodecode import ConfigGet
from rhodecode.lib.exceptions import HTTPBranchProtected, HTTPLockedRC from rhodecode.lib.exceptions import HTTPBranchProtected, HTTPLockedRepo, ClientNotSupported
from rhodecode.lib.hash_utils import sha256_safe, md5, sha1 from rhodecode.lib.hash_utils import sha256_safe, md5, sha1
from rhodecode.lib.type_utils import AttributeDict from rhodecode.lib.type_utils import AttributeDict
from rhodecode.lib.str_utils import safe_bytes, safe_str from rhodecode.lib.str_utils import safe_bytes, safe_str
@ -86,6 +88,7 @@ def adopt_for_celery(func):
@wraps(func) @wraps(func)
def wrapper(extras): def wrapper(extras):
extras = AttributeDict(extras) extras = AttributeDict(extras)
try: try:
# HooksResponse implements to_json method which must be used there. # HooksResponse implements to_json method which must be used there.
return func(extras).to_json() return func(extras).to_json()
@ -100,7 +103,18 @@ def adopt_for_celery(func):
'exception_args': error_args, 'exception_args': error_args,
'exception_traceback': '', 'exception_traceback': '',
} }
except HTTPLockedRC as error: except ClientNotSupported as error:
# Those special cases don't need error reporting. It's a case of
# locked repo or protected branch
error_args = error.args
return {
'status': error.code,
'output': error.explanation,
'exception': type(error).__name__,
'exception_args': error_args,
'exception_traceback': '',
}
except HTTPLockedRepo as error:
# Those special cases don't need error reporting. It's a case of # Those special cases don't need error reporting. It's a case of
# locked repo or protected branch # locked repo or protected branch
error_args = error.args error_args = error.args
@ -117,7 +131,7 @@ def adopt_for_celery(func):
'output': '', 'output': '',
'exception': type(e).__name__, 'exception': type(e).__name__,
'exception_args': e.args, 'exception_args': e.args,
'exception_traceback': '', 'exception_traceback': traceback.format_exc(),
} }
return wrapper return wrapper
@ -411,6 +425,10 @@ def prepare_config_data(clear_session=True, repo=None):
('web', 'push_ssl', 'false'), ('web', 'push_ssl', 'false'),
] ]
for setting in ui_settings: for setting in ui_settings:
# skip certain deprecated keys that might be still in DB
if f"{setting.section}_{setting.key}" in ['extensions_hgsubversion']:
continue
# Todo: remove this section once transition to *.ini files will be completed # Todo: remove this section once transition to *.ini files will be completed
if setting.section in ('largefiles', 'vcs_git_lfs'): if setting.section in ('largefiles', 'vcs_git_lfs'):
if setting.key != 'enabled': if setting.key != 'enabled':
@ -686,22 +704,41 @@ def repo2db_mapper(initial_repo_list, remove_obsolete=False, force_hooks_rebuild
return added, removed return added, removed
def deep_reload_package(package_name):
"""
Deeply reload a package by removing it and its submodules from sys.modules,
then re-importing it.
"""
# Remove the package and its submodules from sys.modules
to_reload = [name for name in sys.modules if name == package_name or name.startswith(package_name + ".")]
for module_name in to_reload:
del sys.modules[module_name]
log.debug(f"Removed module from cache: {module_name}")
# Re-import the package
package = importlib.import_module(package_name)
log.debug(f"Re-imported package: {package_name}")
return package
def load_rcextensions(root_path): def load_rcextensions(root_path):
import rhodecode import rhodecode
from rhodecode.config import conf from rhodecode.config import conf
path = os.path.join(root_path) path = os.path.join(root_path)
sys.path.append(path) deep_reload = path in sys.path
sys.path.insert(0, path)
try: try:
rcextensions = __import__('rcextensions') rcextensions = __import__('rcextensions', fromlist=[''])
except ImportError: except ImportError:
if os.path.isdir(os.path.join(path, 'rcextensions')): if os.path.isdir(os.path.join(path, 'rcextensions')):
log.warning('Unable to load rcextensions from %s', path) log.warning('Unable to load rcextensions from %s', path)
rcextensions = None rcextensions = None
if rcextensions: if rcextensions:
if deep_reload:
rcextensions = deep_reload_package('rcextensions')
log.info('Loaded rcextensions from %s...', rcextensions) log.info('Loaded rcextensions from %s...', rcextensions)
rhodecode.EXTENSIONS = rcextensions rhodecode.EXTENSIONS = rcextensions
@ -741,6 +778,7 @@ def create_test_index(repo_location, config):
except ImportError: except ImportError:
raise ImportError('Failed to import rc_testdata, ' raise ImportError('Failed to import rc_testdata, '
'please make sure this package is installed from requirements_test.txt') 'please make sure this package is installed from requirements_test.txt')
rc_testdata.extract_search_index( rc_testdata.extract_search_index(
'vcs_search_index', os.path.dirname(config['search.location'])) 'vcs_search_index', os.path.dirname(config['search.location']))
@ -785,22 +823,15 @@ def create_test_repositories(test_path, config):
Creates test repositories in the temporary directory. Repositories are Creates test repositories in the temporary directory. Repositories are
extracted from archives within the rc_testdata package. extracted from archives within the rc_testdata package.
""" """
import rc_testdata try:
import rc_testdata
except ImportError:
raise ImportError('Failed to import rc_testdata, '
'please make sure this package is installed from requirements_test.txt')
from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
log.debug('making test vcs repositories') log.debug('making test vcs repositories at %s', test_path)
idx_path = config['search.location']
data_path = config['cache_dir']
# clean index and data
if idx_path and os.path.exists(idx_path):
log.debug('remove %s', idx_path)
shutil.rmtree(idx_path)
if data_path and os.path.exists(data_path):
log.debug('remove %s', data_path)
shutil.rmtree(data_path)
rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO)) rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO)) rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))

View file

@ -140,7 +140,7 @@ class CurlSession(object):
try: try:
curl.perform() curl.perform()
except pycurl.error as exc: except pycurl.error as exc:
log.error('Failed to call endpoint url: {} using pycurl'.format(url)) log.error('Failed to call endpoint url: %s using pycurl', url)
raise raise
status_code = curl.getinfo(pycurl.HTTP_CODE) status_code = curl.getinfo(pycurl.HTTP_CODE)

View file

@ -45,10 +45,3 @@ def discover_git_version(raise_on_exc=False):
if raise_on_exc: if raise_on_exc:
raise raise
return '' return ''
def lfs_store(base_location):
"""
Return a lfs store relative to base_location
"""
return os.path.join(base_location, '.cache', 'lfs_store')

View file

@ -45,10 +45,3 @@ def discover_hg_version(raise_on_exc=False):
if raise_on_exc: if raise_on_exc:
raise raise
return '' return ''
def largefiles_store(base_location):
"""
Return a largefile store relative to base_location
"""
return os.path.join(base_location, '.cache', 'largefiles')

View file

@ -216,7 +216,7 @@ class RemoteRepo(object):
self._cache_region, self._cache_namespace = \ self._cache_region, self._cache_namespace = \
remote_maker.init_cache_region(cache_repo_id) remote_maker.init_cache_region(cache_repo_id)
with_wire = with_wire or {} with_wire = with_wire or {"cache": False}
repo_state_uid = with_wire.get('repo_state_uid') or 'state' repo_state_uid = with_wire.get('repo_state_uid') or 'state'

View file

@ -373,6 +373,7 @@ class CommentsModel(BaseModel):
Session().add(comment) Session().add(comment)
Session().flush() Session().flush()
kwargs = { kwargs = {
'user': user, 'user': user,
'renderer_type': renderer, 'renderer_type': renderer,
@ -387,8 +388,7 @@ class CommentsModel(BaseModel):
} }
if commit_obj: if commit_obj:
recipients = ChangesetComment.get_users( recipients = ChangesetComment.get_users(revision=commit_obj.raw_id)
revision=commit_obj.raw_id)
# add commit author if it's in RhodeCode system # add commit author if it's in RhodeCode system
cs_author = User.get_from_cs_author(commit_obj.author) cs_author = User.get_from_cs_author(commit_obj.author)
if not cs_author: if not cs_author:
@ -397,16 +397,13 @@ class CommentsModel(BaseModel):
recipients += [cs_author] recipients += [cs_author]
commit_comment_url = self.get_url(comment, request=request) commit_comment_url = self.get_url(comment, request=request)
commit_comment_reply_url = self.get_url( commit_comment_reply_url = self.get_url(comment, request=request, anchor=f'comment-{comment.comment_id}/?/ReplyToComment')
comment, request=request,
anchor=f'comment-{comment.comment_id}/?/ReplyToComment')
target_repo_url = h.link_to( target_repo_url = h.link_to(
repo.repo_name, repo.repo_name,
h.route_url('repo_summary', repo_name=repo.repo_name)) h.route_url('repo_summary', repo_name=repo.repo_name))
commit_url = h.route_url('repo_commit', repo_name=repo.repo_name, commit_url = h.route_url('repo_commit', repo_name=repo.repo_name, commit_id=commit_id)
commit_id=commit_id)
# commit specifics # commit specifics
kwargs.update({ kwargs.update({
@ -489,7 +486,6 @@ class CommentsModel(BaseModel):
if not is_draft: if not is_draft:
comment_data = comment.get_api_data() comment_data = comment.get_api_data()
self._log_audit_action( self._log_audit_action(
action, {'data': comment_data}, auth_user, comment) action, {'data': comment_data}, auth_user, comment)

View file

@ -38,7 +38,7 @@ from rhodecode.translation import lazy_ugettext
from rhodecode.lib import helpers as h, hooks_utils, diffs from rhodecode.lib import helpers as h, hooks_utils, diffs
from rhodecode.lib import audit_logger from rhodecode.lib import audit_logger
from collections import OrderedDict from collections import OrderedDict
from rhodecode.lib.hook_daemon.base import prepare_callback_daemon from rhodecode.lib.hook_daemon.utils import prepare_callback_daemon
from rhodecode.lib.ext_json import sjson as json from rhodecode.lib.ext_json import sjson as json
from rhodecode.lib.markup_renderer import ( from rhodecode.lib.markup_renderer import (
DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer) DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
@ -980,9 +980,7 @@ class PullRequestModel(BaseModel):
target_ref = self._refresh_reference( target_ref = self._refresh_reference(
pull_request.target_ref_parts, target_vcs) pull_request.target_ref_parts, target_vcs)
callback_daemon, extras = prepare_callback_daemon( callback_daemon, extras = prepare_callback_daemon(extras, protocol=vcs_settings.HOOKS_PROTOCOL)
extras, protocol=vcs_settings.HOOKS_PROTOCOL,
host=vcs_settings.HOOKS_HOST)
with callback_daemon: with callback_daemon:
# TODO: johbo: Implement a clean way to run a config_override # TODO: johbo: Implement a clean way to run a config_override

View file

@ -862,27 +862,3 @@ class VcsSettingsModel(object):
raise ValueError( raise ValueError(
f'The given data does not contain {data_key} key') f'The given data does not contain {data_key} key')
return data_keys return data_keys
def create_largeobjects_dirs_if_needed(self, repo_store_path):
"""
This is subscribed to the `pyramid.events.ApplicationCreated` event. It
does a repository scan if enabled in the settings.
"""
from rhodecode.lib.vcs.backends.hg import largefiles_store
from rhodecode.lib.vcs.backends.git import lfs_store
paths = [
largefiles_store(repo_store_path),
lfs_store(repo_store_path)]
for path in paths:
if os.path.isdir(path):
continue
if os.path.isfile(path):
continue
# not a file nor dir, we try to create it
try:
os.makedirs(path)
except Exception:
log.warning('Failed to create largefiles dir:%s', path)

View file

@ -1,5 +1,4 @@
# Copyright (C) 2010-2024 RhodeCode GmbH
# Copyright (C) 2010-2023 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3 # it under the terms of the GNU Affero General Public License, version 3
@ -38,7 +37,7 @@ from rhodecode.lib.hash_utils import sha1_safe
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
__all__ = [ __all__ = [
'get_new_dir', 'TestController', 'get_new_dir', 'TestController', 'console_printer',
'clear_cache_regions', 'clear_cache_regions',
'assert_session_flash', 'login_user', 'no_newline_id_generator', 'assert_session_flash', 'login_user', 'no_newline_id_generator',
'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'SVN_REPO', 'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'SVN_REPO',
@ -244,3 +243,11 @@ def no_newline_id_generator(test_name):
return test_name or 'test-with-empty-name' return test_name or 'test-with-empty-name'
def console_printer(*msg):
print_func = print
try:
from rich import print as print_func
except ImportError:
pass
print_func(*msg)

View file

@ -1,5 +1,4 @@
# Copyright (C) 2010-2024 RhodeCode GmbH
# Copyright (C) 2010-2023 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3 # it under the terms of the GNU Affero General Public License, version 3
@ -90,7 +89,7 @@ class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
'firstname': firstname, 'firstname': firstname,
'lastname': lastname, 'lastname': lastname,
'groups': [], 'groups': [],
'email': '%s@rhodecode.com' % username, 'email': f'{username}@rhodecode.com',
'admin': admin, 'admin': admin,
'active': active, 'active': active,
"active_from_extern": None, "active_from_extern": None,

View file

@ -20,14 +20,14 @@
import pytest import pytest
import requests import requests
from rhodecode.config import routing_links from rhodecode.config import routing_links
from rhodecode.tests import console_printer
def check_connection(): def check_connection():
try: try:
response = requests.get('https://rhodecode.com') response = requests.get('https://rhodecode.com')
return response.status_code == 200 return response.status_code == 200
except Exception as e: except Exception as e:
print(e) console_printer(e)
return False return False

View file

@ -1,4 +1,4 @@
# Copyright (C) 2010-2023 RhodeCode GmbH # Copyright (C) 2010-2024 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3 # it under the terms of the GNU Affero General Public License, version 3
@ -16,23 +16,10 @@
# RhodeCode Enterprise Edition, including its added features, Support services, # RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/ # and proprietary license terms, please see https://rhodecode.com/licenses/
""" import pytest # noqa
py.test config for test suite for making push/pull operations.
.. important::
You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
to redirect things to stderr instead of stdout.
"""
import pytest
import logging import logging
import collections
from rhodecode.authentication import AuthenticationPluginRegistry import rhodecode
from rhodecode.model.db import Permission, User
from rhodecode.model.meta import Session
from rhodecode.model.settings import SettingsModel
from rhodecode.model.user import UserModel
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -40,99 +27,3 @@ log = logging.getLogger(__name__)
# Docker image running httpbin... # Docker image running httpbin...
HTTPBIN_DOMAIN = 'http://httpbin' HTTPBIN_DOMAIN = 'http://httpbin'
HTTPBIN_POST = HTTPBIN_DOMAIN + '/post' HTTPBIN_POST = HTTPBIN_DOMAIN + '/post'
@pytest.fixture()
def enable_auth_plugins(request, baseapp, csrf_token):
"""
Return a factory object that when called, allows to control which
authentication plugins are enabled.
"""
class AuthPluginManager(object):
def cleanup(self):
self._enable_plugins(['egg:rhodecode-enterprise-ce#rhodecode'])
def enable(self, plugins_list, override=None):
return self._enable_plugins(plugins_list, override)
def _enable_plugins(self, plugins_list, override=None):
override = override or {}
params = {
'auth_plugins': ','.join(plugins_list),
}
# helper translate some names to others, to fix settings code
name_map = {
'token': 'authtoken'
}
log.debug('enable_auth_plugins: enabling following auth-plugins: %s', plugins_list)
for module in plugins_list:
plugin_name = module.partition('#')[-1]
if plugin_name in name_map:
plugin_name = name_map[plugin_name]
enabled_plugin = f'auth_{plugin_name}_enabled'
cache_ttl = f'auth_{plugin_name}_cache_ttl'
# default params that are needed for each plugin,
# `enabled` and `cache_ttl`
params.update({
enabled_plugin: True,
cache_ttl: 0
})
if override.get:
params.update(override.get(module, {}))
validated_params = params
for k, v in validated_params.items():
setting = SettingsModel().create_or_update_setting(k, v)
Session().add(setting)
Session().commit()
AuthenticationPluginRegistry.invalidate_auth_plugins_cache(hard=True)
enabled_plugins = SettingsModel().get_auth_plugins()
assert plugins_list == enabled_plugins
enabler = AuthPluginManager()
request.addfinalizer(enabler.cleanup)
return enabler
@pytest.fixture()
def test_user_factory(request, baseapp):
def user_factory(username='test_user', password='qweqwe', first_name='John', last_name='Testing', **kwargs):
usr = UserModel().create_or_update(
username=username,
password=password,
email=f'{username}@rhodecode.org',
firstname=first_name, lastname=last_name)
Session().commit()
for k, v in kwargs.items():
setattr(usr, k, v)
Session().add(usr)
new_usr = User.get_by_username(username)
new_usr_id = new_usr.user_id
assert new_usr == usr
@request.addfinalizer
def cleanup():
if User.get(new_usr_id) is None:
return
perm = Permission.query().all()
for p in perm:
UserModel().revoke_perm(usr, p)
UserModel().delete(new_usr_id)
Session().commit()
return usr
return user_factory

View file

@ -1,4 +1,4 @@
# Copyright (C) 2010-2023 RhodeCode GmbH # Copyright (C) 2010-2024 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3 # it under the terms of the GNU Affero General Public License, version 3
@ -98,16 +98,16 @@ def pytest_addoption(parser):
'pyramid_config', 'pyramid_config',
"Set up a Pyramid environment with the specified config file.") "Set up a Pyramid environment with the specified config file.")
parser.addini('rhodecode_config', 'rhodecode config ini for tests')
parser.addini('celery_config', 'celery config ini for tests')
parser.addini('vcsserver_config', 'vcsserver config ini for tests')
vcsgroup = parser.getgroup('vcs') vcsgroup = parser.getgroup('vcs')
vcsgroup.addoption( vcsgroup.addoption(
'--without-vcsserver', dest='with_vcsserver', action='store_false', '--without-vcsserver', dest='with_vcsserver', action='store_false',
help="Do not start the VCSServer in a background process.") help="Do not start the VCSServer in a background process.")
vcsgroup.addoption(
'--with-vcsserver-http', dest='vcsserver_config_http',
help="Start the HTTP VCSServer with the specified config file.")
vcsgroup.addoption(
'--vcsserver-protocol', dest='vcsserver_protocol',
help="Start the VCSServer with HTTP protocol support.")
vcsgroup.addoption( vcsgroup.addoption(
'--vcsserver-config-override', action='store', type=_parse_json, '--vcsserver-config-override', action='store', type=_parse_json,
default=None, dest='vcsserver_config_override', help=( default=None, dest='vcsserver_config_override', help=(
@ -122,12 +122,6 @@ def pytest_addoption(parser):
"Allows to set the port of the vcsserver. Useful when testing " "Allows to set the port of the vcsserver. Useful when testing "
"against an already running server and random ports cause " "against an already running server and random ports cause "
"trouble.")) "trouble."))
parser.addini(
'vcsserver_config_http',
"Start the HTTP VCSServer with the specified config file.")
parser.addini(
'vcsserver_protocol',
"Start the VCSServer with HTTP protocol support.")
@pytest.hookimpl(tryfirst=True, hookwrapper=True) @pytest.hookimpl(tryfirst=True, hookwrapper=True)

View file

@ -0,0 +1,17 @@
# Copyright (C) 2010-2023 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# 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 Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/

View file

@ -1,4 +1,3 @@
# Copyright (C) 2010-2023 RhodeCode GmbH # Copyright (C) 2010-2023 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@ -17,7 +16,7 @@
# RhodeCode Enterprise Edition, including its added features, Support services, # RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/ # and proprietary license terms, please see https://rhodecode.com/licenses/
from subprocess import Popen, PIPE import subprocess
import os import os
import sys import sys
import tempfile import tempfile
@ -26,87 +25,71 @@ import pytest
from sqlalchemy.engine import url from sqlalchemy.engine import url
from rhodecode.lib.str_utils import safe_str, safe_bytes from rhodecode.lib.str_utils import safe_str, safe_bytes
from rhodecode.tests.fixture import TestINI from rhodecode.tests.fixtures.rc_fixture import TestINI
def _get_dbs_from_metafunc(metafunc): def _get_dbs_from_metafunc(metafunc):
dbs_mark = metafunc.definition.get_closest_marker('dbs') dbs_mark = metafunc.definition.get_closest_marker("dbs")
if dbs_mark: if dbs_mark:
# Supported backends by this test function, created from pytest.mark.dbs # Supported backends by this test function, created from pytest.mark.dbs
backends = dbs_mark.args backends = dbs_mark.args
else: else:
backends = metafunc.config.getoption('--dbs') backends = metafunc.config.getoption("--dbs")
return backends return backends
def pytest_generate_tests(metafunc): def pytest_generate_tests(metafunc):
# Support test generation based on --dbs parameter # Support test generation based on --dbs parameter
if 'db_backend' in metafunc.fixturenames: if "db_backend" in metafunc.fixturenames:
requested_backends = set(metafunc.config.getoption('--dbs')) requested_backends = set(metafunc.config.getoption("--dbs"))
backends = _get_dbs_from_metafunc(metafunc) backends = _get_dbs_from_metafunc(metafunc)
backends = requested_backends.intersection(backends) backends = requested_backends.intersection(backends)
# TODO: johbo: Disabling a backend did not work out with # TODO: johbo: Disabling a backend did not work out with
# parametrization, find better way to achieve this. # parametrization, find better way to achieve this.
if not backends: if not backends:
metafunc.function._skip = True metafunc.function._skip = True
metafunc.parametrize('db_backend_name', backends) metafunc.parametrize("db_backend_name", backends)
def pytest_collection_modifyitems(session, config, items): def pytest_collection_modifyitems(session, config, items):
remaining = [ remaining = [i for i in items if not getattr(i.obj, "_skip", False)]
i for i in items if not getattr(i.obj, '_skip', False)]
items[:] = remaining items[:] = remaining
@pytest.fixture() @pytest.fixture()
def db_backend( def db_backend(request, db_backend_name, ini_config, tmpdir_factory):
request, db_backend_name, ini_config, tmpdir_factory):
basetemp = tmpdir_factory.getbasetemp().strpath basetemp = tmpdir_factory.getbasetemp().strpath
klass = _get_backend(db_backend_name) klass = _get_backend(db_backend_name)
option_name = '--{}-connection-string'.format(db_backend_name) option_name = "--{}-connection-string".format(db_backend_name)
connection_string = request.config.getoption(option_name) or None connection_string = request.config.getoption(option_name) or None
return klass( return klass(config_file=ini_config, basetemp=basetemp, connection_string=connection_string)
config_file=ini_config, basetemp=basetemp,
connection_string=connection_string)
def _get_backend(backend_type): def _get_backend(backend_type):
return { return {"sqlite": SQLiteDBBackend, "postgres": PostgresDBBackend, "mysql": MySQLDBBackend, "": EmptyDBBackend}[
'sqlite': SQLiteDBBackend, backend_type
'postgres': PostgresDBBackend, ]
'mysql': MySQLDBBackend,
'': EmptyDBBackend
}[backend_type]
class DBBackend(object): class DBBackend(object):
_store = os.path.dirname(os.path.abspath(__file__)) _store = os.path.dirname(os.path.abspath(__file__))
_type = None _type = None
_base_ini_config = [{'app:main': {'vcs.start_server': 'false', _base_ini_config = [{"app:main": {"vcs.start_server": "false", "startup.import_repos": "false"}}]
'startup.import_repos': 'false'}}] _db_url = [{"app:main": {"sqlalchemy.db1.url": ""}}]
_db_url = [{'app:main': {'sqlalchemy.db1.url': ''}}] _base_db_name = "rhodecode_test_db_backend"
_base_db_name = 'rhodecode_test_db_backend' std_env = {"RC_TEST": "0"}
std_env = {'RC_TEST': '0'}
def __init__(
self, config_file, db_name=None, basetemp=None,
connection_string=None):
from rhodecode.lib.vcs.backends.hg import largefiles_store
from rhodecode.lib.vcs.backends.git import lfs_store
def __init__(self, config_file, db_name=None, basetemp=None, connection_string=None):
self.fixture_store = os.path.join(self._store, self._type) self.fixture_store = os.path.join(self._store, self._type)
self.db_name = db_name or self._base_db_name self.db_name = db_name or self._base_db_name
self._base_ini_file = config_file self._base_ini_file = config_file
self.stderr = '' self.stderr = ""
self.stdout = '' self.stdout = ""
self._basetemp = basetemp or tempfile.gettempdir() self._basetemp = basetemp or tempfile.gettempdir()
self._repos_location = os.path.join(self._basetemp, 'rc_test_repos') self._repos_location = os.path.join(self._basetemp, "rc_test_repos")
self._repos_hg_largefiles_store = largefiles_store(self._basetemp)
self._repos_git_lfs_store = lfs_store(self._basetemp)
self.connection_string = connection_string self.connection_string = connection_string
@property @property
@ -118,8 +101,7 @@ class DBBackend(object):
if not new_connection_string: if not new_connection_string:
new_connection_string = self.get_default_connection_string() new_connection_string = self.get_default_connection_string()
else: else:
new_connection_string = new_connection_string.format( new_connection_string = new_connection_string.format(db_name=self.db_name)
db_name=self.db_name)
url_parts = url.make_url(new_connection_string) url_parts = url.make_url(new_connection_string)
self._connection_string = new_connection_string self._connection_string = new_connection_string
self.user = url_parts.username self.user = url_parts.username
@ -127,73 +109,67 @@ class DBBackend(object):
self.host = url_parts.host self.host = url_parts.host
def get_default_connection_string(self): def get_default_connection_string(self):
raise NotImplementedError('default connection_string is required.') raise NotImplementedError("default connection_string is required.")
def execute(self, cmd, env=None, *args): def execute(self, cmd, env=None, *args):
""" """
Runs command on the system with given ``args``. Runs command on the system with given ``args``.
""" """
command = cmd + ' ' + ' '.join(args) command = cmd + " " + " ".join(args)
sys.stdout.write(f'CMD: {command}') sys.stdout.write(f"CMD: {command}")
# Tell Python to use UTF-8 encoding out stdout # Tell Python to use UTF-8 encoding out stdout
_env = os.environ.copy() _env = os.environ.copy()
_env['PYTHONIOENCODING'] = 'UTF-8' _env["PYTHONIOENCODING"] = "UTF-8"
_env.update(self.std_env) _env.update(self.std_env)
if env: if env:
_env.update(env) _env.update(env)
self.p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, env=_env) self.p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=_env)
self.stdout, self.stderr = self.p.communicate() self.stdout, self.stderr = self.p.communicate()
stdout_str = safe_str(self.stdout) stdout_str = safe_str(self.stdout)
sys.stdout.write(f'COMMAND:{command}\n') sys.stdout.write(f"COMMAND:{command}\n")
sys.stdout.write(stdout_str) sys.stdout.write(stdout_str)
return self.stdout, self.stderr return self.stdout, self.stderr
def assert_returncode_success(self): def assert_returncode_success(self):
from rich import print as pprint from rich import print as pprint
if not self.p.returncode == 0: if not self.p.returncode == 0:
pprint(safe_str(self.stderr)) pprint(safe_str(self.stderr))
raise AssertionError(f'non 0 retcode:{self.p.returncode}') raise AssertionError(f"non 0 retcode:{self.p.returncode}")
def assert_correct_output(self, stdout, version): def assert_correct_output(self, stdout, version):
assert b'UPGRADE FOR STEP %b COMPLETED' % safe_bytes(version) in stdout assert b"UPGRADE FOR STEP %b COMPLETED" % safe_bytes(version) in stdout
def setup_rhodecode_db(self, ini_params=None, env=None): def setup_rhodecode_db(self, ini_params=None, env=None):
if not ini_params: if not ini_params:
ini_params = self._base_ini_config ini_params = self._base_ini_config
ini_params.extend(self._db_url) ini_params.extend(self._db_url)
with TestINI(self._base_ini_file, ini_params, with TestINI(self._base_ini_file, ini_params, self._type, destroy=True) as _ini_file:
self._type, destroy=True) as _ini_file:
if not os.path.isdir(self._repos_location): if not os.path.isdir(self._repos_location):
os.makedirs(self._repos_location) os.makedirs(self._repos_location)
if not os.path.isdir(self._repos_hg_largefiles_store):
os.makedirs(self._repos_hg_largefiles_store)
if not os.path.isdir(self._repos_git_lfs_store):
os.makedirs(self._repos_git_lfs_store)
return self.execute( return self.execute(
"rc-setup-app {0} --user=marcink " "rc-setup-app {0} --user=marcink "
"--email=marcin@rhodeocode.com --password={1} " "--email=marcin@rhodeocode.com --password={1} "
"--repos={2} --force-yes".format( "--repos={2} --force-yes".format(_ini_file, "qweqwe", self._repos_location),
_ini_file, 'qweqwe', self._repos_location), env=env) env=env,
)
def upgrade_database(self, ini_params=None): def upgrade_database(self, ini_params=None):
if not ini_params: if not ini_params:
ini_params = self._base_ini_config ini_params = self._base_ini_config
ini_params.extend(self._db_url) ini_params.extend(self._db_url)
test_ini = TestINI( test_ini = TestINI(self._base_ini_file, ini_params, self._type, destroy=True)
self._base_ini_file, ini_params, self._type, destroy=True)
with test_ini as ini_file: with test_ini as ini_file:
if not os.path.isdir(self._repos_location): if not os.path.isdir(self._repos_location):
os.makedirs(self._repos_location) os.makedirs(self._repos_location)
return self.execute( return self.execute("rc-upgrade-db {0} --force-yes".format(ini_file))
"rc-upgrade-db {0} --force-yes".format(ini_file))
def setup_db(self): def setup_db(self):
raise NotImplementedError raise NotImplementedError
@ -206,7 +182,7 @@ class DBBackend(object):
class EmptyDBBackend(DBBackend): class EmptyDBBackend(DBBackend):
_type = '' _type = ""
def setup_db(self): def setup_db(self):
pass pass
@ -222,21 +198,20 @@ class EmptyDBBackend(DBBackend):
class SQLiteDBBackend(DBBackend): class SQLiteDBBackend(DBBackend):
_type = 'sqlite' _type = "sqlite"
def get_default_connection_string(self): def get_default_connection_string(self):
return 'sqlite:///{}/{}.sqlite'.format(self._basetemp, self.db_name) return "sqlite:///{}/{}.sqlite".format(self._basetemp, self.db_name)
def setup_db(self): def setup_db(self):
# dump schema for tests # dump schema for tests
# cp -v $TEST_DB_NAME # cp -v $TEST_DB_NAME
self._db_url = [{'app:main': { self._db_url = [{"app:main": {"sqlalchemy.db1.url": self.connection_string}}]
'sqlalchemy.db1.url': self.connection_string}}]
def import_dump(self, dumpname): def import_dump(self, dumpname):
dump = os.path.join(self.fixture_store, dumpname) dump = os.path.join(self.fixture_store, dumpname)
target = os.path.join(self._basetemp, '{0.db_name}.sqlite'.format(self)) target = os.path.join(self._basetemp, "{0.db_name}.sqlite".format(self))
return self.execute(f'cp -v {dump} {target}') return self.execute(f"cp -v {dump} {target}")
def teardown_db(self): def teardown_db(self):
target_db = os.path.join(self._basetemp, self.db_name) target_db = os.path.join(self._basetemp, self.db_name)
@ -244,39 +219,39 @@ class SQLiteDBBackend(DBBackend):
class MySQLDBBackend(DBBackend): class MySQLDBBackend(DBBackend):
_type = 'mysql' _type = "mysql"
def get_default_connection_string(self): def get_default_connection_string(self):
return 'mysql://root:qweqwe@127.0.0.1/{}'.format(self.db_name) return "mysql://root:qweqwe@127.0.0.1/{}".format(self.db_name)
def setup_db(self): def setup_db(self):
# dump schema for tests # dump schema for tests
# mysqldump -uroot -pqweqwe $TEST_DB_NAME # mysqldump -uroot -pqweqwe $TEST_DB_NAME
self._db_url = [{'app:main': { self._db_url = [{"app:main": {"sqlalchemy.db1.url": self.connection_string}}]
'sqlalchemy.db1.url': self.connection_string}}] return self.execute(
return self.execute("mysql -v -u{} -p{} -e 'create database '{}';'".format( "mysql -v -u{} -p{} -e 'create database '{}';'".format(self.user, self.password, self.db_name)
self.user, self.password, self.db_name)) )
def import_dump(self, dumpname): def import_dump(self, dumpname):
dump = os.path.join(self.fixture_store, dumpname) dump = os.path.join(self.fixture_store, dumpname)
return self.execute("mysql -u{} -p{} {} < {}".format( return self.execute("mysql -u{} -p{} {} < {}".format(self.user, self.password, self.db_name, dump))
self.user, self.password, self.db_name, dump))
def teardown_db(self): def teardown_db(self):
return self.execute("mysql -v -u{} -p{} -e 'drop database '{}';'".format( return self.execute(
self.user, self.password, self.db_name)) "mysql -v -u{} -p{} -e 'drop database '{}';'".format(self.user, self.password, self.db_name)
)
class PostgresDBBackend(DBBackend): class PostgresDBBackend(DBBackend):
_type = 'postgres' _type = "postgres"
def get_default_connection_string(self): def get_default_connection_string(self):
return 'postgresql://postgres:qweqwe@localhost/{}'.format(self.db_name) return "postgresql://postgres:qweqwe@localhost/{}".format(self.db_name)
def setup_db(self): def setup_db(self):
# dump schema for tests # dump schema for tests
# pg_dump -U postgres -h localhost $TEST_DB_NAME # pg_dump -U postgres -h localhost $TEST_DB_NAME
self._db_url = [{'app:main': {'sqlalchemy.db1.url': self.connection_string}}] self._db_url = [{"app:main": {"sqlalchemy.db1.url": self.connection_string}}]
cmd = f"PGPASSWORD={self.password} psql -U {self.user} -h localhost -c 'create database '{self.db_name}';'" cmd = f"PGPASSWORD={self.password} psql -U {self.user} -h localhost -c 'create database '{self.db_name}';'"
return self.execute(cmd) return self.execute(cmd)

View file

@ -1,4 +1,3 @@
# Copyright (C) 2010-2023 RhodeCode GmbH # Copyright (C) 2010-2023 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify

View file

@ -1,4 +1,3 @@
# Copyright (C) 2010-2023 RhodeCode GmbH # Copyright (C) 2010-2023 RhodeCode GmbH
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@ -21,33 +20,42 @@ import pytest
@pytest.mark.dbs("postgres") @pytest.mark.dbs("postgres")
@pytest.mark.parametrize("dumpname", [ @pytest.mark.parametrize(
'1.4.4.sql', "dumpname",
'1.5.0.sql', [
'1.6.0.sql', "1.4.4.sql",
'1.6.0_no_repo_name_index.sql', "1.5.0.sql",
]) "1.6.0.sql",
"1.6.0_no_repo_name_index.sql",
],
)
def test_migrate_postgres_db(db_backend, dumpname): def test_migrate_postgres_db(db_backend, dumpname):
_run_migration_test(db_backend, dumpname) _run_migration_test(db_backend, dumpname)
@pytest.mark.dbs("sqlite") @pytest.mark.dbs("sqlite")
@pytest.mark.parametrize("dumpname", [ @pytest.mark.parametrize(
'rhodecode.1.4.4.sqlite', "dumpname",
'rhodecode.1.4.4_with_groups.sqlite', [
'rhodecode.1.4.4_with_ldap_active.sqlite', "rhodecode.1.4.4.sqlite",
]) "rhodecode.1.4.4_with_groups.sqlite",
"rhodecode.1.4.4_with_ldap_active.sqlite",
],
)
def test_migrate_sqlite_db(db_backend, dumpname): def test_migrate_sqlite_db(db_backend, dumpname):
_run_migration_test(db_backend, dumpname) _run_migration_test(db_backend, dumpname)
@pytest.mark.dbs("mysql") @pytest.mark.dbs("mysql")
@pytest.mark.parametrize("dumpname", [ @pytest.mark.parametrize(
'1.4.4.sql', "dumpname",
'1.5.0.sql', [
'1.6.0.sql', "1.4.4.sql",
'1.6.0_no_repo_name_index.sql', "1.5.0.sql",
]) "1.6.0.sql",
"1.6.0_no_repo_name_index.sql",
],
)
def test_migrate_mysql_db(db_backend, dumpname): def test_migrate_mysql_db(db_backend, dumpname):
_run_migration_test(db_backend, dumpname) _run_migration_test(db_backend, dumpname)
@ -60,5 +68,5 @@ def _run_migration_test(db_backend, dumpname):
db_backend.import_dump(dumpname) db_backend.import_dump(dumpname)
stdout, stderr = db_backend.upgrade_database() stdout, stderr = db_backend.upgrade_database()
db_backend.assert_correct_output(stdout+stderr, version='16') db_backend.assert_correct_output(stdout + stderr, version="16")
db_backend.assert_returncode_success() db_backend.assert_returncode_success()

View file

@ -1,226 +0,0 @@
# Copyright (C) 2010-2023 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# 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 Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
from rhodecode.lib.config_utils import get_app_config
from rhodecode.tests.fixture import TestINI
from rhodecode.tests import TESTS_TMP_PATH
from rhodecode.tests.server_utils import RcVCSServer
@pytest.fixture(scope='session')
def vcsserver(request, vcsserver_port, vcsserver_factory):
"""
Session scope VCSServer.
Tests which need the VCSServer have to rely on this fixture in order
to ensure it will be running.
For specific needs, the fixture vcsserver_factory can be used. It allows to
adjust the configuration file for the test run.
Command line args:
--without-vcsserver: Allows to switch this fixture off. You have to
manually start the server.
--vcsserver-port: Will expect the VCSServer to listen on this port.
"""
if not request.config.getoption('with_vcsserver'):
return None
return vcsserver_factory(
request, vcsserver_port=vcsserver_port)
@pytest.fixture(scope='session')
def vcsserver_factory(tmpdir_factory):
"""
Use this if you need a running vcsserver with a special configuration.
"""
def factory(request, overrides=(), vcsserver_port=None,
log_file=None, workers='3'):
if vcsserver_port is None:
vcsserver_port = get_available_port()
overrides = list(overrides)
overrides.append({'server:main': {'port': vcsserver_port}})
option_name = 'vcsserver_config_http'
override_option_name = 'vcsserver_config_override'
config_file = get_config(
request.config, option_name=option_name,
override_option_name=override_option_name, overrides=overrides,
basetemp=tmpdir_factory.getbasetemp().strpath,
prefix='test_vcs_')
server = RcVCSServer(config_file, log_file, workers)
server.start()
@request.addfinalizer
def cleanup():
server.shutdown()
server.wait_until_ready()
return server
return factory
def _use_log_level(config):
level = config.getoption('test_loglevel') or 'critical'
return level.upper()
@pytest.fixture(scope='session')
def ini_config(request, tmpdir_factory, rcserver_port, vcsserver_port):
option_name = 'pyramid_config'
log_level = _use_log_level(request.config)
overrides = [
{'server:main': {'port': rcserver_port}},
{'app:main': {
'cache_dir': '%(here)s/rc-tests/rc_data',
'vcs.server': f'localhost:{vcsserver_port}',
# johbo: We will always start the VCSServer on our own based on the
# fixtures of the test cases. For the test run it must always be
# off in the INI file.
'vcs.start_server': 'false',
'vcs.server.protocol': 'http',
'vcs.scm_app_implementation': 'http',
'vcs.svn.proxy.enabled': 'true',
'vcs.hooks.protocol.v2': 'celery',
'vcs.hooks.host': '*',
'repo_store.path': TESTS_TMP_PATH,
'app.service_api.token': 'service_secret_token',
}},
{'handler_console': {
'class': 'StreamHandler',
'args': '(sys.stderr,)',
'level': log_level,
}},
]
filename = get_config(
request.config, option_name=option_name,
override_option_name='{}_override'.format(option_name),
overrides=overrides,
basetemp=tmpdir_factory.getbasetemp().strpath,
prefix='test_rce_')
return filename
@pytest.fixture(scope='session')
def ini_settings(ini_config):
ini_path = ini_config
return get_app_config(ini_path)
def get_available_port(min_port=40000, max_port=55555):
from rhodecode.lib.utils2 import get_available_port as _get_port
return _get_port(min_port, max_port)
@pytest.fixture(scope='session')
def rcserver_port(request):
port = get_available_port()
print(f'Using rhodecode port {port}')
return port
@pytest.fixture(scope='session')
def vcsserver_port(request):
port = request.config.getoption('--vcsserver-port')
if port is None:
port = get_available_port()
print(f'Using vcsserver port {port}')
return port
@pytest.fixture(scope='session')
def available_port_factory() -> get_available_port:
"""
Returns a callable which returns free port numbers.
"""
return get_available_port
@pytest.fixture()
def available_port(available_port_factory):
"""
Gives you one free port for the current test.
Uses "available_port_factory" to retrieve the port.
"""
return available_port_factory()
@pytest.fixture(scope='session')
def testini_factory(tmpdir_factory, ini_config):
"""
Factory to create an INI file based on TestINI.
It will make sure to place the INI file in the correct directory.
"""
basetemp = tmpdir_factory.getbasetemp().strpath
return TestIniFactory(basetemp, ini_config)
class TestIniFactory(object):
def __init__(self, basetemp, template_ini):
self._basetemp = basetemp
self._template_ini = template_ini
def __call__(self, ini_params, new_file_prefix='test'):
ini_file = TestINI(
self._template_ini, ini_params=ini_params,
new_file_prefix=new_file_prefix, dir=self._basetemp)
result = ini_file.create()
return result
def get_config(
config, option_name, override_option_name, overrides=None,
basetemp=None, prefix='test'):
"""
Find a configuration file and apply overrides for the given `prefix`.
"""
config_file = (
config.getoption(option_name) or config.getini(option_name))
if not config_file:
pytest.exit(
"Configuration error, could not extract {}.".format(option_name))
overrides = overrides or []
config_override = config.getoption(override_option_name)
if config_override:
overrides.append(config_override)
temp_ini_file = TestINI(
config_file, ini_params=overrides, new_file_prefix=prefix,
dir=basetemp)
return temp_ini_file.create()

Some files were not shown because too many files have changed in this diff Show more