##// END OF EJS Templates
vcs-ops: store user_id inside the extras for vcs context operations....
marcink -
r2411:f2cd50d7 default
parent child Browse files
Show More
@@ -1,149 +1,150 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import sys
22 import sys
23 import json
23 import json
24 import logging
24 import logging
25
25
26 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
26 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
27 from rhodecode.lib.vcs.conf import settings as vcs_settings
27 from rhodecode.lib.vcs.conf import settings as vcs_settings
28 from rhodecode.model.scm import ScmModel
28 from rhodecode.model.scm import ScmModel
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 class VcsServer(object):
33 class VcsServer(object):
34 _path = None # set executable path for hg/git/svn binary
34 _path = None # set executable path for hg/git/svn binary
35 backend = None # set in child classes
35 backend = None # set in child classes
36 tunnel = None # subprocess handling tunnel
36 tunnel = None # subprocess handling tunnel
37 write_perms = ['repository.admin', 'repository.write']
37 write_perms = ['repository.admin', 'repository.write']
38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
39
39
40 def __init__(self, user, user_permissions, config, env):
40 def __init__(self, user, user_permissions, config, env):
41 self.user = user
41 self.user = user
42 self.user_permissions = user_permissions
42 self.user_permissions = user_permissions
43 self.config = config
43 self.config = config
44 self.env = env
44 self.env = env
45 self.stdin = sys.stdin
45 self.stdin = sys.stdin
46
46
47 self.repo_name = None
47 self.repo_name = None
48 self.repo_mode = None
48 self.repo_mode = None
49 self.store = ''
49 self.store = ''
50 self.ini_path = ''
50 self.ini_path = ''
51
51
52 def _invalidate_cache(self, repo_name):
52 def _invalidate_cache(self, repo_name):
53 """
53 """
54 Set's cache for this repository for invalidation on next access
54 Set's cache for this repository for invalidation on next access
55
55
56 :param repo_name: full repo name, also a cache key
56 :param repo_name: full repo name, also a cache key
57 """
57 """
58 ScmModel().mark_for_invalidation(repo_name)
58 ScmModel().mark_for_invalidation(repo_name)
59
59
60 def has_write_perm(self):
60 def has_write_perm(self):
61 permission = self.user_permissions.get(self.repo_name)
61 permission = self.user_permissions.get(self.repo_name)
62 if permission in ['repository.write', 'repository.admin']:
62 if permission in ['repository.write', 'repository.admin']:
63 return True
63 return True
64
64
65 return False
65 return False
66
66
67 def _check_permissions(self, action):
67 def _check_permissions(self, action):
68 permission = self.user_permissions.get(self.repo_name)
68 permission = self.user_permissions.get(self.repo_name)
69 log.debug(
69 log.debug(
70 'permission for %s on %s are: %s',
70 'permission for %s on %s are: %s',
71 self.user, self.repo_name, permission)
71 self.user, self.repo_name, permission)
72
72
73 if action == 'pull':
73 if action == 'pull':
74 if permission in self.read_perms:
74 if permission in self.read_perms:
75 log.info(
75 log.info(
76 'READ Permissions for User "%s" detected to repo "%s"!',
76 'READ Permissions for User "%s" detected to repo "%s"!',
77 self.user, self.repo_name)
77 self.user, self.repo_name)
78 return 0
78 return 0
79 else:
79 else:
80 if permission in self.write_perms:
80 if permission in self.write_perms:
81 log.info(
81 log.info(
82 'WRITE+ Permissions for User "%s" detected to repo "%s"!',
82 'WRITE+ Permissions for User "%s" detected to repo "%s"!',
83 self.user, self.repo_name)
83 self.user, self.repo_name)
84 return 0
84 return 0
85
85
86 log.error('Cannot properly fetch or allow user %s permissions. '
86 log.error('Cannot properly fetch or allow user %s permissions. '
87 'Return value is: %s, req action: %s',
87 'Return value is: %s, req action: %s',
88 self.user, permission, action)
88 self.user, permission, action)
89 return -2
89 return -2
90
90
91 def update_environment(self, action, extras=None):
91 def update_environment(self, action, extras=None):
92
92
93 scm_data = {
93 scm_data = {
94 'ip': os.environ['SSH_CLIENT'].split()[0],
94 'ip': os.environ['SSH_CLIENT'].split()[0],
95 'username': self.user.username,
95 'username': self.user.username,
96 'user_id': self.user.user_id,
96 'action': action,
97 'action': action,
97 'repository': self.repo_name,
98 'repository': self.repo_name,
98 'scm': self.backend,
99 'scm': self.backend,
99 'config': self.ini_path,
100 'config': self.ini_path,
100 'make_lock': None,
101 'make_lock': None,
101 'locked_by': [None, None],
102 'locked_by': [None, None],
102 'server_url': None,
103 'server_url': None,
103 'is_shadow_repo': False,
104 'is_shadow_repo': False,
104 'hooks_module': 'rhodecode.lib.hooks_daemon',
105 'hooks_module': 'rhodecode.lib.hooks_daemon',
105 'hooks': ['push', 'pull'],
106 'hooks': ['push', 'pull'],
106 'SSH': True,
107 'SSH': True,
107 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name)
108 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name)
108 }
109 }
109 if extras:
110 if extras:
110 scm_data.update(extras)
111 scm_data.update(extras)
111 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
112 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
112
113
113 def get_root_store(self):
114 def get_root_store(self):
114 root_store = self.store
115 root_store = self.store
115 if not root_store.endswith('/'):
116 if not root_store.endswith('/'):
116 # always append trailing slash
117 # always append trailing slash
117 root_store = root_store + '/'
118 root_store = root_store + '/'
118 return root_store
119 return root_store
119
120
120 def _handle_tunnel(self, extras):
121 def _handle_tunnel(self, extras):
121 # pre-auth
122 # pre-auth
122 action = 'pull'
123 action = 'pull'
123 exit_code = self._check_permissions(action)
124 exit_code = self._check_permissions(action)
124 if exit_code:
125 if exit_code:
125 return exit_code, False
126 return exit_code, False
126
127
127 req = self.env['request']
128 req = self.env['request']
128 server_url = req.host_url + req.script_name
129 server_url = req.host_url + req.script_name
129 extras['server_url'] = server_url
130 extras['server_url'] = server_url
130
131
131 log.debug('Using %s binaries from path %s', self.backend, self._path)
132 log.debug('Using %s binaries from path %s', self.backend, self._path)
132 exit_code = self.tunnel.run(extras)
133 exit_code = self.tunnel.run(extras)
133
134
134 return exit_code, action == "push"
135 return exit_code, action == "push"
135
136
136 def run(self):
137 def run(self):
137 extras = {}
138 extras = {}
138
139
139 callback_daemon, extras = prepare_callback_daemon(
140 callback_daemon, extras = prepare_callback_daemon(
140 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
141 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
141 use_direct_calls=False)
142 use_direct_calls=False)
142
143
143 with callback_daemon:
144 with callback_daemon:
144 try:
145 try:
145 return self._handle_tunnel(extras)
146 return self._handle_tunnel(extras)
146 finally:
147 finally:
147 log.debug('Running cleanup with cache invalidation')
148 log.debug('Running cleanup with cache invalidation')
148 if self.repo_name:
149 if self.repo_name:
149 self._invalidate_cache(self.repo_name)
150 self._invalidate_cache(self.repo_name)
@@ -1,145 +1,146 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import json
21 import json
22 import mock
22 import mock
23 import pytest
23 import pytest
24
24
25 from rhodecode.apps.ssh_support.lib.backends.git import GitServer
25 from rhodecode.apps.ssh_support.lib.backends.git import GitServer
26 from rhodecode.apps.ssh_support.tests.conftest import dummy_env, dummy_user
26 from rhodecode.apps.ssh_support.tests.conftest import dummy_env, dummy_user
27
27
28
28
29 class GitServerCreator(object):
29 class GitServerCreator(object):
30 root = '/tmp/repo/path/'
30 root = '/tmp/repo/path/'
31 git_path = '/usr/local/bin/git'
31 git_path = '/usr/local/bin/git'
32 config_data = {
32 config_data = {
33 'app:main': {
33 'app:main': {
34 'ssh.executable.git': git_path,
34 'ssh.executable.git': git_path,
35 'vcs.hooks.protocol': 'http',
35 'vcs.hooks.protocol': 'http',
36 }
36 }
37 }
37 }
38 repo_name = 'test_git'
38 repo_name = 'test_git'
39 repo_mode = 'receive-pack'
39 repo_mode = 'receive-pack'
40 user = dummy_user()
40 user = dummy_user()
41
41
42 def __init__(self):
42 def __init__(self):
43 def config_get(part, key):
43 def config_get(part, key):
44 return self.config_data.get(part, {}).get(key)
44 return self.config_data.get(part, {}).get(key)
45 self.config_mock = mock.Mock()
45 self.config_mock = mock.Mock()
46 self.config_mock.get = mock.Mock(side_effect=config_get)
46 self.config_mock.get = mock.Mock(side_effect=config_get)
47
47
48 def create(self, **kwargs):
48 def create(self, **kwargs):
49 parameters = {
49 parameters = {
50 'store': self.root,
50 'store': self.root,
51 'ini_path': '',
51 'ini_path': '',
52 'user': self.user,
52 'user': self.user,
53 'repo_name': self.repo_name,
53 'repo_name': self.repo_name,
54 'repo_mode': self.repo_mode,
54 'repo_mode': self.repo_mode,
55 'user_permissions': {
55 'user_permissions': {
56 self.repo_name: 'repository.admin'
56 self.repo_name: 'repository.admin'
57 },
57 },
58 'config': self.config_mock,
58 'config': self.config_mock,
59 'env': dummy_env()
59 'env': dummy_env()
60 }
60 }
61 parameters.update(kwargs)
61 parameters.update(kwargs)
62 server = GitServer(**parameters)
62 server = GitServer(**parameters)
63 return server
63 return server
64
64
65
65
66 @pytest.fixture
66 @pytest.fixture
67 def git_server(app):
67 def git_server(app):
68 return GitServerCreator()
68 return GitServerCreator()
69
69
70
70
71 class TestGitServer(object):
71 class TestGitServer(object):
72
72
73 def test_command(self, git_server):
73 def test_command(self, git_server):
74 server = git_server.create()
74 server = git_server.create()
75 expected_command = (
75 expected_command = (
76 'cd {root}; {git_path} {repo_mode} \'{root}{repo_name}\''.format(
76 'cd {root}; {git_path} {repo_mode} \'{root}{repo_name}\''.format(
77 root=git_server.root, git_path=git_server.git_path,
77 root=git_server.root, git_path=git_server.git_path,
78 repo_mode=git_server.repo_mode, repo_name=git_server.repo_name)
78 repo_mode=git_server.repo_mode, repo_name=git_server.repo_name)
79 )
79 )
80 assert expected_command == server.tunnel.command()
80 assert expected_command == server.tunnel.command()
81
81
82 @pytest.mark.parametrize('permissions, action, code', [
82 @pytest.mark.parametrize('permissions, action, code', [
83 ({}, 'pull', -2),
83 ({}, 'pull', -2),
84 ({'test_git': 'repository.read'}, 'pull', 0),
84 ({'test_git': 'repository.read'}, 'pull', 0),
85 ({'test_git': 'repository.read'}, 'push', -2),
85 ({'test_git': 'repository.read'}, 'push', -2),
86 ({'test_git': 'repository.write'}, 'push', 0),
86 ({'test_git': 'repository.write'}, 'push', 0),
87 ({'test_git': 'repository.admin'}, 'push', 0),
87 ({'test_git': 'repository.admin'}, 'push', 0),
88
88
89 ])
89 ])
90 def test_permission_checks(self, git_server, permissions, action, code):
90 def test_permission_checks(self, git_server, permissions, action, code):
91 server = git_server.create(user_permissions=permissions)
91 server = git_server.create(user_permissions=permissions)
92 result = server._check_permissions(action)
92 result = server._check_permissions(action)
93 assert result is code
93 assert result is code
94
94
95 @pytest.mark.parametrize('permissions, value', [
95 @pytest.mark.parametrize('permissions, value', [
96 ({}, False),
96 ({}, False),
97 ({'test_git': 'repository.read'}, False),
97 ({'test_git': 'repository.read'}, False),
98 ({'test_git': 'repository.write'}, True),
98 ({'test_git': 'repository.write'}, True),
99 ({'test_git': 'repository.admin'}, True),
99 ({'test_git': 'repository.admin'}, True),
100
100
101 ])
101 ])
102 def test_has_write_permissions(self, git_server, permissions, value):
102 def test_has_write_permissions(self, git_server, permissions, value):
103 server = git_server.create(user_permissions=permissions)
103 server = git_server.create(user_permissions=permissions)
104 result = server.has_write_perm()
104 result = server.has_write_perm()
105 assert result is value
105 assert result is value
106
106
107 def test_run_returns_executes_command(self, git_server):
107 def test_run_returns_executes_command(self, git_server):
108 server = git_server.create()
108 server = git_server.create()
109 from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper
109 from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper
110 with mock.patch.object(GitTunnelWrapper, 'create_hooks_env') as _patch:
110 with mock.patch.object(GitTunnelWrapper, 'create_hooks_env') as _patch:
111 _patch.return_value = 0
111 _patch.return_value = 0
112 with mock.patch.object(GitTunnelWrapper, 'command', return_value='date'):
112 with mock.patch.object(GitTunnelWrapper, 'command', return_value='date'):
113 exit_code = server.run()
113 exit_code = server.run()
114
114
115 assert exit_code == (0, False)
115 assert exit_code == (0, False)
116
116
117 @pytest.mark.parametrize(
117 @pytest.mark.parametrize(
118 'repo_mode, action', [
118 'repo_mode, action', [
119 ['receive-pack', 'push'],
119 ['receive-pack', 'push'],
120 ['upload-pack', 'pull']
120 ['upload-pack', 'pull']
121 ])
121 ])
122 def test_update_environment(self, git_server, repo_mode, action):
122 def test_update_environment(self, git_server, repo_mode, action):
123 server = git_server.create(repo_mode=repo_mode)
123 server = git_server.create(repo_mode=repo_mode)
124 with mock.patch('os.environ', {'SSH_CLIENT': '10.10.10.10 b'}):
124 with mock.patch('os.environ', {'SSH_CLIENT': '10.10.10.10 b'}):
125 with mock.patch('os.putenv') as putenv_mock:
125 with mock.patch('os.putenv') as putenv_mock:
126 server.update_environment(action)
126 server.update_environment(action)
127
127
128 expected_data = {
128 expected_data = {
129 'username': git_server.user.username,
129 'username': git_server.user.username,
130 'user_id': git_server.user.user_id,
130 'scm': 'git',
131 'scm': 'git',
131 'repository': git_server.repo_name,
132 'repository': git_server.repo_name,
132 'make_lock': None,
133 'make_lock': None,
133 'action': action,
134 'action': action,
134 'ip': '10.10.10.10',
135 'ip': '10.10.10.10',
135 'locked_by': [None, None],
136 'locked_by': [None, None],
136 'config': '',
137 'config': '',
137 'server_url': None,
138 'server_url': None,
138 'hooks': ['push', 'pull'],
139 'hooks': ['push', 'pull'],
139 'is_shadow_repo': False,
140 'is_shadow_repo': False,
140 'hooks_module': 'rhodecode.lib.hooks_daemon',
141 'hooks_module': 'rhodecode.lib.hooks_daemon',
141 'SSH': True,
142 'SSH': True,
142 'SSH_PERMISSIONS': 'repository.admin',
143 'SSH_PERMISSIONS': 'repository.admin',
143 }
144 }
144 args, kwargs = putenv_mock.call_args
145 args, kwargs = putenv_mock.call_args
145 assert json.loads(args[1]) == expected_data
146 assert json.loads(args[1]) == expected_data
@@ -1,541 +1,542 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import markupsafe
30 import markupsafe
31 import ipaddress
31 import ipaddress
32
32
33 from paste.auth.basic import AuthBasicAuthenticator
33 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36
36
37 import rhodecode
37 import rhodecode
38 from rhodecode.authentication.base import VCS_TYPE
38 from rhodecode.authentication.base import VCS_TYPE
39 from rhodecode.lib import auth, utils2
39 from rhodecode.lib import auth, utils2
40 from rhodecode.lib import helpers as h
40 from rhodecode.lib import helpers as h
41 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
41 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
42 from rhodecode.lib.exceptions import UserCreationError
42 from rhodecode.lib.exceptions import UserCreationError
43 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
43 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
44 from rhodecode.lib.utils2 import (
44 from rhodecode.lib.utils2 import (
45 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
45 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
46 from rhodecode.model.db import Repository, User, ChangesetComment
46 from rhodecode.model.db import Repository, User, ChangesetComment
47 from rhodecode.model.notification import NotificationModel
47 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
48 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 def _filter_proxy(ip):
53 def _filter_proxy(ip):
54 """
54 """
55 Passed in IP addresses in HEADERS can be in a special format of multiple
55 Passed in IP addresses in HEADERS can be in a special format of multiple
56 ips. Those comma separated IPs are passed from various proxies in the
56 ips. Those comma separated IPs are passed from various proxies in the
57 chain of request processing. The left-most being the original client.
57 chain of request processing. The left-most being the original client.
58 We only care about the first IP which came from the org. client.
58 We only care about the first IP which came from the org. client.
59
59
60 :param ip: ip string from headers
60 :param ip: ip string from headers
61 """
61 """
62 if ',' in ip:
62 if ',' in ip:
63 _ips = ip.split(',')
63 _ips = ip.split(',')
64 _first_ip = _ips[0].strip()
64 _first_ip = _ips[0].strip()
65 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
65 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
66 return _first_ip
66 return _first_ip
67 return ip
67 return ip
68
68
69
69
70 def _filter_port(ip):
70 def _filter_port(ip):
71 """
71 """
72 Removes a port from ip, there are 4 main cases to handle here.
72 Removes a port from ip, there are 4 main cases to handle here.
73 - ipv4 eg. 127.0.0.1
73 - ipv4 eg. 127.0.0.1
74 - ipv6 eg. ::1
74 - ipv6 eg. ::1
75 - ipv4+port eg. 127.0.0.1:8080
75 - ipv4+port eg. 127.0.0.1:8080
76 - ipv6+port eg. [::1]:8080
76 - ipv6+port eg. [::1]:8080
77
77
78 :param ip:
78 :param ip:
79 """
79 """
80 def is_ipv6(ip_addr):
80 def is_ipv6(ip_addr):
81 if hasattr(socket, 'inet_pton'):
81 if hasattr(socket, 'inet_pton'):
82 try:
82 try:
83 socket.inet_pton(socket.AF_INET6, ip_addr)
83 socket.inet_pton(socket.AF_INET6, ip_addr)
84 except socket.error:
84 except socket.error:
85 return False
85 return False
86 else:
86 else:
87 # fallback to ipaddress
87 # fallback to ipaddress
88 try:
88 try:
89 ipaddress.IPv6Address(safe_unicode(ip_addr))
89 ipaddress.IPv6Address(safe_unicode(ip_addr))
90 except Exception:
90 except Exception:
91 return False
91 return False
92 return True
92 return True
93
93
94 if ':' not in ip: # must be ipv4 pure ip
94 if ':' not in ip: # must be ipv4 pure ip
95 return ip
95 return ip
96
96
97 if '[' in ip and ']' in ip: # ipv6 with port
97 if '[' in ip and ']' in ip: # ipv6 with port
98 return ip.split(']')[0][1:].lower()
98 return ip.split(']')[0][1:].lower()
99
99
100 # must be ipv6 or ipv4 with port
100 # must be ipv6 or ipv4 with port
101 if is_ipv6(ip):
101 if is_ipv6(ip):
102 return ip
102 return ip
103 else:
103 else:
104 ip, _port = ip.split(':')[:2] # means ipv4+port
104 ip, _port = ip.split(':')[:2] # means ipv4+port
105 return ip
105 return ip
106
106
107
107
108 def get_ip_addr(environ):
108 def get_ip_addr(environ):
109 proxy_key = 'HTTP_X_REAL_IP'
109 proxy_key = 'HTTP_X_REAL_IP'
110 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
110 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
111 def_key = 'REMOTE_ADDR'
111 def_key = 'REMOTE_ADDR'
112 _filters = lambda x: _filter_port(_filter_proxy(x))
112 _filters = lambda x: _filter_port(_filter_proxy(x))
113
113
114 ip = environ.get(proxy_key)
114 ip = environ.get(proxy_key)
115 if ip:
115 if ip:
116 return _filters(ip)
116 return _filters(ip)
117
117
118 ip = environ.get(proxy_key2)
118 ip = environ.get(proxy_key2)
119 if ip:
119 if ip:
120 return _filters(ip)
120 return _filters(ip)
121
121
122 ip = environ.get(def_key, '0.0.0.0')
122 ip = environ.get(def_key, '0.0.0.0')
123 return _filters(ip)
123 return _filters(ip)
124
124
125
125
126 def get_server_ip_addr(environ, log_errors=True):
126 def get_server_ip_addr(environ, log_errors=True):
127 hostname = environ.get('SERVER_NAME')
127 hostname = environ.get('SERVER_NAME')
128 try:
128 try:
129 return socket.gethostbyname(hostname)
129 return socket.gethostbyname(hostname)
130 except Exception as e:
130 except Exception as e:
131 if log_errors:
131 if log_errors:
132 # in some cases this lookup is not possible, and we don't want to
132 # in some cases this lookup is not possible, and we don't want to
133 # make it an exception in logs
133 # make it an exception in logs
134 log.exception('Could not retrieve server ip address: %s', e)
134 log.exception('Could not retrieve server ip address: %s', e)
135 return hostname
135 return hostname
136
136
137
137
138 def get_server_port(environ):
138 def get_server_port(environ):
139 return environ.get('SERVER_PORT')
139 return environ.get('SERVER_PORT')
140
140
141
141
142 def get_access_path(environ):
142 def get_access_path(environ):
143 path = environ.get('PATH_INFO')
143 path = environ.get('PATH_INFO')
144 org_req = environ.get('pylons.original_request')
144 org_req = environ.get('pylons.original_request')
145 if org_req:
145 if org_req:
146 path = org_req.environ.get('PATH_INFO')
146 path = org_req.environ.get('PATH_INFO')
147 return path
147 return path
148
148
149
149
150 def get_user_agent(environ):
150 def get_user_agent(environ):
151 return environ.get('HTTP_USER_AGENT')
151 return environ.get('HTTP_USER_AGENT')
152
152
153
153
154 def vcs_operation_context(
154 def vcs_operation_context(
155 environ, repo_name, username, action, scm, check_locking=True,
155 environ, repo_name, username, action, scm, check_locking=True,
156 is_shadow_repo=False):
156 is_shadow_repo=False):
157 """
157 """
158 Generate the context for a vcs operation, e.g. push or pull.
158 Generate the context for a vcs operation, e.g. push or pull.
159
159
160 This context is passed over the layers so that hooks triggered by the
160 This context is passed over the layers so that hooks triggered by the
161 vcs operation know details like the user, the user's IP address etc.
161 vcs operation know details like the user, the user's IP address etc.
162
162
163 :param check_locking: Allows to switch of the computation of the locking
163 :param check_locking: Allows to switch of the computation of the locking
164 data. This serves mainly the need of the simplevcs middleware to be
164 data. This serves mainly the need of the simplevcs middleware to be
165 able to disable this for certain operations.
165 able to disable this for certain operations.
166
166
167 """
167 """
168 # Tri-state value: False: unlock, None: nothing, True: lock
168 # Tri-state value: False: unlock, None: nothing, True: lock
169 make_lock = None
169 make_lock = None
170 locked_by = [None, None, None]
170 locked_by = [None, None, None]
171 is_anonymous = username == User.DEFAULT_USER
171 is_anonymous = username == User.DEFAULT_USER
172 user = User.get_by_username(username)
172 if not is_anonymous and check_locking:
173 if not is_anonymous and check_locking:
173 log.debug('Checking locking on repository "%s"', repo_name)
174 log.debug('Checking locking on repository "%s"', repo_name)
174 user = User.get_by_username(username)
175 repo = Repository.get_by_repo_name(repo_name)
175 repo = Repository.get_by_repo_name(repo_name)
176 make_lock, __, locked_by = repo.get_locking_state(
176 make_lock, __, locked_by = repo.get_locking_state(
177 action, user.user_id)
177 action, user.user_id)
178
178 user_id = user.user_id
179 settings_model = VcsSettingsModel(repo=repo_name)
179 settings_model = VcsSettingsModel(repo=repo_name)
180 ui_settings = settings_model.get_ui_settings()
180 ui_settings = settings_model.get_ui_settings()
181
181
182 extras = {
182 extras = {
183 'ip': get_ip_addr(environ),
183 'ip': get_ip_addr(environ),
184 'username': username,
184 'username': username,
185 'user_id': user_id,
185 'action': action,
186 'action': action,
186 'repository': repo_name,
187 'repository': repo_name,
187 'scm': scm,
188 'scm': scm,
188 'config': rhodecode.CONFIG['__file__'],
189 'config': rhodecode.CONFIG['__file__'],
189 'make_lock': make_lock,
190 'make_lock': make_lock,
190 'locked_by': locked_by,
191 'locked_by': locked_by,
191 'server_url': utils2.get_server_url(environ),
192 'server_url': utils2.get_server_url(environ),
192 'user_agent': get_user_agent(environ),
193 'user_agent': get_user_agent(environ),
193 'hooks': get_enabled_hook_classes(ui_settings),
194 'hooks': get_enabled_hook_classes(ui_settings),
194 'is_shadow_repo': is_shadow_repo,
195 'is_shadow_repo': is_shadow_repo,
195 }
196 }
196 return extras
197 return extras
197
198
198
199
199 class BasicAuth(AuthBasicAuthenticator):
200 class BasicAuth(AuthBasicAuthenticator):
200
201
201 def __init__(self, realm, authfunc, registry, auth_http_code=None,
202 def __init__(self, realm, authfunc, registry, auth_http_code=None,
202 initial_call_detection=False, acl_repo_name=None):
203 initial_call_detection=False, acl_repo_name=None):
203 self.realm = realm
204 self.realm = realm
204 self.initial_call = initial_call_detection
205 self.initial_call = initial_call_detection
205 self.authfunc = authfunc
206 self.authfunc = authfunc
206 self.registry = registry
207 self.registry = registry
207 self.acl_repo_name = acl_repo_name
208 self.acl_repo_name = acl_repo_name
208 self._rc_auth_http_code = auth_http_code
209 self._rc_auth_http_code = auth_http_code
209
210
210 def _get_response_from_code(self, http_code):
211 def _get_response_from_code(self, http_code):
211 try:
212 try:
212 return get_exception(safe_int(http_code))
213 return get_exception(safe_int(http_code))
213 except Exception:
214 except Exception:
214 log.exception('Failed to fetch response for code %s' % http_code)
215 log.exception('Failed to fetch response for code %s' % http_code)
215 return HTTPForbidden
216 return HTTPForbidden
216
217
217 def get_rc_realm(self):
218 def get_rc_realm(self):
218 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
219 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
219
220
220 def build_authentication(self):
221 def build_authentication(self):
221 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
222 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
222 if self._rc_auth_http_code and not self.initial_call:
223 if self._rc_auth_http_code and not self.initial_call:
223 # return alternative HTTP code if alternative http return code
224 # return alternative HTTP code if alternative http return code
224 # is specified in RhodeCode config, but ONLY if it's not the
225 # is specified in RhodeCode config, but ONLY if it's not the
225 # FIRST call
226 # FIRST call
226 custom_response_klass = self._get_response_from_code(
227 custom_response_klass = self._get_response_from_code(
227 self._rc_auth_http_code)
228 self._rc_auth_http_code)
228 return custom_response_klass(headers=head)
229 return custom_response_klass(headers=head)
229 return HTTPUnauthorized(headers=head)
230 return HTTPUnauthorized(headers=head)
230
231
231 def authenticate(self, environ):
232 def authenticate(self, environ):
232 authorization = AUTHORIZATION(environ)
233 authorization = AUTHORIZATION(environ)
233 if not authorization:
234 if not authorization:
234 return self.build_authentication()
235 return self.build_authentication()
235 (authmeth, auth) = authorization.split(' ', 1)
236 (authmeth, auth) = authorization.split(' ', 1)
236 if 'basic' != authmeth.lower():
237 if 'basic' != authmeth.lower():
237 return self.build_authentication()
238 return self.build_authentication()
238 auth = auth.strip().decode('base64')
239 auth = auth.strip().decode('base64')
239 _parts = auth.split(':', 1)
240 _parts = auth.split(':', 1)
240 if len(_parts) == 2:
241 if len(_parts) == 2:
241 username, password = _parts
242 username, password = _parts
242 auth_data = self.authfunc(
243 auth_data = self.authfunc(
243 username, password, environ, VCS_TYPE,
244 username, password, environ, VCS_TYPE,
244 registry=self.registry, acl_repo_name=self.acl_repo_name)
245 registry=self.registry, acl_repo_name=self.acl_repo_name)
245 if auth_data:
246 if auth_data:
246 return {'username': username, 'auth_data': auth_data}
247 return {'username': username, 'auth_data': auth_data}
247 if username and password:
248 if username and password:
248 # we mark that we actually executed authentication once, at
249 # we mark that we actually executed authentication once, at
249 # that point we can use the alternative auth code
250 # that point we can use the alternative auth code
250 self.initial_call = False
251 self.initial_call = False
251
252
252 return self.build_authentication()
253 return self.build_authentication()
253
254
254 __call__ = authenticate
255 __call__ = authenticate
255
256
256
257
257 def calculate_version_hash(config):
258 def calculate_version_hash(config):
258 return md5(
259 return md5(
259 config.get('beaker.session.secret', '') +
260 config.get('beaker.session.secret', '') +
260 rhodecode.__version__)[:8]
261 rhodecode.__version__)[:8]
261
262
262
263
263 def get_current_lang(request):
264 def get_current_lang(request):
264 # NOTE(marcink): remove after pyramid move
265 # NOTE(marcink): remove after pyramid move
265 try:
266 try:
266 return translation.get_lang()[0]
267 return translation.get_lang()[0]
267 except:
268 except:
268 pass
269 pass
269
270
270 return getattr(request, '_LOCALE_', request.locale_name)
271 return getattr(request, '_LOCALE_', request.locale_name)
271
272
272
273
273 def attach_context_attributes(context, request, user_id):
274 def attach_context_attributes(context, request, user_id):
274 """
275 """
275 Attach variables into template context called `c`.
276 Attach variables into template context called `c`.
276 """
277 """
277 config = request.registry.settings
278 config = request.registry.settings
278
279
279
280
280 rc_config = SettingsModel().get_all_settings(cache=True)
281 rc_config = SettingsModel().get_all_settings(cache=True)
281
282
282 context.rhodecode_version = rhodecode.__version__
283 context.rhodecode_version = rhodecode.__version__
283 context.rhodecode_edition = config.get('rhodecode.edition')
284 context.rhodecode_edition = config.get('rhodecode.edition')
284 # unique secret + version does not leak the version but keep consistency
285 # unique secret + version does not leak the version but keep consistency
285 context.rhodecode_version_hash = calculate_version_hash(config)
286 context.rhodecode_version_hash = calculate_version_hash(config)
286
287
287 # Default language set for the incoming request
288 # Default language set for the incoming request
288 context.language = get_current_lang(request)
289 context.language = get_current_lang(request)
289
290
290 # Visual options
291 # Visual options
291 context.visual = AttributeDict({})
292 context.visual = AttributeDict({})
292
293
293 # DB stored Visual Items
294 # DB stored Visual Items
294 context.visual.show_public_icon = str2bool(
295 context.visual.show_public_icon = str2bool(
295 rc_config.get('rhodecode_show_public_icon'))
296 rc_config.get('rhodecode_show_public_icon'))
296 context.visual.show_private_icon = str2bool(
297 context.visual.show_private_icon = str2bool(
297 rc_config.get('rhodecode_show_private_icon'))
298 rc_config.get('rhodecode_show_private_icon'))
298 context.visual.stylify_metatags = str2bool(
299 context.visual.stylify_metatags = str2bool(
299 rc_config.get('rhodecode_stylify_metatags'))
300 rc_config.get('rhodecode_stylify_metatags'))
300 context.visual.dashboard_items = safe_int(
301 context.visual.dashboard_items = safe_int(
301 rc_config.get('rhodecode_dashboard_items', 100))
302 rc_config.get('rhodecode_dashboard_items', 100))
302 context.visual.admin_grid_items = safe_int(
303 context.visual.admin_grid_items = safe_int(
303 rc_config.get('rhodecode_admin_grid_items', 100))
304 rc_config.get('rhodecode_admin_grid_items', 100))
304 context.visual.repository_fields = str2bool(
305 context.visual.repository_fields = str2bool(
305 rc_config.get('rhodecode_repository_fields'))
306 rc_config.get('rhodecode_repository_fields'))
306 context.visual.show_version = str2bool(
307 context.visual.show_version = str2bool(
307 rc_config.get('rhodecode_show_version'))
308 rc_config.get('rhodecode_show_version'))
308 context.visual.use_gravatar = str2bool(
309 context.visual.use_gravatar = str2bool(
309 rc_config.get('rhodecode_use_gravatar'))
310 rc_config.get('rhodecode_use_gravatar'))
310 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
311 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
311 context.visual.default_renderer = rc_config.get(
312 context.visual.default_renderer = rc_config.get(
312 'rhodecode_markup_renderer', 'rst')
313 'rhodecode_markup_renderer', 'rst')
313 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
314 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
314 context.visual.rhodecode_support_url = \
315 context.visual.rhodecode_support_url = \
315 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
316 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
316
317
317 context.visual.affected_files_cut_off = 60
318 context.visual.affected_files_cut_off = 60
318
319
319 context.pre_code = rc_config.get('rhodecode_pre_code')
320 context.pre_code = rc_config.get('rhodecode_pre_code')
320 context.post_code = rc_config.get('rhodecode_post_code')
321 context.post_code = rc_config.get('rhodecode_post_code')
321 context.rhodecode_name = rc_config.get('rhodecode_title')
322 context.rhodecode_name = rc_config.get('rhodecode_title')
322 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
323 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
323 # if we have specified default_encoding in the request, it has more
324 # if we have specified default_encoding in the request, it has more
324 # priority
325 # priority
325 if request.GET.get('default_encoding'):
326 if request.GET.get('default_encoding'):
326 context.default_encodings.insert(0, request.GET.get('default_encoding'))
327 context.default_encodings.insert(0, request.GET.get('default_encoding'))
327 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
328 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
328
329
329 # INI stored
330 # INI stored
330 context.labs_active = str2bool(
331 context.labs_active = str2bool(
331 config.get('labs_settings_active', 'false'))
332 config.get('labs_settings_active', 'false'))
332 context.visual.allow_repo_location_change = str2bool(
333 context.visual.allow_repo_location_change = str2bool(
333 config.get('allow_repo_location_change', True))
334 config.get('allow_repo_location_change', True))
334 context.visual.allow_custom_hooks_settings = str2bool(
335 context.visual.allow_custom_hooks_settings = str2bool(
335 config.get('allow_custom_hooks_settings', True))
336 config.get('allow_custom_hooks_settings', True))
336 context.debug_style = str2bool(config.get('debug_style', False))
337 context.debug_style = str2bool(config.get('debug_style', False))
337
338
338 context.rhodecode_instanceid = config.get('instance_id')
339 context.rhodecode_instanceid = config.get('instance_id')
339
340
340 context.visual.cut_off_limit_diff = safe_int(
341 context.visual.cut_off_limit_diff = safe_int(
341 config.get('cut_off_limit_diff'))
342 config.get('cut_off_limit_diff'))
342 context.visual.cut_off_limit_file = safe_int(
343 context.visual.cut_off_limit_file = safe_int(
343 config.get('cut_off_limit_file'))
344 config.get('cut_off_limit_file'))
344
345
345 # AppEnlight
346 # AppEnlight
346 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
347 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
347 context.appenlight_api_public_key = config.get(
348 context.appenlight_api_public_key = config.get(
348 'appenlight.api_public_key', '')
349 'appenlight.api_public_key', '')
349 context.appenlight_server_url = config.get('appenlight.server_url', '')
350 context.appenlight_server_url = config.get('appenlight.server_url', '')
350
351
351 # JS template context
352 # JS template context
352 context.template_context = {
353 context.template_context = {
353 'repo_name': None,
354 'repo_name': None,
354 'repo_type': None,
355 'repo_type': None,
355 'repo_landing_commit': None,
356 'repo_landing_commit': None,
356 'rhodecode_user': {
357 'rhodecode_user': {
357 'username': None,
358 'username': None,
358 'email': None,
359 'email': None,
359 'notification_status': False
360 'notification_status': False
360 },
361 },
361 'visual': {
362 'visual': {
362 'default_renderer': None
363 'default_renderer': None
363 },
364 },
364 'commit_data': {
365 'commit_data': {
365 'commit_id': None
366 'commit_id': None
366 },
367 },
367 'pull_request_data': {'pull_request_id': None},
368 'pull_request_data': {'pull_request_id': None},
368 'timeago': {
369 'timeago': {
369 'refresh_time': 120 * 1000,
370 'refresh_time': 120 * 1000,
370 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
371 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
371 },
372 },
372 'pyramid_dispatch': {
373 'pyramid_dispatch': {
373
374
374 },
375 },
375 'extra': {'plugins': {}}
376 'extra': {'plugins': {}}
376 }
377 }
377 # END CONFIG VARS
378 # END CONFIG VARS
378
379
379 diffmode = 'sideside'
380 diffmode = 'sideside'
380 if request.GET.get('diffmode'):
381 if request.GET.get('diffmode'):
381 if request.GET['diffmode'] == 'unified':
382 if request.GET['diffmode'] == 'unified':
382 diffmode = 'unified'
383 diffmode = 'unified'
383 elif request.session.get('diffmode'):
384 elif request.session.get('diffmode'):
384 diffmode = request.session['diffmode']
385 diffmode = request.session['diffmode']
385
386
386 context.diffmode = diffmode
387 context.diffmode = diffmode
387
388
388 if request.session.get('diffmode') != diffmode:
389 if request.session.get('diffmode') != diffmode:
389 request.session['diffmode'] = diffmode
390 request.session['diffmode'] = diffmode
390
391
391 context.csrf_token = auth.get_csrf_token(session=request.session)
392 context.csrf_token = auth.get_csrf_token(session=request.session)
392 context.backends = rhodecode.BACKENDS.keys()
393 context.backends = rhodecode.BACKENDS.keys()
393 context.backends.sort()
394 context.backends.sort()
394 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
395 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
395
396
396 # web case
397 # web case
397 if hasattr(request, 'user'):
398 if hasattr(request, 'user'):
398 context.auth_user = request.user
399 context.auth_user = request.user
399 context.rhodecode_user = request.user
400 context.rhodecode_user = request.user
400
401
401 # api case
402 # api case
402 if hasattr(request, 'rpc_user'):
403 if hasattr(request, 'rpc_user'):
403 context.auth_user = request.rpc_user
404 context.auth_user = request.rpc_user
404 context.rhodecode_user = request.rpc_user
405 context.rhodecode_user = request.rpc_user
405
406
406 # attach the whole call context to the request
407 # attach the whole call context to the request
407 request.call_context = context
408 request.call_context = context
408
409
409
410
410 def get_auth_user(request):
411 def get_auth_user(request):
411 environ = request.environ
412 environ = request.environ
412 session = request.session
413 session = request.session
413
414
414 ip_addr = get_ip_addr(environ)
415 ip_addr = get_ip_addr(environ)
415 # make sure that we update permissions each time we call controller
416 # make sure that we update permissions each time we call controller
416 _auth_token = (request.GET.get('auth_token', '') or
417 _auth_token = (request.GET.get('auth_token', '') or
417 request.GET.get('api_key', ''))
418 request.GET.get('api_key', ''))
418
419
419 if _auth_token:
420 if _auth_token:
420 # when using API_KEY we assume user exists, and
421 # when using API_KEY we assume user exists, and
421 # doesn't need auth based on cookies.
422 # doesn't need auth based on cookies.
422 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
423 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
423 authenticated = False
424 authenticated = False
424 else:
425 else:
425 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
426 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
426 try:
427 try:
427 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
428 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
428 ip_addr=ip_addr)
429 ip_addr=ip_addr)
429 except UserCreationError as e:
430 except UserCreationError as e:
430 h.flash(e, 'error')
431 h.flash(e, 'error')
431 # container auth or other auth functions that create users
432 # container auth or other auth functions that create users
432 # on the fly can throw this exception signaling that there's
433 # on the fly can throw this exception signaling that there's
433 # issue with user creation, explanation should be provided
434 # issue with user creation, explanation should be provided
434 # in Exception itself. We then create a simple blank
435 # in Exception itself. We then create a simple blank
435 # AuthUser
436 # AuthUser
436 auth_user = AuthUser(ip_addr=ip_addr)
437 auth_user = AuthUser(ip_addr=ip_addr)
437
438
438 # in case someone changes a password for user it triggers session
439 # in case someone changes a password for user it triggers session
439 # flush and forces a re-login
440 # flush and forces a re-login
440 if password_changed(auth_user, session):
441 if password_changed(auth_user, session):
441 session.invalidate()
442 session.invalidate()
442 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
443 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
443 auth_user = AuthUser(ip_addr=ip_addr)
444 auth_user = AuthUser(ip_addr=ip_addr)
444
445
445 authenticated = cookie_store.get('is_authenticated')
446 authenticated = cookie_store.get('is_authenticated')
446
447
447 if not auth_user.is_authenticated and auth_user.is_user_object:
448 if not auth_user.is_authenticated and auth_user.is_user_object:
448 # user is not authenticated and not empty
449 # user is not authenticated and not empty
449 auth_user.set_authenticated(authenticated)
450 auth_user.set_authenticated(authenticated)
450
451
451 return auth_user
452 return auth_user
452
453
453
454
454 def h_filter(s):
455 def h_filter(s):
455 """
456 """
456 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
457 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
457 we wrap this with additional functionality that converts None to empty
458 we wrap this with additional functionality that converts None to empty
458 strings
459 strings
459 """
460 """
460 if s is None:
461 if s is None:
461 return markupsafe.Markup()
462 return markupsafe.Markup()
462 return markupsafe.escape(s)
463 return markupsafe.escape(s)
463
464
464
465
465 def add_events_routes(config):
466 def add_events_routes(config):
466 """
467 """
467 Adds routing that can be used in events. Because some events are triggered
468 Adds routing that can be used in events. Because some events are triggered
468 outside of pyramid context, we need to bootstrap request with some
469 outside of pyramid context, we need to bootstrap request with some
469 routing registered
470 routing registered
470 """
471 """
471
472
472 from rhodecode.apps._base import ADMIN_PREFIX
473 from rhodecode.apps._base import ADMIN_PREFIX
473
474
474 config.add_route(name='home', pattern='/')
475 config.add_route(name='home', pattern='/')
475
476
476 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
477 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
477 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
478 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
478 config.add_route(name='repo_summary', pattern='/{repo_name}')
479 config.add_route(name='repo_summary', pattern='/{repo_name}')
479 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
480 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
480 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
481 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
481
482
482 config.add_route(name='pullrequest_show',
483 config.add_route(name='pullrequest_show',
483 pattern='/{repo_name}/pull-request/{pull_request_id}')
484 pattern='/{repo_name}/pull-request/{pull_request_id}')
484 config.add_route(name='pull_requests_global',
485 config.add_route(name='pull_requests_global',
485 pattern='/pull-request/{pull_request_id}')
486 pattern='/pull-request/{pull_request_id}')
486 config.add_route(name='repo_commit',
487 config.add_route(name='repo_commit',
487 pattern='/{repo_name}/changeset/{commit_id}')
488 pattern='/{repo_name}/changeset/{commit_id}')
488
489
489 config.add_route(name='repo_files',
490 config.add_route(name='repo_files',
490 pattern='/{repo_name}/files/{commit_id}/{f_path}')
491 pattern='/{repo_name}/files/{commit_id}/{f_path}')
491
492
492
493
493 def bootstrap_config(request):
494 def bootstrap_config(request):
494 import pyramid.testing
495 import pyramid.testing
495 registry = pyramid.testing.Registry('RcTestRegistry')
496 registry = pyramid.testing.Registry('RcTestRegistry')
496
497
497 config = pyramid.testing.setUp(registry=registry, request=request)
498 config = pyramid.testing.setUp(registry=registry, request=request)
498
499
499 # allow pyramid lookup in testing
500 # allow pyramid lookup in testing
500 config.include('pyramid_mako')
501 config.include('pyramid_mako')
501 config.include('pyramid_beaker')
502 config.include('pyramid_beaker')
502 config.include('rhodecode.lib.caches')
503 config.include('rhodecode.lib.caches')
503
504
504 add_events_routes(config)
505 add_events_routes(config)
505
506
506 return config
507 return config
507
508
508
509
509 def bootstrap_request(**kwargs):
510 def bootstrap_request(**kwargs):
510 import pyramid.testing
511 import pyramid.testing
511
512
512 class TestRequest(pyramid.testing.DummyRequest):
513 class TestRequest(pyramid.testing.DummyRequest):
513 application_url = kwargs.pop('application_url', 'http://example.com')
514 application_url = kwargs.pop('application_url', 'http://example.com')
514 host = kwargs.pop('host', 'example.com:80')
515 host = kwargs.pop('host', 'example.com:80')
515 domain = kwargs.pop('domain', 'example.com')
516 domain = kwargs.pop('domain', 'example.com')
516
517
517 def translate(self, msg):
518 def translate(self, msg):
518 return msg
519 return msg
519
520
520 def plularize(self, singular, plural, n):
521 def plularize(self, singular, plural, n):
521 return singular
522 return singular
522
523
523 def get_partial_renderer(self, tmpl_name):
524 def get_partial_renderer(self, tmpl_name):
524
525
525 from rhodecode.lib.partial_renderer import get_partial_renderer
526 from rhodecode.lib.partial_renderer import get_partial_renderer
526 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
527 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
527
528
528 _call_context = {}
529 _call_context = {}
529 @property
530 @property
530 def call_context(self):
531 def call_context(self):
531 return self._call_context
532 return self._call_context
532
533
533 class TestDummySession(pyramid.testing.DummySession):
534 class TestDummySession(pyramid.testing.DummySession):
534 def save(*arg, **kw):
535 def save(*arg, **kw):
535 pass
536 pass
536
537
537 request = TestRequest(**kwargs)
538 request = TestRequest(**kwargs)
538 request.session = TestDummySession()
539 request.session = TestDummySession()
539
540
540 return request
541 return request
541
542
@@ -1,120 +1,121 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.tests.events.conftest import EventCatcher
23 from rhodecode.tests.events.conftest import EventCatcher
24
24
25 from rhodecode.lib import hooks_base, utils2
25 from rhodecode.lib import hooks_base, utils2
26 from rhodecode.model.repo import RepoModel
26 from rhodecode.model.repo import RepoModel
27 from rhodecode.events.repo import (
27 from rhodecode.events.repo import (
28 RepoPrePullEvent, RepoPullEvent,
28 RepoPrePullEvent, RepoPullEvent,
29 RepoPrePushEvent, RepoPushEvent,
29 RepoPrePushEvent, RepoPushEvent,
30 RepoPreCreateEvent, RepoCreateEvent,
30 RepoPreCreateEvent, RepoCreateEvent,
31 RepoPreDeleteEvent, RepoDeleteEvent,
31 RepoPreDeleteEvent, RepoDeleteEvent,
32 )
32 )
33
33
34
34
35 @pytest.fixture
35 @pytest.fixture
36 def scm_extras(user_regular, repo_stub):
36 def scm_extras(user_regular, repo_stub):
37 extras = utils2.AttributeDict({
37 extras = utils2.AttributeDict({
38 'ip': '127.0.0.1',
38 'ip': '127.0.0.1',
39 'username': user_regular.username,
39 'username': user_regular.username,
40 'user_id': user_regular.user_id,
40 'action': '',
41 'action': '',
41 'repository': repo_stub.repo_name,
42 'repository': repo_stub.repo_name,
42 'scm': repo_stub.scm_instance().alias,
43 'scm': repo_stub.scm_instance().alias,
43 'config': '',
44 'config': '',
44 'server_url': 'http://example.com',
45 'server_url': 'http://example.com',
45 'make_lock': None,
46 'make_lock': None,
46 'user-agent': 'some-client',
47 'user-agent': 'some-client',
47 'locked_by': [None],
48 'locked_by': [None],
48 'commit_ids': ['a' * 40] * 3,
49 'commit_ids': ['a' * 40] * 3,
49 'is_shadow_repo': False,
50 'is_shadow_repo': False,
50 })
51 })
51 return extras
52 return extras
52
53
53
54
54 # TODO: dan: make the serialization tests complete json comparisons
55 # TODO: dan: make the serialization tests complete json comparisons
55 @pytest.mark.parametrize('EventClass', [
56 @pytest.mark.parametrize('EventClass', [
56 RepoPreCreateEvent, RepoCreateEvent,
57 RepoPreCreateEvent, RepoCreateEvent,
57 RepoPreDeleteEvent, RepoDeleteEvent,
58 RepoPreDeleteEvent, RepoDeleteEvent,
58 ])
59 ])
59 def test_repo_events_serialized(config_stub, repo_stub, EventClass):
60 def test_repo_events_serialized(config_stub, repo_stub, EventClass):
60 event = EventClass(repo_stub)
61 event = EventClass(repo_stub)
61 data = event.as_dict()
62 data = event.as_dict()
62 assert data['name'] == EventClass.name
63 assert data['name'] == EventClass.name
63 assert data['repo']['repo_name'] == repo_stub.repo_name
64 assert data['repo']['repo_name'] == repo_stub.repo_name
64 assert data['repo']['url']
65 assert data['repo']['url']
65 assert data['repo']['permalink_url']
66 assert data['repo']['permalink_url']
66
67
67
68
68 @pytest.mark.parametrize('EventClass', [
69 @pytest.mark.parametrize('EventClass', [
69 RepoPrePullEvent, RepoPullEvent, RepoPrePushEvent
70 RepoPrePullEvent, RepoPullEvent, RepoPrePushEvent
70 ])
71 ])
71 def test_vcs_repo_events_serialize(config_stub, repo_stub, scm_extras, EventClass):
72 def test_vcs_repo_events_serialize(config_stub, repo_stub, scm_extras, EventClass):
72 event = EventClass(repo_name=repo_stub.repo_name, extras=scm_extras)
73 event = EventClass(repo_name=repo_stub.repo_name, extras=scm_extras)
73 data = event.as_dict()
74 data = event.as_dict()
74 assert data['name'] == EventClass.name
75 assert data['name'] == EventClass.name
75 assert data['repo']['repo_name'] == repo_stub.repo_name
76 assert data['repo']['repo_name'] == repo_stub.repo_name
76 assert data['repo']['url']
77 assert data['repo']['url']
77 assert data['repo']['permalink_url']
78 assert data['repo']['permalink_url']
78
79
79
80
80 @pytest.mark.parametrize('EventClass', [RepoPushEvent])
81 @pytest.mark.parametrize('EventClass', [RepoPushEvent])
81 def test_vcs_repo_push_event_serialize(config_stub, repo_stub, scm_extras, EventClass):
82 def test_vcs_repo_push_event_serialize(config_stub, repo_stub, scm_extras, EventClass):
82 event = EventClass(repo_name=repo_stub.repo_name,
83 event = EventClass(repo_name=repo_stub.repo_name,
83 pushed_commit_ids=scm_extras['commit_ids'],
84 pushed_commit_ids=scm_extras['commit_ids'],
84 extras=scm_extras)
85 extras=scm_extras)
85 data = event.as_dict()
86 data = event.as_dict()
86 assert data['name'] == EventClass.name
87 assert data['name'] == EventClass.name
87 assert data['repo']['repo_name'] == repo_stub.repo_name
88 assert data['repo']['repo_name'] == repo_stub.repo_name
88 assert data['repo']['url']
89 assert data['repo']['url']
89 assert data['repo']['permalink_url']
90 assert data['repo']['permalink_url']
90
91
91
92
92 def test_create_delete_repo_fires_events(backend):
93 def test_create_delete_repo_fires_events(backend):
93 with EventCatcher() as event_catcher:
94 with EventCatcher() as event_catcher:
94 repo = backend.create_repo()
95 repo = backend.create_repo()
95 assert event_catcher.events_types == [RepoPreCreateEvent, RepoCreateEvent]
96 assert event_catcher.events_types == [RepoPreCreateEvent, RepoCreateEvent]
96
97
97 with EventCatcher() as event_catcher:
98 with EventCatcher() as event_catcher:
98 RepoModel().delete(repo)
99 RepoModel().delete(repo)
99 assert event_catcher.events_types == [RepoPreDeleteEvent, RepoDeleteEvent]
100 assert event_catcher.events_types == [RepoPreDeleteEvent, RepoDeleteEvent]
100
101
101
102
102 def test_pull_fires_events(scm_extras):
103 def test_pull_fires_events(scm_extras):
103 with EventCatcher() as event_catcher:
104 with EventCatcher() as event_catcher:
104 hooks_base.pre_push(scm_extras)
105 hooks_base.pre_push(scm_extras)
105 assert event_catcher.events_types == [RepoPrePushEvent]
106 assert event_catcher.events_types == [RepoPrePushEvent]
106
107
107 with EventCatcher() as event_catcher:
108 with EventCatcher() as event_catcher:
108 hooks_base.post_push(scm_extras)
109 hooks_base.post_push(scm_extras)
109 assert event_catcher.events_types == [RepoPushEvent]
110 assert event_catcher.events_types == [RepoPushEvent]
110
111
111
112
112 def test_push_fires_events(scm_extras):
113 def test_push_fires_events(scm_extras):
113 with EventCatcher() as event_catcher:
114 with EventCatcher() as event_catcher:
114 hooks_base.pre_pull(scm_extras)
115 hooks_base.pre_pull(scm_extras)
115 assert event_catcher.events_types == [RepoPrePullEvent]
116 assert event_catcher.events_types == [RepoPrePullEvent]
116
117
117 with EventCatcher() as event_catcher:
118 with EventCatcher() as event_catcher:
118 hooks_base.post_pull(scm_extras)
119 hooks_base.post_pull(scm_extras)
119 assert event_catcher.events_types == [RepoPullEvent]
120 assert event_catcher.events_types == [RepoPullEvent]
120
121
@@ -1,52 +1,53 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import pytest
22 import pytest
23 from rhodecode import events
23 from rhodecode import events
24
24
25
25
26 @pytest.fixture
26 @pytest.fixture
27 def repo_push_event(backend, user_regular):
27 def repo_push_event(backend, user_regular):
28 commits = [
28 commits = [
29 {'message': 'ancestor commit fixes #15'},
29 {'message': 'ancestor commit fixes #15'},
30 {'message': 'quick fixes'},
30 {'message': 'quick fixes'},
31 {'message': 'change that fixes #41, #2'},
31 {'message': 'change that fixes #41, #2'},
32 {'message': 'this is because 5b23c3532 broke stuff'},
32 {'message': 'this is because 5b23c3532 broke stuff'},
33 {'message': 'last commit'},
33 {'message': 'last commit'},
34 ]
34 ]
35 commit_ids = backend.create_master_repo(commits).values()
35 commit_ids = backend.create_master_repo(commits).values()
36 repo = backend.create_repo()
36 repo = backend.create_repo()
37 scm_extras = {
37 scm_extras = {
38 'ip': '127.0.0.1',
38 'ip': '127.0.0.1',
39 'username': user_regular.username,
39 'username': user_regular.username,
40 'user_id': user_regular.user_id,
40 'action': '',
41 'action': '',
41 'repository': repo.repo_name,
42 'repository': repo.repo_name,
42 'scm': repo.scm_instance().alias,
43 'scm': repo.scm_instance().alias,
43 'config': '',
44 'config': '',
44 'server_url': 'http://example.com',
45 'server_url': 'http://example.com',
45 'make_lock': None,
46 'make_lock': None,
46 'locked_by': [None],
47 'locked_by': [None],
47 'commit_ids': commit_ids,
48 'commit_ids': commit_ids,
48 }
49 }
49
50
50 return events.RepoPushEvent(repo_name=repo.repo_name,
51 return events.RepoPushEvent(repo_name=repo.repo_name,
51 pushed_commit_ids=commit_ids,
52 pushed_commit_ids=commit_ids,
52 extras=scm_extras)
53 extras=scm_extras)
@@ -1,94 +1,96 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22 from mock import Mock, patch
22 from mock import Mock, patch
23
23
24 from rhodecode.lib import base
24 from rhodecode.lib import base
25 from rhodecode.model import db
25 from rhodecode.model import db
26
26
27
27
28 @pytest.mark.parametrize('result_key, expected_value', [
28 @pytest.mark.parametrize('result_key, expected_value', [
29 ('username', 'stub_username'),
29 ('username', 'stub_username'),
30 ('action', 'stub_action'),
30 ('action', 'stub_action'),
31 ('repository', 'stub_repo_name'),
31 ('repository', 'stub_repo_name'),
32 ('scm', 'stub_scm'),
32 ('scm', 'stub_scm'),
33 ('hooks', ['stub_hook']),
33 ('hooks', ['stub_hook']),
34 ('config', 'stub_ini_filename'),
34 ('config', 'stub_ini_filename'),
35 ('ip', '1.2.3.4'),
35 ('ip', '1.2.3.4'),
36 ('server_url', 'https://example.com'),
36 ('server_url', 'https://example.com'),
37 ('user_agent', 'client-text-v1.1'),
37 ('user_agent', 'client-text-v1.1'),
38 # TODO: johbo: Commpare locking parameters with `_get_rc_scm_extras`
38 # TODO: johbo: Commpare locking parameters with `_get_rc_scm_extras`
39 # in hooks_utils.
39 # in hooks_utils.
40 ('make_lock', None),
40 ('make_lock', None),
41 ('locked_by', [None, None, None]),
41 ('locked_by', [None, None, None]),
42 ])
42 ])
43 def test_vcs_operation_context_parameters(result_key, expected_value):
43 def test_vcs_operation_context_parameters(result_key, expected_value):
44 result = call_vcs_operation_context()
44 result = call_vcs_operation_context()
45 assert result[result_key] == expected_value
45 assert result[result_key] == expected_value
46
46
47
47
48 @patch('rhodecode.model.db.User.get_by_username', Mock())
48 @patch('rhodecode.model.db.User.get_by_username', Mock())
49 @patch('rhodecode.model.db.Repository.get_by_repo_name')
49 @patch('rhodecode.model.db.Repository.get_by_repo_name')
50 def test_vcs_operation_context_checks_locking(mock_get_by_repo_name):
50 def test_vcs_operation_context_checks_locking(mock_get_by_repo_name):
51 mock_get_locking_state = mock_get_by_repo_name().get_locking_state
51 mock_get_locking_state = mock_get_by_repo_name().get_locking_state
52 mock_get_locking_state.return_value = (None, None, [None, None, None])
52 mock_get_locking_state.return_value = (None, None, [None, None, None])
53 call_vcs_operation_context(check_locking=True)
53 call_vcs_operation_context(check_locking=True)
54 assert mock_get_locking_state.called
54 assert mock_get_locking_state.called
55
55
56
56
57 @patch('rhodecode.model.db.Repository.get_locking_state')
57 @patch('rhodecode.model.db.Repository.get_locking_state')
58 def test_vcs_operation_context_skips_locking_checks_if_anonymouse(
58 def test_vcs_operation_context_skips_locking_checks_if_anonymouse(
59 mock_get_locking_state):
59 mock_get_locking_state):
60 call_vcs_operation_context(
60 call_vcs_operation_context(
61 username=db.User.DEFAULT_USER, check_locking=True)
61 username=db.User.DEFAULT_USER, check_locking=True)
62 assert not mock_get_locking_state.called
62 assert not mock_get_locking_state.called
63
63
64
64
65 @patch('rhodecode.model.db.Repository.get_locking_state')
65 @patch('rhodecode.model.db.Repository.get_locking_state')
66 def test_vcs_operation_context_can_skip_locking_check(mock_get_locking_state):
66 def test_vcs_operation_context_can_skip_locking_check(mock_get_locking_state):
67 call_vcs_operation_context(check_locking=False)
67 call_vcs_operation_context(check_locking=False)
68 assert not mock_get_locking_state.called
68 assert not mock_get_locking_state.called
69
69
70
70
71 @patch.object(
71 @patch.object(
72 base, 'get_enabled_hook_classes', Mock(return_value=['stub_hook']))
72 base, 'get_enabled_hook_classes', Mock(return_value=['stub_hook']))
73 @patch('rhodecode.lib.utils2.get_server_url',
73 @patch('rhodecode.lib.utils2.get_server_url',
74 Mock(return_value='https://example.com'))
74 Mock(return_value='https://example.com'))
75 @patch.object(db.User, 'get_by_username',
76 Mock(return_value=Mock(return_value=1)))
75 def call_vcs_operation_context(**kwargs_override):
77 def call_vcs_operation_context(**kwargs_override):
76 kwargs = {
78 kwargs = {
77 'repo_name': 'stub_repo_name',
79 'repo_name': 'stub_repo_name',
78 'username': 'stub_username',
80 'username': 'stub_username',
79 'action': 'stub_action',
81 'action': 'stub_action',
80 'scm': 'stub_scm',
82 'scm': 'stub_scm',
81 'check_locking': False,
83 'check_locking': False,
82 }
84 }
83 kwargs.update(kwargs_override)
85 kwargs.update(kwargs_override)
84 config_file_patch = patch.dict(
86 config_file_patch = patch.dict(
85 'rhodecode.CONFIG', {'__file__': 'stub_ini_filename'})
87 'rhodecode.CONFIG', {'__file__': 'stub_ini_filename'})
86 settings_patch = patch.object(base, 'VcsSettingsModel')
88 settings_patch = patch.object(base, 'VcsSettingsModel')
87 with config_file_patch, settings_patch as settings_mock:
89 with config_file_patch, settings_patch as settings_mock:
88 result = base.vcs_operation_context(
90 result = base.vcs_operation_context(
89 environ={'HTTP_USER_AGENT': 'client-text-v1.1',
91 environ={'HTTP_USER_AGENT': 'client-text-v1.1',
90 'REMOTE_ADDR': '1.2.3.4'}, **kwargs)
92 'REMOTE_ADDR': '1.2.3.4'}, **kwargs)
91 settings_mock.assert_called_once_with(repo='stub_repo_name')
93 settings_mock.assert_called_once_with(repo='stub_repo_name')
92 return result
94 return result
93
95
94
96
@@ -1,141 +1,143 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23 from rhodecode.model.db import Session, UserLog
23 from rhodecode.model.db import Session, UserLog
24 from rhodecode.lib import hooks_base, utils2
24 from rhodecode.lib import hooks_base, utils2
25
25
26
26
27 def test_post_push_truncates_commits(user_regular, repo_stub):
27 def test_post_push_truncates_commits(user_regular, repo_stub):
28 extras = {
28 extras = {
29 'ip': '127.0.0.1',
29 'ip': '127.0.0.1',
30 'username': user_regular.username,
30 'username': user_regular.username,
31 'user_id': user_regular.user_id,
31 'action': 'push_local',
32 'action': 'push_local',
32 'repository': repo_stub.repo_name,
33 'repository': repo_stub.repo_name,
33 'scm': 'git',
34 'scm': 'git',
34 'config': '',
35 'config': '',
35 'server_url': 'http://example.com',
36 'server_url': 'http://example.com',
36 'make_lock': None,
37 'make_lock': None,
37 'user_agent': 'some-client',
38 'user_agent': 'some-client',
38 'locked_by': [None],
39 'locked_by': [None],
39 'commit_ids': ['abcde12345' * 4] * 30000,
40 'commit_ids': ['abcde12345' * 4] * 30000,
40 'is_shadow_repo': False,
41 'is_shadow_repo': False,
41 }
42 }
42 extras = utils2.AttributeDict(extras)
43 extras = utils2.AttributeDict(extras)
43
44
44 hooks_base.post_push(extras)
45 hooks_base.post_push(extras)
45
46
46 # Calculate appropriate action string here
47 # Calculate appropriate action string here
47 commit_ids = extras.commit_ids[:400]
48 commit_ids = extras.commit_ids[:400]
48
49
49 entry = UserLog.query().order_by('-user_log_id').first()
50 entry = UserLog.query().order_by('-user_log_id').first()
50 assert entry.action == 'user.push'
51 assert entry.action == 'user.push'
51 assert entry.action_data['commit_ids'] == commit_ids
52 assert entry.action_data['commit_ids'] == commit_ids
52 Session().delete(entry)
53 Session().delete(entry)
53 Session().commit()
54 Session().commit()
54
55
55
56
56 def assert_called_with_mock(callable_, expected_mock_name):
57 def assert_called_with_mock(callable_, expected_mock_name):
57 mock_obj = callable_.call_args[0][0]
58 mock_obj = callable_.call_args[0][0]
58 mock_name = mock_obj._mock_new_parent._mock_new_name
59 mock_name = mock_obj._mock_new_parent._mock_new_name
59 assert mock_name == expected_mock_name
60 assert mock_name == expected_mock_name
60
61
61
62
62 @pytest.fixture
63 @pytest.fixture
63 def hook_extras(user_regular, repo_stub):
64 def hook_extras(user_regular, repo_stub):
64 extras = utils2.AttributeDict({
65 extras = utils2.AttributeDict({
65 'ip': '127.0.0.1',
66 'ip': '127.0.0.1',
66 'username': user_regular.username,
67 'username': user_regular.username,
68 'user_id': user_regular.user_id,
67 'action': 'push',
69 'action': 'push',
68 'repository': repo_stub.repo_name,
70 'repository': repo_stub.repo_name,
69 'scm': '',
71 'scm': '',
70 'config': '',
72 'config': '',
71 'server_url': 'http://example.com',
73 'server_url': 'http://example.com',
72 'make_lock': None,
74 'make_lock': None,
73 'user_agent': 'some-client',
75 'user_agent': 'some-client',
74 'locked_by': [None],
76 'locked_by': [None],
75 'commit_ids': [],
77 'commit_ids': [],
76 'is_shadow_repo': False,
78 'is_shadow_repo': False,
77 })
79 })
78 return extras
80 return extras
79
81
80
82
81 @pytest.mark.parametrize('func, extension, event', [
83 @pytest.mark.parametrize('func, extension, event', [
82 (hooks_base.pre_push, 'pre_push_extension', 'RepoPrePushEvent'),
84 (hooks_base.pre_push, 'pre_push_extension', 'RepoPrePushEvent'),
83 (hooks_base.post_push, 'post_pull_extension', 'RepoPushEvent'),
85 (hooks_base.post_push, 'post_pull_extension', 'RepoPushEvent'),
84 (hooks_base.pre_pull, 'pre_pull_extension', 'RepoPrePullEvent'),
86 (hooks_base.pre_pull, 'pre_pull_extension', 'RepoPrePullEvent'),
85 (hooks_base.post_pull, 'post_push_extension', 'RepoPullEvent'),
87 (hooks_base.post_pull, 'post_push_extension', 'RepoPullEvent'),
86 ])
88 ])
87 def test_hooks_propagate(func, extension, event, hook_extras):
89 def test_hooks_propagate(func, extension, event, hook_extras):
88 """
90 """
89 Tests that our hook code propagates to rhodecode extensions and triggers
91 Tests that our hook code propagates to rhodecode extensions and triggers
90 the appropriate event.
92 the appropriate event.
91 """
93 """
92 extension_mock = mock.Mock()
94 extension_mock = mock.Mock()
93 events_mock = mock.Mock()
95 events_mock = mock.Mock()
94 patches = {
96 patches = {
95 'Repository': mock.Mock(),
97 'Repository': mock.Mock(),
96 'events': events_mock,
98 'events': events_mock,
97 extension: extension_mock,
99 extension: extension_mock,
98 }
100 }
99
101
100 # Clear shadow repo flag.
102 # Clear shadow repo flag.
101 hook_extras.is_shadow_repo = False
103 hook_extras.is_shadow_repo = False
102
104
103 # Execute hook function.
105 # Execute hook function.
104 with mock.patch.multiple(hooks_base, **patches):
106 with mock.patch.multiple(hooks_base, **patches):
105 func(hook_extras)
107 func(hook_extras)
106
108
107 # Assert that extensions are called and event was fired.
109 # Assert that extensions are called and event was fired.
108 extension_mock.called_once()
110 extension_mock.called_once()
109 assert_called_with_mock(events_mock.trigger, event)
111 assert_called_with_mock(events_mock.trigger, event)
110
112
111
113
112 @pytest.mark.parametrize('func, extension, event', [
114 @pytest.mark.parametrize('func, extension, event', [
113 (hooks_base.pre_push, 'pre_push_extension', 'RepoPrePushEvent'),
115 (hooks_base.pre_push, 'pre_push_extension', 'RepoPrePushEvent'),
114 (hooks_base.post_push, 'post_pull_extension', 'RepoPushEvent'),
116 (hooks_base.post_push, 'post_pull_extension', 'RepoPushEvent'),
115 (hooks_base.pre_pull, 'pre_pull_extension', 'RepoPrePullEvent'),
117 (hooks_base.pre_pull, 'pre_pull_extension', 'RepoPrePullEvent'),
116 (hooks_base.post_pull, 'post_push_extension', 'RepoPullEvent'),
118 (hooks_base.post_pull, 'post_push_extension', 'RepoPullEvent'),
117 ])
119 ])
118 def test_hooks_propagates_not_on_shadow(func, extension, event, hook_extras):
120 def test_hooks_propagates_not_on_shadow(func, extension, event, hook_extras):
119 """
121 """
120 If hooks are called by a request to a shadow repo we only want to run our
122 If hooks are called by a request to a shadow repo we only want to run our
121 internal hooks code but not external ones like rhodecode extensions or
123 internal hooks code but not external ones like rhodecode extensions or
122 trigger an event.
124 trigger an event.
123 """
125 """
124 extension_mock = mock.Mock()
126 extension_mock = mock.Mock()
125 events_mock = mock.Mock()
127 events_mock = mock.Mock()
126 patches = {
128 patches = {
127 'Repository': mock.Mock(),
129 'Repository': mock.Mock(),
128 'events': events_mock,
130 'events': events_mock,
129 extension: extension_mock,
131 extension: extension_mock,
130 }
132 }
131
133
132 # Set shadow repo flag.
134 # Set shadow repo flag.
133 hook_extras.is_shadow_repo = True
135 hook_extras.is_shadow_repo = True
134
136
135 # Execute hook function.
137 # Execute hook function.
136 with mock.patch.multiple(hooks_base, **patches):
138 with mock.patch.multiple(hooks_base, **patches):
137 func(hook_extras)
139 func(hook_extras)
138
140
139 # Assert that extensions are *not* called and event was *not* fired.
141 # Assert that extensions are *not* called and event was *not* fired.
140 assert not extension_mock.called
142 assert not extension_mock.called
141 assert not events_mock.trigger.called
143 assert not events_mock.trigger.called
@@ -1,859 +1,860 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23 import textwrap
23 import textwrap
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.utils2 import safe_unicode
26 from rhodecode.lib.utils2 import safe_unicode
27 from rhodecode.lib.vcs.backends import get_backend
27 from rhodecode.lib.vcs.backends import get_backend
28 from rhodecode.lib.vcs.backends.base import (
28 from rhodecode.lib.vcs.backends.base import (
29 MergeResponse, MergeFailureReason, Reference)
29 MergeResponse, MergeFailureReason, Reference)
30 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import RepositoryError
31 from rhodecode.lib.vcs.nodes import FileNode
31 from rhodecode.lib.vcs.nodes import FileNode
32 from rhodecode.model.comment import CommentsModel
32 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.db import PullRequest, Session
33 from rhodecode.model.db import PullRequest, Session
34 from rhodecode.model.pull_request import PullRequestModel
34 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.user import UserModel
35 from rhodecode.model.user import UserModel
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
37
37
38
38
39 pytestmark = [
39 pytestmark = [
40 pytest.mark.backends("git", "hg"),
40 pytest.mark.backends("git", "hg"),
41 ]
41 ]
42
42
43
43
44 @pytest.mark.usefixtures('config_stub')
44 @pytest.mark.usefixtures('config_stub')
45 class TestPullRequestModel(object):
45 class TestPullRequestModel(object):
46
46
47 @pytest.fixture
47 @pytest.fixture
48 def pull_request(self, request, backend, pr_util):
48 def pull_request(self, request, backend, pr_util):
49 """
49 """
50 A pull request combined with multiples patches.
50 A pull request combined with multiples patches.
51 """
51 """
52 BackendClass = get_backend(backend.alias)
52 BackendClass = get_backend(backend.alias)
53 self.merge_patcher = mock.patch.object(
53 self.merge_patcher = mock.patch.object(
54 BackendClass, 'merge', return_value=MergeResponse(
54 BackendClass, 'merge', return_value=MergeResponse(
55 False, False, None, MergeFailureReason.UNKNOWN))
55 False, False, None, MergeFailureReason.UNKNOWN))
56 self.workspace_remove_patcher = mock.patch.object(
56 self.workspace_remove_patcher = mock.patch.object(
57 BackendClass, 'cleanup_merge_workspace')
57 BackendClass, 'cleanup_merge_workspace')
58
58
59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
60 self.merge_mock = self.merge_patcher.start()
60 self.merge_mock = self.merge_patcher.start()
61 self.comment_patcher = mock.patch(
61 self.comment_patcher = mock.patch(
62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
63 self.comment_patcher.start()
63 self.comment_patcher.start()
64 self.notification_patcher = mock.patch(
64 self.notification_patcher = mock.patch(
65 'rhodecode.model.notification.NotificationModel.create')
65 'rhodecode.model.notification.NotificationModel.create')
66 self.notification_patcher.start()
66 self.notification_patcher.start()
67 self.helper_patcher = mock.patch(
67 self.helper_patcher = mock.patch(
68 'rhodecode.lib.helpers.route_path')
68 'rhodecode.lib.helpers.route_path')
69 self.helper_patcher.start()
69 self.helper_patcher.start()
70
70
71 self.hook_patcher = mock.patch.object(PullRequestModel,
71 self.hook_patcher = mock.patch.object(PullRequestModel,
72 '_trigger_pull_request_hook')
72 '_trigger_pull_request_hook')
73 self.hook_mock = self.hook_patcher.start()
73 self.hook_mock = self.hook_patcher.start()
74
74
75 self.invalidation_patcher = mock.patch(
75 self.invalidation_patcher = mock.patch(
76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
77 self.invalidation_mock = self.invalidation_patcher.start()
77 self.invalidation_mock = self.invalidation_patcher.start()
78
78
79 self.pull_request = pr_util.create_pull_request(
79 self.pull_request = pr_util.create_pull_request(
80 mergeable=True, name_suffix=u'Δ…Δ‡')
80 mergeable=True, name_suffix=u'Δ…Δ‡')
81 self.source_commit = self.pull_request.source_ref_parts.commit_id
81 self.source_commit = self.pull_request.source_ref_parts.commit_id
82 self.target_commit = self.pull_request.target_ref_parts.commit_id
82 self.target_commit = self.pull_request.target_ref_parts.commit_id
83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
84
84
85 @request.addfinalizer
85 @request.addfinalizer
86 def cleanup_pull_request():
86 def cleanup_pull_request():
87 calls = [mock.call(
87 calls = [mock.call(
88 self.pull_request, self.pull_request.author, 'create')]
88 self.pull_request, self.pull_request.author, 'create')]
89 self.hook_mock.assert_has_calls(calls)
89 self.hook_mock.assert_has_calls(calls)
90
90
91 self.workspace_remove_patcher.stop()
91 self.workspace_remove_patcher.stop()
92 self.merge_patcher.stop()
92 self.merge_patcher.stop()
93 self.comment_patcher.stop()
93 self.comment_patcher.stop()
94 self.notification_patcher.stop()
94 self.notification_patcher.stop()
95 self.helper_patcher.stop()
95 self.helper_patcher.stop()
96 self.hook_patcher.stop()
96 self.hook_patcher.stop()
97 self.invalidation_patcher.stop()
97 self.invalidation_patcher.stop()
98
98
99 return self.pull_request
99 return self.pull_request
100
100
101 def test_get_all(self, pull_request):
101 def test_get_all(self, pull_request):
102 prs = PullRequestModel().get_all(pull_request.target_repo)
102 prs = PullRequestModel().get_all(pull_request.target_repo)
103 assert isinstance(prs, list)
103 assert isinstance(prs, list)
104 assert len(prs) == 1
104 assert len(prs) == 1
105
105
106 def test_count_all(self, pull_request):
106 def test_count_all(self, pull_request):
107 pr_count = PullRequestModel().count_all(pull_request.target_repo)
107 pr_count = PullRequestModel().count_all(pull_request.target_repo)
108 assert pr_count == 1
108 assert pr_count == 1
109
109
110 def test_get_awaiting_review(self, pull_request):
110 def test_get_awaiting_review(self, pull_request):
111 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
111 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
112 assert isinstance(prs, list)
112 assert isinstance(prs, list)
113 assert len(prs) == 1
113 assert len(prs) == 1
114
114
115 def test_count_awaiting_review(self, pull_request):
115 def test_count_awaiting_review(self, pull_request):
116 pr_count = PullRequestModel().count_awaiting_review(
116 pr_count = PullRequestModel().count_awaiting_review(
117 pull_request.target_repo)
117 pull_request.target_repo)
118 assert pr_count == 1
118 assert pr_count == 1
119
119
120 def test_get_awaiting_my_review(self, pull_request):
120 def test_get_awaiting_my_review(self, pull_request):
121 PullRequestModel().update_reviewers(
121 PullRequestModel().update_reviewers(
122 pull_request, [(pull_request.author, ['author'], False)],
122 pull_request, [(pull_request.author, ['author'], False)],
123 pull_request.author)
123 pull_request.author)
124 prs = PullRequestModel().get_awaiting_my_review(
124 prs = PullRequestModel().get_awaiting_my_review(
125 pull_request.target_repo, user_id=pull_request.author.user_id)
125 pull_request.target_repo, user_id=pull_request.author.user_id)
126 assert isinstance(prs, list)
126 assert isinstance(prs, list)
127 assert len(prs) == 1
127 assert len(prs) == 1
128
128
129 def test_count_awaiting_my_review(self, pull_request):
129 def test_count_awaiting_my_review(self, pull_request):
130 PullRequestModel().update_reviewers(
130 PullRequestModel().update_reviewers(
131 pull_request, [(pull_request.author, ['author'], False)],
131 pull_request, [(pull_request.author, ['author'], False)],
132 pull_request.author)
132 pull_request.author)
133 pr_count = PullRequestModel().count_awaiting_my_review(
133 pr_count = PullRequestModel().count_awaiting_my_review(
134 pull_request.target_repo, user_id=pull_request.author.user_id)
134 pull_request.target_repo, user_id=pull_request.author.user_id)
135 assert pr_count == 1
135 assert pr_count == 1
136
136
137 def test_delete_calls_cleanup_merge(self, pull_request):
137 def test_delete_calls_cleanup_merge(self, pull_request):
138 PullRequestModel().delete(pull_request, pull_request.author)
138 PullRequestModel().delete(pull_request, pull_request.author)
139
139
140 self.workspace_remove_mock.assert_called_once_with(
140 self.workspace_remove_mock.assert_called_once_with(
141 self.workspace_id)
141 self.workspace_id)
142
142
143 def test_close_calls_cleanup_and_hook(self, pull_request):
143 def test_close_calls_cleanup_and_hook(self, pull_request):
144 PullRequestModel().close_pull_request(
144 PullRequestModel().close_pull_request(
145 pull_request, pull_request.author)
145 pull_request, pull_request.author)
146
146
147 self.workspace_remove_mock.assert_called_once_with(
147 self.workspace_remove_mock.assert_called_once_with(
148 self.workspace_id)
148 self.workspace_id)
149 self.hook_mock.assert_called_with(
149 self.hook_mock.assert_called_with(
150 self.pull_request, self.pull_request.author, 'close')
150 self.pull_request, self.pull_request.author, 'close')
151
151
152 def test_merge_status(self, pull_request):
152 def test_merge_status(self, pull_request):
153 self.merge_mock.return_value = MergeResponse(
153 self.merge_mock.return_value = MergeResponse(
154 True, False, None, MergeFailureReason.NONE)
154 True, False, None, MergeFailureReason.NONE)
155
155
156 assert pull_request._last_merge_source_rev is None
156 assert pull_request._last_merge_source_rev is None
157 assert pull_request._last_merge_target_rev is None
157 assert pull_request._last_merge_target_rev is None
158 assert pull_request.last_merge_status is None
158 assert pull_request.last_merge_status is None
159
159
160 status, msg = PullRequestModel().merge_status(pull_request)
160 status, msg = PullRequestModel().merge_status(pull_request)
161 assert status is True
161 assert status is True
162 assert msg.eval() == 'This pull request can be automatically merged.'
162 assert msg.eval() == 'This pull request can be automatically merged.'
163 self.merge_mock.assert_called_with(
163 self.merge_mock.assert_called_with(
164 pull_request.target_ref_parts,
164 pull_request.target_ref_parts,
165 pull_request.source_repo.scm_instance(),
165 pull_request.source_repo.scm_instance(),
166 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
166 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
167 use_rebase=False, close_branch=False)
167 use_rebase=False, close_branch=False)
168
168
169 assert pull_request._last_merge_source_rev == self.source_commit
169 assert pull_request._last_merge_source_rev == self.source_commit
170 assert pull_request._last_merge_target_rev == self.target_commit
170 assert pull_request._last_merge_target_rev == self.target_commit
171 assert pull_request.last_merge_status is MergeFailureReason.NONE
171 assert pull_request.last_merge_status is MergeFailureReason.NONE
172
172
173 self.merge_mock.reset_mock()
173 self.merge_mock.reset_mock()
174 status, msg = PullRequestModel().merge_status(pull_request)
174 status, msg = PullRequestModel().merge_status(pull_request)
175 assert status is True
175 assert status is True
176 assert msg.eval() == 'This pull request can be automatically merged.'
176 assert msg.eval() == 'This pull request can be automatically merged.'
177 assert self.merge_mock.called is False
177 assert self.merge_mock.called is False
178
178
179 def test_merge_status_known_failure(self, pull_request):
179 def test_merge_status_known_failure(self, pull_request):
180 self.merge_mock.return_value = MergeResponse(
180 self.merge_mock.return_value = MergeResponse(
181 False, False, None, MergeFailureReason.MERGE_FAILED)
181 False, False, None, MergeFailureReason.MERGE_FAILED)
182
182
183 assert pull_request._last_merge_source_rev is None
183 assert pull_request._last_merge_source_rev is None
184 assert pull_request._last_merge_target_rev is None
184 assert pull_request._last_merge_target_rev is None
185 assert pull_request.last_merge_status is None
185 assert pull_request.last_merge_status is None
186
186
187 status, msg = PullRequestModel().merge_status(pull_request)
187 status, msg = PullRequestModel().merge_status(pull_request)
188 assert status is False
188 assert status is False
189 assert (
189 assert (
190 msg.eval() ==
190 msg.eval() ==
191 'This pull request cannot be merged because of merge conflicts.')
191 'This pull request cannot be merged because of merge conflicts.')
192 self.merge_mock.assert_called_with(
192 self.merge_mock.assert_called_with(
193 pull_request.target_ref_parts,
193 pull_request.target_ref_parts,
194 pull_request.source_repo.scm_instance(),
194 pull_request.source_repo.scm_instance(),
195 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
195 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
196 use_rebase=False, close_branch=False)
196 use_rebase=False, close_branch=False)
197
197
198 assert pull_request._last_merge_source_rev == self.source_commit
198 assert pull_request._last_merge_source_rev == self.source_commit
199 assert pull_request._last_merge_target_rev == self.target_commit
199 assert pull_request._last_merge_target_rev == self.target_commit
200 assert (
200 assert (
201 pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED)
201 pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED)
202
202
203 self.merge_mock.reset_mock()
203 self.merge_mock.reset_mock()
204 status, msg = PullRequestModel().merge_status(pull_request)
204 status, msg = PullRequestModel().merge_status(pull_request)
205 assert status is False
205 assert status is False
206 assert (
206 assert (
207 msg.eval() ==
207 msg.eval() ==
208 'This pull request cannot be merged because of merge conflicts.')
208 'This pull request cannot be merged because of merge conflicts.')
209 assert self.merge_mock.called is False
209 assert self.merge_mock.called is False
210
210
211 def test_merge_status_unknown_failure(self, pull_request):
211 def test_merge_status_unknown_failure(self, pull_request):
212 self.merge_mock.return_value = MergeResponse(
212 self.merge_mock.return_value = MergeResponse(
213 False, False, None, MergeFailureReason.UNKNOWN)
213 False, False, None, MergeFailureReason.UNKNOWN)
214
214
215 assert pull_request._last_merge_source_rev is None
215 assert pull_request._last_merge_source_rev is None
216 assert pull_request._last_merge_target_rev is None
216 assert pull_request._last_merge_target_rev is None
217 assert pull_request.last_merge_status is None
217 assert pull_request.last_merge_status is None
218
218
219 status, msg = PullRequestModel().merge_status(pull_request)
219 status, msg = PullRequestModel().merge_status(pull_request)
220 assert status is False
220 assert status is False
221 assert msg.eval() == (
221 assert msg.eval() == (
222 'This pull request cannot be merged because of an unhandled'
222 'This pull request cannot be merged because of an unhandled'
223 ' exception.')
223 ' exception.')
224 self.merge_mock.assert_called_with(
224 self.merge_mock.assert_called_with(
225 pull_request.target_ref_parts,
225 pull_request.target_ref_parts,
226 pull_request.source_repo.scm_instance(),
226 pull_request.source_repo.scm_instance(),
227 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
227 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
228 use_rebase=False, close_branch=False)
228 use_rebase=False, close_branch=False)
229
229
230 assert pull_request._last_merge_source_rev is None
230 assert pull_request._last_merge_source_rev is None
231 assert pull_request._last_merge_target_rev is None
231 assert pull_request._last_merge_target_rev is None
232 assert pull_request.last_merge_status is None
232 assert pull_request.last_merge_status is None
233
233
234 self.merge_mock.reset_mock()
234 self.merge_mock.reset_mock()
235 status, msg = PullRequestModel().merge_status(pull_request)
235 status, msg = PullRequestModel().merge_status(pull_request)
236 assert status is False
236 assert status is False
237 assert msg.eval() == (
237 assert msg.eval() == (
238 'This pull request cannot be merged because of an unhandled'
238 'This pull request cannot be merged because of an unhandled'
239 ' exception.')
239 ' exception.')
240 assert self.merge_mock.called is True
240 assert self.merge_mock.called is True
241
241
242 def test_merge_status_when_target_is_locked(self, pull_request):
242 def test_merge_status_when_target_is_locked(self, pull_request):
243 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
243 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
244 status, msg = PullRequestModel().merge_status(pull_request)
244 status, msg = PullRequestModel().merge_status(pull_request)
245 assert status is False
245 assert status is False
246 assert msg.eval() == (
246 assert msg.eval() == (
247 'This pull request cannot be merged because the target repository'
247 'This pull request cannot be merged because the target repository'
248 ' is locked.')
248 ' is locked.')
249
249
250 def test_merge_status_requirements_check_target(self, pull_request):
250 def test_merge_status_requirements_check_target(self, pull_request):
251
251
252 def has_largefiles(self, repo):
252 def has_largefiles(self, repo):
253 return repo == pull_request.source_repo
253 return repo == pull_request.source_repo
254
254
255 patcher = mock.patch.object(
255 patcher = mock.patch.object(
256 PullRequestModel, '_has_largefiles', has_largefiles)
256 PullRequestModel, '_has_largefiles', has_largefiles)
257 with patcher:
257 with patcher:
258 status, msg = PullRequestModel().merge_status(pull_request)
258 status, msg = PullRequestModel().merge_status(pull_request)
259
259
260 assert status is False
260 assert status is False
261 assert msg == 'Target repository large files support is disabled.'
261 assert msg == 'Target repository large files support is disabled.'
262
262
263 def test_merge_status_requirements_check_source(self, pull_request):
263 def test_merge_status_requirements_check_source(self, pull_request):
264
264
265 def has_largefiles(self, repo):
265 def has_largefiles(self, repo):
266 return repo == pull_request.target_repo
266 return repo == pull_request.target_repo
267
267
268 patcher = mock.patch.object(
268 patcher = mock.patch.object(
269 PullRequestModel, '_has_largefiles', has_largefiles)
269 PullRequestModel, '_has_largefiles', has_largefiles)
270 with patcher:
270 with patcher:
271 status, msg = PullRequestModel().merge_status(pull_request)
271 status, msg = PullRequestModel().merge_status(pull_request)
272
272
273 assert status is False
273 assert status is False
274 assert msg == 'Source repository large files support is disabled.'
274 assert msg == 'Source repository large files support is disabled.'
275
275
276 def test_merge(self, pull_request, merge_extras):
276 def test_merge(self, pull_request, merge_extras):
277 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
277 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
278 merge_ref = Reference(
278 merge_ref = Reference(
279 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
279 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
280 self.merge_mock.return_value = MergeResponse(
280 self.merge_mock.return_value = MergeResponse(
281 True, True, merge_ref, MergeFailureReason.NONE)
281 True, True, merge_ref, MergeFailureReason.NONE)
282
282
283 merge_extras['repository'] = pull_request.target_repo.repo_name
283 merge_extras['repository'] = pull_request.target_repo.repo_name
284 PullRequestModel().merge(
284 PullRequestModel().merge(
285 pull_request, pull_request.author, extras=merge_extras)
285 pull_request, pull_request.author, extras=merge_extras)
286
286
287 message = (
287 message = (
288 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
288 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
289 u'\n\n {pr_title}'.format(
289 u'\n\n {pr_title}'.format(
290 pr_id=pull_request.pull_request_id,
290 pr_id=pull_request.pull_request_id,
291 source_repo=safe_unicode(
291 source_repo=safe_unicode(
292 pull_request.source_repo.scm_instance().name),
292 pull_request.source_repo.scm_instance().name),
293 source_ref_name=pull_request.source_ref_parts.name,
293 source_ref_name=pull_request.source_ref_parts.name,
294 pr_title=safe_unicode(pull_request.title)
294 pr_title=safe_unicode(pull_request.title)
295 )
295 )
296 )
296 )
297 self.merge_mock.assert_called_with(
297 self.merge_mock.assert_called_with(
298 pull_request.target_ref_parts,
298 pull_request.target_ref_parts,
299 pull_request.source_repo.scm_instance(),
299 pull_request.source_repo.scm_instance(),
300 pull_request.source_ref_parts, self.workspace_id,
300 pull_request.source_ref_parts, self.workspace_id,
301 user_name=user.username, user_email=user.email, message=message,
301 user_name=user.username, user_email=user.email, message=message,
302 use_rebase=False, close_branch=False
302 use_rebase=False, close_branch=False
303 )
303 )
304 self.invalidation_mock.assert_called_once_with(
304 self.invalidation_mock.assert_called_once_with(
305 pull_request.target_repo.repo_name)
305 pull_request.target_repo.repo_name)
306
306
307 self.hook_mock.assert_called_with(
307 self.hook_mock.assert_called_with(
308 self.pull_request, self.pull_request.author, 'merge')
308 self.pull_request, self.pull_request.author, 'merge')
309
309
310 pull_request = PullRequest.get(pull_request.pull_request_id)
310 pull_request = PullRequest.get(pull_request.pull_request_id)
311 assert (
311 assert (
312 pull_request.merge_rev ==
312 pull_request.merge_rev ==
313 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
313 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
314
314
315 def test_merge_failed(self, pull_request, merge_extras):
315 def test_merge_failed(self, pull_request, merge_extras):
316 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
316 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
317 merge_ref = Reference(
317 merge_ref = Reference(
318 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
318 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
319 self.merge_mock.return_value = MergeResponse(
319 self.merge_mock.return_value = MergeResponse(
320 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
320 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
321
321
322 merge_extras['repository'] = pull_request.target_repo.repo_name
322 merge_extras['repository'] = pull_request.target_repo.repo_name
323 PullRequestModel().merge(
323 PullRequestModel().merge(
324 pull_request, pull_request.author, extras=merge_extras)
324 pull_request, pull_request.author, extras=merge_extras)
325
325
326 message = (
326 message = (
327 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
327 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
328 u'\n\n {pr_title}'.format(
328 u'\n\n {pr_title}'.format(
329 pr_id=pull_request.pull_request_id,
329 pr_id=pull_request.pull_request_id,
330 source_repo=safe_unicode(
330 source_repo=safe_unicode(
331 pull_request.source_repo.scm_instance().name),
331 pull_request.source_repo.scm_instance().name),
332 source_ref_name=pull_request.source_ref_parts.name,
332 source_ref_name=pull_request.source_ref_parts.name,
333 pr_title=safe_unicode(pull_request.title)
333 pr_title=safe_unicode(pull_request.title)
334 )
334 )
335 )
335 )
336 self.merge_mock.assert_called_with(
336 self.merge_mock.assert_called_with(
337 pull_request.target_ref_parts,
337 pull_request.target_ref_parts,
338 pull_request.source_repo.scm_instance(),
338 pull_request.source_repo.scm_instance(),
339 pull_request.source_ref_parts, self.workspace_id,
339 pull_request.source_ref_parts, self.workspace_id,
340 user_name=user.username, user_email=user.email, message=message,
340 user_name=user.username, user_email=user.email, message=message,
341 use_rebase=False, close_branch=False
341 use_rebase=False, close_branch=False
342 )
342 )
343
343
344 pull_request = PullRequest.get(pull_request.pull_request_id)
344 pull_request = PullRequest.get(pull_request.pull_request_id)
345 assert self.invalidation_mock.called is False
345 assert self.invalidation_mock.called is False
346 assert pull_request.merge_rev is None
346 assert pull_request.merge_rev is None
347
347
348 def test_get_commit_ids(self, pull_request):
348 def test_get_commit_ids(self, pull_request):
349 # The PR has been not merget yet, so expect an exception
349 # The PR has been not merget yet, so expect an exception
350 with pytest.raises(ValueError):
350 with pytest.raises(ValueError):
351 PullRequestModel()._get_commit_ids(pull_request)
351 PullRequestModel()._get_commit_ids(pull_request)
352
352
353 # Merge revision is in the revisions list
353 # Merge revision is in the revisions list
354 pull_request.merge_rev = pull_request.revisions[0]
354 pull_request.merge_rev = pull_request.revisions[0]
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
356 assert commit_ids == pull_request.revisions
356 assert commit_ids == pull_request.revisions
357
357
358 # Merge revision is not in the revisions list
358 # Merge revision is not in the revisions list
359 pull_request.merge_rev = 'f000' * 10
359 pull_request.merge_rev = 'f000' * 10
360 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
360 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
361 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
361 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
362
362
363 def test_get_diff_from_pr_version(self, pull_request):
363 def test_get_diff_from_pr_version(self, pull_request):
364 source_repo = pull_request.source_repo
364 source_repo = pull_request.source_repo
365 source_ref_id = pull_request.source_ref_parts.commit_id
365 source_ref_id = pull_request.source_ref_parts.commit_id
366 target_ref_id = pull_request.target_ref_parts.commit_id
366 target_ref_id = pull_request.target_ref_parts.commit_id
367 diff = PullRequestModel()._get_diff_from_pr_or_version(
367 diff = PullRequestModel()._get_diff_from_pr_or_version(
368 source_repo, source_ref_id, target_ref_id, context=6)
368 source_repo, source_ref_id, target_ref_id, context=6)
369 assert 'file_1' in diff.raw
369 assert 'file_1' in diff.raw
370
370
371 def test_generate_title_returns_unicode(self):
371 def test_generate_title_returns_unicode(self):
372 title = PullRequestModel().generate_pullrequest_title(
372 title = PullRequestModel().generate_pullrequest_title(
373 source='source-dummy',
373 source='source-dummy',
374 source_ref='source-ref-dummy',
374 source_ref='source-ref-dummy',
375 target='target-dummy',
375 target='target-dummy',
376 )
376 )
377 assert type(title) == unicode
377 assert type(title) == unicode
378
378
379
379
380 @pytest.mark.usefixtures('config_stub')
380 @pytest.mark.usefixtures('config_stub')
381 class TestIntegrationMerge(object):
381 class TestIntegrationMerge(object):
382 @pytest.mark.parametrize('extra_config', (
382 @pytest.mark.parametrize('extra_config', (
383 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
383 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
384 ))
384 ))
385 def test_merge_triggers_push_hooks(
385 def test_merge_triggers_push_hooks(
386 self, pr_util, user_admin, capture_rcextensions, merge_extras,
386 self, pr_util, user_admin, capture_rcextensions, merge_extras,
387 extra_config):
387 extra_config):
388 pull_request = pr_util.create_pull_request(
388 pull_request = pr_util.create_pull_request(
389 approved=True, mergeable=True)
389 approved=True, mergeable=True)
390 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
390 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
391 merge_extras['repository'] = pull_request.target_repo.repo_name
391 merge_extras['repository'] = pull_request.target_repo.repo_name
392 Session().commit()
392 Session().commit()
393
393
394 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
394 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
395 merge_state = PullRequestModel().merge(
395 merge_state = PullRequestModel().merge(
396 pull_request, user_admin, extras=merge_extras)
396 pull_request, user_admin, extras=merge_extras)
397
397
398 assert merge_state.executed
398 assert merge_state.executed
399 assert 'pre_push' in capture_rcextensions
399 assert 'pre_push' in capture_rcextensions
400 assert 'post_push' in capture_rcextensions
400 assert 'post_push' in capture_rcextensions
401
401
402 def test_merge_can_be_rejected_by_pre_push_hook(
402 def test_merge_can_be_rejected_by_pre_push_hook(
403 self, pr_util, user_admin, capture_rcextensions, merge_extras):
403 self, pr_util, user_admin, capture_rcextensions, merge_extras):
404 pull_request = pr_util.create_pull_request(
404 pull_request = pr_util.create_pull_request(
405 approved=True, mergeable=True)
405 approved=True, mergeable=True)
406 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
406 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
407 merge_extras['repository'] = pull_request.target_repo.repo_name
407 merge_extras['repository'] = pull_request.target_repo.repo_name
408 Session().commit()
408 Session().commit()
409
409
410 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
410 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
411 pre_pull.side_effect = RepositoryError("Disallow push!")
411 pre_pull.side_effect = RepositoryError("Disallow push!")
412 merge_status = PullRequestModel().merge(
412 merge_status = PullRequestModel().merge(
413 pull_request, user_admin, extras=merge_extras)
413 pull_request, user_admin, extras=merge_extras)
414
414
415 assert not merge_status.executed
415 assert not merge_status.executed
416 assert 'pre_push' not in capture_rcextensions
416 assert 'pre_push' not in capture_rcextensions
417 assert 'post_push' not in capture_rcextensions
417 assert 'post_push' not in capture_rcextensions
418
418
419 def test_merge_fails_if_target_is_locked(
419 def test_merge_fails_if_target_is_locked(
420 self, pr_util, user_regular, merge_extras):
420 self, pr_util, user_regular, merge_extras):
421 pull_request = pr_util.create_pull_request(
421 pull_request = pr_util.create_pull_request(
422 approved=True, mergeable=True)
422 approved=True, mergeable=True)
423 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
423 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
424 pull_request.target_repo.locked = locked_by
424 pull_request.target_repo.locked = locked_by
425 # TODO: johbo: Check if this can work based on the database, currently
425 # TODO: johbo: Check if this can work based on the database, currently
426 # all data is pre-computed, that's why just updating the DB is not
426 # all data is pre-computed, that's why just updating the DB is not
427 # enough.
427 # enough.
428 merge_extras['locked_by'] = locked_by
428 merge_extras['locked_by'] = locked_by
429 merge_extras['repository'] = pull_request.target_repo.repo_name
429 merge_extras['repository'] = pull_request.target_repo.repo_name
430 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
430 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
431 Session().commit()
431 Session().commit()
432 merge_status = PullRequestModel().merge(
432 merge_status = PullRequestModel().merge(
433 pull_request, user_regular, extras=merge_extras)
433 pull_request, user_regular, extras=merge_extras)
434 assert not merge_status.executed
434 assert not merge_status.executed
435
435
436
436
437 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
437 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
438 (False, 1, 0),
438 (False, 1, 0),
439 (True, 0, 1),
439 (True, 0, 1),
440 ])
440 ])
441 def test_outdated_comments(
441 def test_outdated_comments(
442 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
442 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
443 pull_request = pr_util.create_pull_request()
443 pull_request = pr_util.create_pull_request()
444 pr_util.create_inline_comment(file_path='not_in_updated_diff')
444 pr_util.create_inline_comment(file_path='not_in_updated_diff')
445
445
446 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
446 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
447 pr_util.add_one_commit()
447 pr_util.add_one_commit()
448 assert_inline_comments(
448 assert_inline_comments(
449 pull_request, visible=inlines_count, outdated=outdated_count)
449 pull_request, visible=inlines_count, outdated=outdated_count)
450 outdated_comment_mock.assert_called_with(pull_request)
450 outdated_comment_mock.assert_called_with(pull_request)
451
451
452
452
453 @pytest.fixture
453 @pytest.fixture
454 def merge_extras(user_regular):
454 def merge_extras(user_regular):
455 """
455 """
456 Context for the vcs operation when running a merge.
456 Context for the vcs operation when running a merge.
457 """
457 """
458 extras = {
458 extras = {
459 'ip': '127.0.0.1',
459 'ip': '127.0.0.1',
460 'username': user_regular.username,
460 'username': user_regular.username,
461 'user_id': user_regular.user_id,
461 'action': 'push',
462 'action': 'push',
462 'repository': 'fake_target_repo_name',
463 'repository': 'fake_target_repo_name',
463 'scm': 'git',
464 'scm': 'git',
464 'config': 'fake_config_ini_path',
465 'config': 'fake_config_ini_path',
465 'make_lock': None,
466 'make_lock': None,
466 'locked_by': [None, None, None],
467 'locked_by': [None, None, None],
467 'server_url': 'http://test.example.com:5000',
468 'server_url': 'http://test.example.com:5000',
468 'hooks': ['push', 'pull'],
469 'hooks': ['push', 'pull'],
469 'is_shadow_repo': False,
470 'is_shadow_repo': False,
470 }
471 }
471 return extras
472 return extras
472
473
473
474
474 @pytest.mark.usefixtures('config_stub')
475 @pytest.mark.usefixtures('config_stub')
475 class TestUpdateCommentHandling(object):
476 class TestUpdateCommentHandling(object):
476
477
477 @pytest.fixture(autouse=True, scope='class')
478 @pytest.fixture(autouse=True, scope='class')
478 def enable_outdated_comments(self, request, baseapp):
479 def enable_outdated_comments(self, request, baseapp):
479 config_patch = mock.patch.dict(
480 config_patch = mock.patch.dict(
480 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
481 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
481 config_patch.start()
482 config_patch.start()
482
483
483 @request.addfinalizer
484 @request.addfinalizer
484 def cleanup():
485 def cleanup():
485 config_patch.stop()
486 config_patch.stop()
486
487
487 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
488 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
488 commits = [
489 commits = [
489 {'message': 'a'},
490 {'message': 'a'},
490 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
491 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
491 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
492 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
492 ]
493 ]
493 pull_request = pr_util.create_pull_request(
494 pull_request = pr_util.create_pull_request(
494 commits=commits, target_head='a', source_head='b', revisions=['b'])
495 commits=commits, target_head='a', source_head='b', revisions=['b'])
495 pr_util.create_inline_comment(file_path='file_b')
496 pr_util.create_inline_comment(file_path='file_b')
496 pr_util.add_one_commit(head='c')
497 pr_util.add_one_commit(head='c')
497
498
498 assert_inline_comments(pull_request, visible=1, outdated=0)
499 assert_inline_comments(pull_request, visible=1, outdated=0)
499
500
500 def test_comment_stays_unflagged_on_change_above(self, pr_util):
501 def test_comment_stays_unflagged_on_change_above(self, pr_util):
501 original_content = ''.join(
502 original_content = ''.join(
502 ['line {}\n'.format(x) for x in range(1, 11)])
503 ['line {}\n'.format(x) for x in range(1, 11)])
503 updated_content = 'new_line_at_top\n' + original_content
504 updated_content = 'new_line_at_top\n' + original_content
504 commits = [
505 commits = [
505 {'message': 'a'},
506 {'message': 'a'},
506 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
507 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
507 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
508 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
508 ]
509 ]
509 pull_request = pr_util.create_pull_request(
510 pull_request = pr_util.create_pull_request(
510 commits=commits, target_head='a', source_head='b', revisions=['b'])
511 commits=commits, target_head='a', source_head='b', revisions=['b'])
511
512
512 with outdated_comments_patcher():
513 with outdated_comments_patcher():
513 comment = pr_util.create_inline_comment(
514 comment = pr_util.create_inline_comment(
514 line_no=u'n8', file_path='file_b')
515 line_no=u'n8', file_path='file_b')
515 pr_util.add_one_commit(head='c')
516 pr_util.add_one_commit(head='c')
516
517
517 assert_inline_comments(pull_request, visible=1, outdated=0)
518 assert_inline_comments(pull_request, visible=1, outdated=0)
518 assert comment.line_no == u'n9'
519 assert comment.line_no == u'n9'
519
520
520 def test_comment_stays_unflagged_on_change_below(self, pr_util):
521 def test_comment_stays_unflagged_on_change_below(self, pr_util):
521 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
522 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
522 updated_content = original_content + 'new_line_at_end\n'
523 updated_content = original_content + 'new_line_at_end\n'
523 commits = [
524 commits = [
524 {'message': 'a'},
525 {'message': 'a'},
525 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
526 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
526 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
527 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
527 ]
528 ]
528 pull_request = pr_util.create_pull_request(
529 pull_request = pr_util.create_pull_request(
529 commits=commits, target_head='a', source_head='b', revisions=['b'])
530 commits=commits, target_head='a', source_head='b', revisions=['b'])
530 pr_util.create_inline_comment(file_path='file_b')
531 pr_util.create_inline_comment(file_path='file_b')
531 pr_util.add_one_commit(head='c')
532 pr_util.add_one_commit(head='c')
532
533
533 assert_inline_comments(pull_request, visible=1, outdated=0)
534 assert_inline_comments(pull_request, visible=1, outdated=0)
534
535
535 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
536 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
536 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
537 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
537 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
538 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
538 change_lines = list(base_lines)
539 change_lines = list(base_lines)
539 change_lines.insert(6, 'line 6a added\n')
540 change_lines.insert(6, 'line 6a added\n')
540
541
541 # Changes on the last line of sight
542 # Changes on the last line of sight
542 update_lines = list(change_lines)
543 update_lines = list(change_lines)
543 update_lines[0] = 'line 1 changed\n'
544 update_lines[0] = 'line 1 changed\n'
544 update_lines[-1] = 'line 12 changed\n'
545 update_lines[-1] = 'line 12 changed\n'
545
546
546 def file_b(lines):
547 def file_b(lines):
547 return FileNode('file_b', ''.join(lines))
548 return FileNode('file_b', ''.join(lines))
548
549
549 commits = [
550 commits = [
550 {'message': 'a', 'added': [file_b(base_lines)]},
551 {'message': 'a', 'added': [file_b(base_lines)]},
551 {'message': 'b', 'changed': [file_b(change_lines)]},
552 {'message': 'b', 'changed': [file_b(change_lines)]},
552 {'message': 'c', 'changed': [file_b(update_lines)]},
553 {'message': 'c', 'changed': [file_b(update_lines)]},
553 ]
554 ]
554
555
555 pull_request = pr_util.create_pull_request(
556 pull_request = pr_util.create_pull_request(
556 commits=commits, target_head='a', source_head='b', revisions=['b'])
557 commits=commits, target_head='a', source_head='b', revisions=['b'])
557 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
558 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
558
559
559 with outdated_comments_patcher():
560 with outdated_comments_patcher():
560 pr_util.add_one_commit(head='c')
561 pr_util.add_one_commit(head='c')
561 assert_inline_comments(pull_request, visible=0, outdated=1)
562 assert_inline_comments(pull_request, visible=0, outdated=1)
562
563
563 @pytest.mark.parametrize("change, content", [
564 @pytest.mark.parametrize("change, content", [
564 ('changed', 'changed\n'),
565 ('changed', 'changed\n'),
565 ('removed', ''),
566 ('removed', ''),
566 ], ids=['changed', 'removed'])
567 ], ids=['changed', 'removed'])
567 def test_comment_flagged_on_change(self, pr_util, change, content):
568 def test_comment_flagged_on_change(self, pr_util, change, content):
568 commits = [
569 commits = [
569 {'message': 'a'},
570 {'message': 'a'},
570 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
571 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
571 {'message': 'c', change: [FileNode('file_b', content)]},
572 {'message': 'c', change: [FileNode('file_b', content)]},
572 ]
573 ]
573 pull_request = pr_util.create_pull_request(
574 pull_request = pr_util.create_pull_request(
574 commits=commits, target_head='a', source_head='b', revisions=['b'])
575 commits=commits, target_head='a', source_head='b', revisions=['b'])
575 pr_util.create_inline_comment(file_path='file_b')
576 pr_util.create_inline_comment(file_path='file_b')
576
577
577 with outdated_comments_patcher():
578 with outdated_comments_patcher():
578 pr_util.add_one_commit(head='c')
579 pr_util.add_one_commit(head='c')
579 assert_inline_comments(pull_request, visible=0, outdated=1)
580 assert_inline_comments(pull_request, visible=0, outdated=1)
580
581
581
582
582 @pytest.mark.usefixtures('config_stub')
583 @pytest.mark.usefixtures('config_stub')
583 class TestUpdateChangedFiles(object):
584 class TestUpdateChangedFiles(object):
584
585
585 def test_no_changes_on_unchanged_diff(self, pr_util):
586 def test_no_changes_on_unchanged_diff(self, pr_util):
586 commits = [
587 commits = [
587 {'message': 'a'},
588 {'message': 'a'},
588 {'message': 'b',
589 {'message': 'b',
589 'added': [FileNode('file_b', 'test_content b\n')]},
590 'added': [FileNode('file_b', 'test_content b\n')]},
590 {'message': 'c',
591 {'message': 'c',
591 'added': [FileNode('file_c', 'test_content c\n')]},
592 'added': [FileNode('file_c', 'test_content c\n')]},
592 ]
593 ]
593 # open a PR from a to b, adding file_b
594 # open a PR from a to b, adding file_b
594 pull_request = pr_util.create_pull_request(
595 pull_request = pr_util.create_pull_request(
595 commits=commits, target_head='a', source_head='b', revisions=['b'],
596 commits=commits, target_head='a', source_head='b', revisions=['b'],
596 name_suffix='per-file-review')
597 name_suffix='per-file-review')
597
598
598 # modify PR adding new file file_c
599 # modify PR adding new file file_c
599 pr_util.add_one_commit(head='c')
600 pr_util.add_one_commit(head='c')
600
601
601 assert_pr_file_changes(
602 assert_pr_file_changes(
602 pull_request,
603 pull_request,
603 added=['file_c'],
604 added=['file_c'],
604 modified=[],
605 modified=[],
605 removed=[])
606 removed=[])
606
607
607 def test_modify_and_undo_modification_diff(self, pr_util):
608 def test_modify_and_undo_modification_diff(self, pr_util):
608 commits = [
609 commits = [
609 {'message': 'a'},
610 {'message': 'a'},
610 {'message': 'b',
611 {'message': 'b',
611 'added': [FileNode('file_b', 'test_content b\n')]},
612 'added': [FileNode('file_b', 'test_content b\n')]},
612 {'message': 'c',
613 {'message': 'c',
613 'changed': [FileNode('file_b', 'test_content b modified\n')]},
614 'changed': [FileNode('file_b', 'test_content b modified\n')]},
614 {'message': 'd',
615 {'message': 'd',
615 'changed': [FileNode('file_b', 'test_content b\n')]},
616 'changed': [FileNode('file_b', 'test_content b\n')]},
616 ]
617 ]
617 # open a PR from a to b, adding file_b
618 # open a PR from a to b, adding file_b
618 pull_request = pr_util.create_pull_request(
619 pull_request = pr_util.create_pull_request(
619 commits=commits, target_head='a', source_head='b', revisions=['b'],
620 commits=commits, target_head='a', source_head='b', revisions=['b'],
620 name_suffix='per-file-review')
621 name_suffix='per-file-review')
621
622
622 # modify PR modifying file file_b
623 # modify PR modifying file file_b
623 pr_util.add_one_commit(head='c')
624 pr_util.add_one_commit(head='c')
624
625
625 assert_pr_file_changes(
626 assert_pr_file_changes(
626 pull_request,
627 pull_request,
627 added=[],
628 added=[],
628 modified=['file_b'],
629 modified=['file_b'],
629 removed=[])
630 removed=[])
630
631
631 # move the head again to d, which rollbacks change,
632 # move the head again to d, which rollbacks change,
632 # meaning we should indicate no changes
633 # meaning we should indicate no changes
633 pr_util.add_one_commit(head='d')
634 pr_util.add_one_commit(head='d')
634
635
635 assert_pr_file_changes(
636 assert_pr_file_changes(
636 pull_request,
637 pull_request,
637 added=[],
638 added=[],
638 modified=[],
639 modified=[],
639 removed=[])
640 removed=[])
640
641
641 def test_updated_all_files_in_pr(self, pr_util):
642 def test_updated_all_files_in_pr(self, pr_util):
642 commits = [
643 commits = [
643 {'message': 'a'},
644 {'message': 'a'},
644 {'message': 'b', 'added': [
645 {'message': 'b', 'added': [
645 FileNode('file_a', 'test_content a\n'),
646 FileNode('file_a', 'test_content a\n'),
646 FileNode('file_b', 'test_content b\n'),
647 FileNode('file_b', 'test_content b\n'),
647 FileNode('file_c', 'test_content c\n')]},
648 FileNode('file_c', 'test_content c\n')]},
648 {'message': 'c', 'changed': [
649 {'message': 'c', 'changed': [
649 FileNode('file_a', 'test_content a changed\n'),
650 FileNode('file_a', 'test_content a changed\n'),
650 FileNode('file_b', 'test_content b changed\n'),
651 FileNode('file_b', 'test_content b changed\n'),
651 FileNode('file_c', 'test_content c changed\n')]},
652 FileNode('file_c', 'test_content c changed\n')]},
652 ]
653 ]
653 # open a PR from a to b, changing 3 files
654 # open a PR from a to b, changing 3 files
654 pull_request = pr_util.create_pull_request(
655 pull_request = pr_util.create_pull_request(
655 commits=commits, target_head='a', source_head='b', revisions=['b'],
656 commits=commits, target_head='a', source_head='b', revisions=['b'],
656 name_suffix='per-file-review')
657 name_suffix='per-file-review')
657
658
658 pr_util.add_one_commit(head='c')
659 pr_util.add_one_commit(head='c')
659
660
660 assert_pr_file_changes(
661 assert_pr_file_changes(
661 pull_request,
662 pull_request,
662 added=[],
663 added=[],
663 modified=['file_a', 'file_b', 'file_c'],
664 modified=['file_a', 'file_b', 'file_c'],
664 removed=[])
665 removed=[])
665
666
666 def test_updated_and_removed_all_files_in_pr(self, pr_util):
667 def test_updated_and_removed_all_files_in_pr(self, pr_util):
667 commits = [
668 commits = [
668 {'message': 'a'},
669 {'message': 'a'},
669 {'message': 'b', 'added': [
670 {'message': 'b', 'added': [
670 FileNode('file_a', 'test_content a\n'),
671 FileNode('file_a', 'test_content a\n'),
671 FileNode('file_b', 'test_content b\n'),
672 FileNode('file_b', 'test_content b\n'),
672 FileNode('file_c', 'test_content c\n')]},
673 FileNode('file_c', 'test_content c\n')]},
673 {'message': 'c', 'removed': [
674 {'message': 'c', 'removed': [
674 FileNode('file_a', 'test_content a changed\n'),
675 FileNode('file_a', 'test_content a changed\n'),
675 FileNode('file_b', 'test_content b changed\n'),
676 FileNode('file_b', 'test_content b changed\n'),
676 FileNode('file_c', 'test_content c changed\n')]},
677 FileNode('file_c', 'test_content c changed\n')]},
677 ]
678 ]
678 # open a PR from a to b, removing 3 files
679 # open a PR from a to b, removing 3 files
679 pull_request = pr_util.create_pull_request(
680 pull_request = pr_util.create_pull_request(
680 commits=commits, target_head='a', source_head='b', revisions=['b'],
681 commits=commits, target_head='a', source_head='b', revisions=['b'],
681 name_suffix='per-file-review')
682 name_suffix='per-file-review')
682
683
683 pr_util.add_one_commit(head='c')
684 pr_util.add_one_commit(head='c')
684
685
685 assert_pr_file_changes(
686 assert_pr_file_changes(
686 pull_request,
687 pull_request,
687 added=[],
688 added=[],
688 modified=[],
689 modified=[],
689 removed=['file_a', 'file_b', 'file_c'])
690 removed=['file_a', 'file_b', 'file_c'])
690
691
691
692
692 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
693 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
693 model = PullRequestModel()
694 model = PullRequestModel()
694 pull_request = pr_util.create_pull_request()
695 pull_request = pr_util.create_pull_request()
695 pr_util.update_source_repository()
696 pr_util.update_source_repository()
696
697
697 model.update_commits(pull_request)
698 model.update_commits(pull_request)
698
699
699 # Expect that it has a version entry now
700 # Expect that it has a version entry now
700 assert len(model.get_versions(pull_request)) == 1
701 assert len(model.get_versions(pull_request)) == 1
701
702
702
703
703 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
704 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
704 pull_request = pr_util.create_pull_request()
705 pull_request = pr_util.create_pull_request()
705 model = PullRequestModel()
706 model = PullRequestModel()
706 model.update_commits(pull_request)
707 model.update_commits(pull_request)
707
708
708 # Expect that it still has no versions
709 # Expect that it still has no versions
709 assert len(model.get_versions(pull_request)) == 0
710 assert len(model.get_versions(pull_request)) == 0
710
711
711
712
712 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
713 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
713 model = PullRequestModel()
714 model = PullRequestModel()
714 pull_request = pr_util.create_pull_request()
715 pull_request = pr_util.create_pull_request()
715 comment = pr_util.create_comment()
716 comment = pr_util.create_comment()
716 pr_util.update_source_repository()
717 pr_util.update_source_repository()
717
718
718 model.update_commits(pull_request)
719 model.update_commits(pull_request)
719
720
720 # Expect that the comment is linked to the pr version now
721 # Expect that the comment is linked to the pr version now
721 assert comment.pull_request_version == model.get_versions(pull_request)[0]
722 assert comment.pull_request_version == model.get_versions(pull_request)[0]
722
723
723
724
724 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
725 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
725 model = PullRequestModel()
726 model = PullRequestModel()
726 pull_request = pr_util.create_pull_request()
727 pull_request = pr_util.create_pull_request()
727 pr_util.update_source_repository()
728 pr_util.update_source_repository()
728 pr_util.update_source_repository()
729 pr_util.update_source_repository()
729
730
730 model.update_commits(pull_request)
731 model.update_commits(pull_request)
731
732
732 # Expect to find a new comment about the change
733 # Expect to find a new comment about the change
733 expected_message = textwrap.dedent(
734 expected_message = textwrap.dedent(
734 """\
735 """\
735 Pull request updated. Auto status change to |under_review|
736 Pull request updated. Auto status change to |under_review|
736
737
737 .. role:: added
738 .. role:: added
738 .. role:: removed
739 .. role:: removed
739 .. parsed-literal::
740 .. parsed-literal::
740
741
741 Changed commits:
742 Changed commits:
742 * :added:`1 added`
743 * :added:`1 added`
743 * :removed:`0 removed`
744 * :removed:`0 removed`
744
745
745 Changed files:
746 Changed files:
746 * `A file_2 <#a_c--92ed3b5f07b4>`_
747 * `A file_2 <#a_c--92ed3b5f07b4>`_
747
748
748 .. |under_review| replace:: *"Under Review"*"""
749 .. |under_review| replace:: *"Under Review"*"""
749 )
750 )
750 pull_request_comments = sorted(
751 pull_request_comments = sorted(
751 pull_request.comments, key=lambda c: c.modified_at)
752 pull_request.comments, key=lambda c: c.modified_at)
752 update_comment = pull_request_comments[-1]
753 update_comment = pull_request_comments[-1]
753 assert update_comment.text == expected_message
754 assert update_comment.text == expected_message
754
755
755
756
756 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
757 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
757 pull_request = pr_util.create_pull_request()
758 pull_request = pr_util.create_pull_request()
758
759
759 # Avoiding default values
760 # Avoiding default values
760 pull_request.status = PullRequest.STATUS_CLOSED
761 pull_request.status = PullRequest.STATUS_CLOSED
761 pull_request._last_merge_source_rev = "0" * 40
762 pull_request._last_merge_source_rev = "0" * 40
762 pull_request._last_merge_target_rev = "1" * 40
763 pull_request._last_merge_target_rev = "1" * 40
763 pull_request.last_merge_status = 1
764 pull_request.last_merge_status = 1
764 pull_request.merge_rev = "2" * 40
765 pull_request.merge_rev = "2" * 40
765
766
766 # Remember automatic values
767 # Remember automatic values
767 created_on = pull_request.created_on
768 created_on = pull_request.created_on
768 updated_on = pull_request.updated_on
769 updated_on = pull_request.updated_on
769
770
770 # Create a new version of the pull request
771 # Create a new version of the pull request
771 version = PullRequestModel()._create_version_from_snapshot(pull_request)
772 version = PullRequestModel()._create_version_from_snapshot(pull_request)
772
773
773 # Check attributes
774 # Check attributes
774 assert version.title == pr_util.create_parameters['title']
775 assert version.title == pr_util.create_parameters['title']
775 assert version.description == pr_util.create_parameters['description']
776 assert version.description == pr_util.create_parameters['description']
776 assert version.status == PullRequest.STATUS_CLOSED
777 assert version.status == PullRequest.STATUS_CLOSED
777
778
778 # versions get updated created_on
779 # versions get updated created_on
779 assert version.created_on != created_on
780 assert version.created_on != created_on
780
781
781 assert version.updated_on == updated_on
782 assert version.updated_on == updated_on
782 assert version.user_id == pull_request.user_id
783 assert version.user_id == pull_request.user_id
783 assert version.revisions == pr_util.create_parameters['revisions']
784 assert version.revisions == pr_util.create_parameters['revisions']
784 assert version.source_repo == pr_util.source_repository
785 assert version.source_repo == pr_util.source_repository
785 assert version.source_ref == pr_util.create_parameters['source_ref']
786 assert version.source_ref == pr_util.create_parameters['source_ref']
786 assert version.target_repo == pr_util.target_repository
787 assert version.target_repo == pr_util.target_repository
787 assert version.target_ref == pr_util.create_parameters['target_ref']
788 assert version.target_ref == pr_util.create_parameters['target_ref']
788 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
789 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
789 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
790 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
790 assert version.last_merge_status == pull_request.last_merge_status
791 assert version.last_merge_status == pull_request.last_merge_status
791 assert version.merge_rev == pull_request.merge_rev
792 assert version.merge_rev == pull_request.merge_rev
792 assert version.pull_request == pull_request
793 assert version.pull_request == pull_request
793
794
794
795
795 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
796 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
796 version1 = pr_util.create_version_of_pull_request()
797 version1 = pr_util.create_version_of_pull_request()
797 comment_linked = pr_util.create_comment(linked_to=version1)
798 comment_linked = pr_util.create_comment(linked_to=version1)
798 comment_unlinked = pr_util.create_comment()
799 comment_unlinked = pr_util.create_comment()
799 version2 = pr_util.create_version_of_pull_request()
800 version2 = pr_util.create_version_of_pull_request()
800
801
801 PullRequestModel()._link_comments_to_version(version2)
802 PullRequestModel()._link_comments_to_version(version2)
802
803
803 # Expect that only the new comment is linked to version2
804 # Expect that only the new comment is linked to version2
804 assert (
805 assert (
805 comment_unlinked.pull_request_version_id ==
806 comment_unlinked.pull_request_version_id ==
806 version2.pull_request_version_id)
807 version2.pull_request_version_id)
807 assert (
808 assert (
808 comment_linked.pull_request_version_id ==
809 comment_linked.pull_request_version_id ==
809 version1.pull_request_version_id)
810 version1.pull_request_version_id)
810 assert (
811 assert (
811 comment_unlinked.pull_request_version_id !=
812 comment_unlinked.pull_request_version_id !=
812 comment_linked.pull_request_version_id)
813 comment_linked.pull_request_version_id)
813
814
814
815
815 def test_calculate_commits():
816 def test_calculate_commits():
816 old_ids = [1, 2, 3]
817 old_ids = [1, 2, 3]
817 new_ids = [1, 3, 4, 5]
818 new_ids = [1, 3, 4, 5]
818 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
819 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
819 assert change.added == [4, 5]
820 assert change.added == [4, 5]
820 assert change.common == [1, 3]
821 assert change.common == [1, 3]
821 assert change.removed == [2]
822 assert change.removed == [2]
822 assert change.total == [1, 3, 4, 5]
823 assert change.total == [1, 3, 4, 5]
823
824
824
825
825 def assert_inline_comments(pull_request, visible=None, outdated=None):
826 def assert_inline_comments(pull_request, visible=None, outdated=None):
826 if visible is not None:
827 if visible is not None:
827 inline_comments = CommentsModel().get_inline_comments(
828 inline_comments = CommentsModel().get_inline_comments(
828 pull_request.target_repo.repo_id, pull_request=pull_request)
829 pull_request.target_repo.repo_id, pull_request=pull_request)
829 inline_cnt = CommentsModel().get_inline_comments_count(
830 inline_cnt = CommentsModel().get_inline_comments_count(
830 inline_comments)
831 inline_comments)
831 assert inline_cnt == visible
832 assert inline_cnt == visible
832 if outdated is not None:
833 if outdated is not None:
833 outdated_comments = CommentsModel().get_outdated_comments(
834 outdated_comments = CommentsModel().get_outdated_comments(
834 pull_request.target_repo.repo_id, pull_request)
835 pull_request.target_repo.repo_id, pull_request)
835 assert len(outdated_comments) == outdated
836 assert len(outdated_comments) == outdated
836
837
837
838
838 def assert_pr_file_changes(
839 def assert_pr_file_changes(
839 pull_request, added=None, modified=None, removed=None):
840 pull_request, added=None, modified=None, removed=None):
840 pr_versions = PullRequestModel().get_versions(pull_request)
841 pr_versions = PullRequestModel().get_versions(pull_request)
841 # always use first version, ie original PR to calculate changes
842 # always use first version, ie original PR to calculate changes
842 pull_request_version = pr_versions[0]
843 pull_request_version = pr_versions[0]
843 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
844 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
844 pull_request, pull_request_version)
845 pull_request, pull_request_version)
845 file_changes = PullRequestModel()._calculate_file_changes(
846 file_changes = PullRequestModel()._calculate_file_changes(
846 old_diff_data, new_diff_data)
847 old_diff_data, new_diff_data)
847
848
848 assert added == file_changes.added, \
849 assert added == file_changes.added, \
849 'expected added:%s vs value:%s' % (added, file_changes.added)
850 'expected added:%s vs value:%s' % (added, file_changes.added)
850 assert modified == file_changes.modified, \
851 assert modified == file_changes.modified, \
851 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
852 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
852 assert removed == file_changes.removed, \
853 assert removed == file_changes.removed, \
853 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
854 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
854
855
855
856
856 def outdated_comments_patcher(use_outdated=True):
857 def outdated_comments_patcher(use_outdated=True):
857 return mock.patch.object(
858 return mock.patch.object(
858 CommentsModel, 'use_outdated_comments',
859 CommentsModel, 'use_outdated_comments',
859 return_value=use_outdated)
860 return_value=use_outdated)
General Comments 0
You need to be logged in to leave comments. Login now