##// END OF EJS Templates
python3: fix urllib usage
super-admin -
r4914:49ad81de default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,171 +1,171 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 csv
22 import csv
23 import datetime
23 import datetime
24
24
25 import pytest
25 import pytest
26
26
27 from rhodecode.tests import *
27 from rhodecode.tests import *
28 from rhodecode.tests.fixture import FIXTURES
28 from rhodecode.tests.fixture import FIXTURES
29 from rhodecode.model.db import UserLog
29 from rhodecode.model.db import UserLog
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.lib.utils2 import safe_unicode
31 from rhodecode.lib.utils2 import safe_unicode
32
32
33
33
34 def route_path(name, params=None, **kwargs):
34 def route_path(name, params=None, **kwargs):
35 import urllib
35 import urllib.request, urllib.parse, urllib.error
36 from rhodecode.apps._base import ADMIN_PREFIX
36 from rhodecode.apps._base import ADMIN_PREFIX
37
37
38 base_url = {
38 base_url = {
39 'admin_home': ADMIN_PREFIX,
39 'admin_home': ADMIN_PREFIX,
40 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs',
40 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs',
41
41
42 }[name].format(**kwargs)
42 }[name].format(**kwargs)
43
43
44 if params:
44 if params:
45 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
45 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 return base_url
46 return base_url
47
47
48
48
49 @pytest.mark.usefixtures('app')
49 @pytest.mark.usefixtures('app')
50 class TestAdminController(object):
50 class TestAdminController(object):
51
51
52 @pytest.fixture(scope='class', autouse=True)
52 @pytest.fixture(scope='class', autouse=True)
53 def prepare(self, request, baseapp):
53 def prepare(self, request, baseapp):
54 UserLog.query().delete()
54 UserLog.query().delete()
55 Session().commit()
55 Session().commit()
56
56
57 def strptime(val):
57 def strptime(val):
58 fmt = '%Y-%m-%d %H:%M:%S'
58 fmt = '%Y-%m-%d %H:%M:%S'
59 if '.' not in val:
59 if '.' not in val:
60 return datetime.datetime.strptime(val, fmt)
60 return datetime.datetime.strptime(val, fmt)
61
61
62 nofrag, frag = val.split(".")
62 nofrag, frag = val.split(".")
63 date = datetime.datetime.strptime(nofrag, fmt)
63 date = datetime.datetime.strptime(nofrag, fmt)
64
64
65 frag = frag[:6] # truncate to microseconds
65 frag = frag[:6] # truncate to microseconds
66 frag += (6 - len(frag)) * '0' # add 0s
66 frag += (6 - len(frag)) * '0' # add 0s
67 return date.replace(microsecond=int(frag))
67 return date.replace(microsecond=int(frag))
68
68
69 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
69 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
70 for row in csv.DictReader(f):
70 for row in csv.DictReader(f):
71 ul = UserLog()
71 ul = UserLog()
72 for k, v in row.iteritems():
72 for k, v in row.iteritems():
73 v = safe_unicode(v)
73 v = safe_unicode(v)
74 if k == 'action_date':
74 if k == 'action_date':
75 v = strptime(v)
75 v = strptime(v)
76 if k in ['user_id', 'repository_id']:
76 if k in ['user_id', 'repository_id']:
77 # nullable due to FK problems
77 # nullable due to FK problems
78 v = None
78 v = None
79 setattr(ul, k, v)
79 setattr(ul, k, v)
80 Session().add(ul)
80 Session().add(ul)
81 Session().commit()
81 Session().commit()
82
82
83 @request.addfinalizer
83 @request.addfinalizer
84 def cleanup():
84 def cleanup():
85 UserLog.query().delete()
85 UserLog.query().delete()
86 Session().commit()
86 Session().commit()
87
87
88 def test_index(self, autologin_user):
88 def test_index(self, autologin_user):
89 response = self.app.get(route_path('admin_audit_logs'))
89 response = self.app.get(route_path('admin_audit_logs'))
90 response.mustcontain('Admin audit logs')
90 response.mustcontain('Admin audit logs')
91
91
92 def test_filter_all_entries(self, autologin_user):
92 def test_filter_all_entries(self, autologin_user):
93 response = self.app.get(route_path('admin_audit_logs'))
93 response = self.app.get(route_path('admin_audit_logs'))
94 all_count = UserLog.query().count()
94 all_count = UserLog.query().count()
95 response.mustcontain('%s entries' % all_count)
95 response.mustcontain('%s entries' % all_count)
96
96
97 def test_filter_journal_filter_exact_match_on_repository(self, autologin_user):
97 def test_filter_journal_filter_exact_match_on_repository(self, autologin_user):
98 response = self.app.get(route_path('admin_audit_logs',
98 response = self.app.get(route_path('admin_audit_logs',
99 params=dict(filter='repository:rhodecode')))
99 params=dict(filter='repository:rhodecode')))
100 response.mustcontain('3 entries')
100 response.mustcontain('3 entries')
101
101
102 def test_filter_journal_filter_exact_match_on_repository_CamelCase(self, autologin_user):
102 def test_filter_journal_filter_exact_match_on_repository_CamelCase(self, autologin_user):
103 response = self.app.get(route_path('admin_audit_logs',
103 response = self.app.get(route_path('admin_audit_logs',
104 params=dict(filter='repository:RhodeCode')))
104 params=dict(filter='repository:RhodeCode')))
105 response.mustcontain('3 entries')
105 response.mustcontain('3 entries')
106
106
107 def test_filter_journal_filter_wildcard_on_repository(self, autologin_user):
107 def test_filter_journal_filter_wildcard_on_repository(self, autologin_user):
108 response = self.app.get(route_path('admin_audit_logs',
108 response = self.app.get(route_path('admin_audit_logs',
109 params=dict(filter='repository:*test*')))
109 params=dict(filter='repository:*test*')))
110 response.mustcontain('862 entries')
110 response.mustcontain('862 entries')
111
111
112 def test_filter_journal_filter_prefix_on_repository(self, autologin_user):
112 def test_filter_journal_filter_prefix_on_repository(self, autologin_user):
113 response = self.app.get(route_path('admin_audit_logs',
113 response = self.app.get(route_path('admin_audit_logs',
114 params=dict(filter='repository:test*')))
114 params=dict(filter='repository:test*')))
115 response.mustcontain('257 entries')
115 response.mustcontain('257 entries')
116
116
117 def test_filter_journal_filter_prefix_on_repository_CamelCase(self, autologin_user):
117 def test_filter_journal_filter_prefix_on_repository_CamelCase(self, autologin_user):
118 response = self.app.get(route_path('admin_audit_logs',
118 response = self.app.get(route_path('admin_audit_logs',
119 params=dict(filter='repository:Test*')))
119 params=dict(filter='repository:Test*')))
120 response.mustcontain('257 entries')
120 response.mustcontain('257 entries')
121
121
122 def test_filter_journal_filter_prefix_on_repository_and_user(self, autologin_user):
122 def test_filter_journal_filter_prefix_on_repository_and_user(self, autologin_user):
123 response = self.app.get(route_path('admin_audit_logs',
123 response = self.app.get(route_path('admin_audit_logs',
124 params=dict(filter='repository:test* AND username:demo')))
124 params=dict(filter='repository:test* AND username:demo')))
125 response.mustcontain('130 entries')
125 response.mustcontain('130 entries')
126
126
127 def test_filter_journal_filter_prefix_on_repository_or_target_repo(self, autologin_user):
127 def test_filter_journal_filter_prefix_on_repository_or_target_repo(self, autologin_user):
128 response = self.app.get(route_path('admin_audit_logs',
128 response = self.app.get(route_path('admin_audit_logs',
129 params=dict(filter='repository:test* OR repository:rhodecode')))
129 params=dict(filter='repository:test* OR repository:rhodecode')))
130 response.mustcontain('260 entries') # 257 + 3
130 response.mustcontain('260 entries') # 257 + 3
131
131
132 def test_filter_journal_filter_exact_match_on_username(self, autologin_user):
132 def test_filter_journal_filter_exact_match_on_username(self, autologin_user):
133 response = self.app.get(route_path('admin_audit_logs',
133 response = self.app.get(route_path('admin_audit_logs',
134 params=dict(filter='username:demo')))
134 params=dict(filter='username:demo')))
135 response.mustcontain('1087 entries')
135 response.mustcontain('1087 entries')
136
136
137 def test_filter_journal_filter_exact_match_on_username_camelCase(self, autologin_user):
137 def test_filter_journal_filter_exact_match_on_username_camelCase(self, autologin_user):
138 response = self.app.get(route_path('admin_audit_logs',
138 response = self.app.get(route_path('admin_audit_logs',
139 params=dict(filter='username:DemO')))
139 params=dict(filter='username:DemO')))
140 response.mustcontain('1087 entries')
140 response.mustcontain('1087 entries')
141
141
142 def test_filter_journal_filter_wildcard_on_username(self, autologin_user):
142 def test_filter_journal_filter_wildcard_on_username(self, autologin_user):
143 response = self.app.get(route_path('admin_audit_logs',
143 response = self.app.get(route_path('admin_audit_logs',
144 params=dict(filter='username:*test*')))
144 params=dict(filter='username:*test*')))
145 entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
145 entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
146 response.mustcontain('{} entries'.format(entries_count))
146 response.mustcontain('{} entries'.format(entries_count))
147
147
148 def test_filter_journal_filter_prefix_on_username(self, autologin_user):
148 def test_filter_journal_filter_prefix_on_username(self, autologin_user):
149 response = self.app.get(route_path('admin_audit_logs',
149 response = self.app.get(route_path('admin_audit_logs',
150 params=dict(filter='username:demo*')))
150 params=dict(filter='username:demo*')))
151 response.mustcontain('1101 entries')
151 response.mustcontain('1101 entries')
152
152
153 def test_filter_journal_filter_prefix_on_user_or_other_user(self, autologin_user):
153 def test_filter_journal_filter_prefix_on_user_or_other_user(self, autologin_user):
154 response = self.app.get(route_path('admin_audit_logs',
154 response = self.app.get(route_path('admin_audit_logs',
155 params=dict(filter='username:demo OR username:volcan')))
155 params=dict(filter='username:demo OR username:volcan')))
156 response.mustcontain('1095 entries') # 1087 + 8
156 response.mustcontain('1095 entries') # 1087 + 8
157
157
158 def test_filter_journal_filter_wildcard_on_action(self, autologin_user):
158 def test_filter_journal_filter_wildcard_on_action(self, autologin_user):
159 response = self.app.get(route_path('admin_audit_logs',
159 response = self.app.get(route_path('admin_audit_logs',
160 params=dict(filter='action:*pull_request*')))
160 params=dict(filter='action:*pull_request*')))
161 response.mustcontain('187 entries')
161 response.mustcontain('187 entries')
162
162
163 def test_filter_journal_filter_on_date(self, autologin_user):
163 def test_filter_journal_filter_on_date(self, autologin_user):
164 response = self.app.get(route_path('admin_audit_logs',
164 response = self.app.get(route_path('admin_audit_logs',
165 params=dict(filter='date:20121010')))
165 params=dict(filter='date:20121010')))
166 response.mustcontain('47 entries')
166 response.mustcontain('47 entries')
167
167
168 def test_filter_journal_filter_on_date_2(self, autologin_user):
168 def test_filter_journal_filter_on_date_2(self, autologin_user):
169 response = self.app.get(route_path('admin_audit_logs',
169 response = self.app.get(route_path('admin_audit_logs',
170 params=dict(filter='date:20121020')))
170 params=dict(filter='date:20121020')))
171 response.mustcontain('17 entries')
171 response.mustcontain('17 entries')
@@ -1,85 +1,85 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import assert_session_flash
23 from rhodecode.tests import assert_session_flash
24 from rhodecode.model.settings import SettingsModel
24 from rhodecode.model.settings import SettingsModel
25
25
26
26
27 def route_path(name, params=None, **kwargs):
27 def route_path(name, params=None, **kwargs):
28 import urllib
28 import urllib.request, urllib.parse, urllib.error
29 from rhodecode.apps._base import ADMIN_PREFIX
29 from rhodecode.apps._base import ADMIN_PREFIX
30
30
31 base_url = {
31 base_url = {
32 'admin_defaults_repositories':
32 'admin_defaults_repositories':
33 ADMIN_PREFIX + '/defaults/repositories',
33 ADMIN_PREFIX + '/defaults/repositories',
34 'admin_defaults_repositories_update':
34 'admin_defaults_repositories_update':
35 ADMIN_PREFIX + '/defaults/repositories/update',
35 ADMIN_PREFIX + '/defaults/repositories/update',
36 }[name].format(**kwargs)
36 }[name].format(**kwargs)
37
37
38 if params:
38 if params:
39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
39 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 return base_url
40 return base_url
41
41
42
42
43 @pytest.mark.usefixtures("app")
43 @pytest.mark.usefixtures("app")
44 class TestDefaultsView(object):
44 class TestDefaultsView(object):
45
45
46 def test_index(self, autologin_user):
46 def test_index(self, autologin_user):
47 response = self.app.get(route_path('admin_defaults_repositories'))
47 response = self.app.get(route_path('admin_defaults_repositories'))
48 response.mustcontain('default_repo_private')
48 response.mustcontain('default_repo_private')
49 response.mustcontain('default_repo_enable_statistics')
49 response.mustcontain('default_repo_enable_statistics')
50 response.mustcontain('default_repo_enable_downloads')
50 response.mustcontain('default_repo_enable_downloads')
51 response.mustcontain('default_repo_enable_locking')
51 response.mustcontain('default_repo_enable_locking')
52
52
53 def test_update_params_true_hg(self, autologin_user, csrf_token):
53 def test_update_params_true_hg(self, autologin_user, csrf_token):
54 params = {
54 params = {
55 'default_repo_enable_locking': True,
55 'default_repo_enable_locking': True,
56 'default_repo_enable_downloads': True,
56 'default_repo_enable_downloads': True,
57 'default_repo_enable_statistics': True,
57 'default_repo_enable_statistics': True,
58 'default_repo_private': True,
58 'default_repo_private': True,
59 'default_repo_type': 'hg',
59 'default_repo_type': 'hg',
60 'csrf_token': csrf_token,
60 'csrf_token': csrf_token,
61 }
61 }
62 response = self.app.post(
62 response = self.app.post(
63 route_path('admin_defaults_repositories_update'), params=params)
63 route_path('admin_defaults_repositories_update'), params=params)
64 assert_session_flash(response, 'Default settings updated successfully')
64 assert_session_flash(response, 'Default settings updated successfully')
65
65
66 defs = SettingsModel().get_default_repo_settings()
66 defs = SettingsModel().get_default_repo_settings()
67 del params['csrf_token']
67 del params['csrf_token']
68 assert params == defs
68 assert params == defs
69
69
70 def test_update_params_false_git(self, autologin_user, csrf_token):
70 def test_update_params_false_git(self, autologin_user, csrf_token):
71 params = {
71 params = {
72 'default_repo_enable_locking': False,
72 'default_repo_enable_locking': False,
73 'default_repo_enable_downloads': False,
73 'default_repo_enable_downloads': False,
74 'default_repo_enable_statistics': False,
74 'default_repo_enable_statistics': False,
75 'default_repo_private': False,
75 'default_repo_private': False,
76 'default_repo_type': 'git',
76 'default_repo_type': 'git',
77 'csrf_token': csrf_token,
77 'csrf_token': csrf_token,
78 }
78 }
79 response = self.app.post(
79 response = self.app.post(
80 route_path('admin_defaults_repositories_update'), params=params)
80 route_path('admin_defaults_repositories_update'), params=params)
81 assert_session_flash(response, 'Default settings updated successfully')
81 assert_session_flash(response, 'Default settings updated successfully')
82
82
83 defs = SettingsModel().get_default_repo_settings()
83 defs = SettingsModel().get_default_repo_settings()
84 del params['csrf_token']
84 del params['csrf_token']
85 assert params == defs
85 assert params == defs
@@ -1,82 +1,82 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import TestController
23 from rhodecode.tests import TestController
24 from rhodecode.tests.fixture import Fixture
24 from rhodecode.tests.fixture import Fixture
25
25
26 fixture = Fixture()
26 fixture = Fixture()
27
27
28
28
29 def route_path(name, params=None, **kwargs):
29 def route_path(name, params=None, **kwargs):
30 import urllib
30 import urllib.request, urllib.parse, urllib.error
31 from rhodecode.apps._base import ADMIN_PREFIX
31 from rhodecode.apps._base import ADMIN_PREFIX
32
32
33 base_url = {
33 base_url = {
34 'admin_home': ADMIN_PREFIX,
34 'admin_home': ADMIN_PREFIX,
35 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
35 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
36 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}',
36 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}',
37 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}',
37 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}',
38 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}',
38 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}',
39
39
40 }[name].format(**kwargs)
40 }[name].format(**kwargs)
41
41
42 if params:
42 if params:
43 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
43 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
44 return base_url
44 return base_url
45
45
46
46
47 class TestAdminMainView(TestController):
47 class TestAdminMainView(TestController):
48
48
49 def test_access_admin_home(self):
49 def test_access_admin_home(self):
50 self.log_user()
50 self.log_user()
51 response = self.app.get(route_path('admin_home'), status=200)
51 response = self.app.get(route_path('admin_home'), status=200)
52 response.mustcontain("Administration area")
52 response.mustcontain("Administration area")
53
53
54 def test_redirect_pull_request_view(self, view):
54 def test_redirect_pull_request_view(self, view):
55 self.log_user()
55 self.log_user()
56 self.app.get(
56 self.app.get(
57 route_path(view, pull_request_id='xxxx'),
57 route_path(view, pull_request_id='xxxx'),
58 status=404)
58 status=404)
59
59
60 @pytest.mark.backends("git", "hg")
60 @pytest.mark.backends("git", "hg")
61 @pytest.mark.parametrize('view', [
61 @pytest.mark.parametrize('view', [
62 'pull_requests_global',
62 'pull_requests_global',
63 'pull_requests_global_0',
63 'pull_requests_global_0',
64 'pull_requests_global_1',
64 'pull_requests_global_1',
65 ])
65 ])
66 def test_redirect_pull_request_view(self, view, pr_util):
66 def test_redirect_pull_request_view(self, view, pr_util):
67 self.log_user()
67 self.log_user()
68 pull_request = pr_util.create_pull_request()
68 pull_request = pr_util.create_pull_request()
69 pull_request_id = pull_request.pull_request_id
69 pull_request_id = pull_request.pull_request_id
70 repo_name = pull_request.target_repo.repo_name
70 repo_name = pull_request.target_repo.repo_name
71
71
72 response = self.app.get(
72 response = self.app.get(
73 route_path(view, pull_request_id=pull_request_id),
73 route_path(view, pull_request_id=pull_request_id),
74 status=302)
74 status=302)
75 assert response.location.endswith(
75 assert response.location.endswith(
76 'pull-request/{}'.format(pull_request_id))
76 'pull-request/{}'.format(pull_request_id))
77
77
78 redirect_url = route_path(
78 redirect_url = route_path(
79 'pullrequest_show', repo_name=repo_name,
79 'pullrequest_show', repo_name=repo_name,
80 pull_request_id=pull_request_id)
80 pull_request_id=pull_request_id)
81
81
82 assert redirect_url in response.location
82 assert redirect_url in response.location
@@ -1,299 +1,299 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 User, UserIpMap
23 from rhodecode.model.db import User, UserIpMap
24 from rhodecode.model.meta import Session
24 from rhodecode.model.meta import Session
25 from rhodecode.model.permission import PermissionModel
25 from rhodecode.model.permission import PermissionModel
26 from rhodecode.model.ssh_key import SshKeyModel
26 from rhodecode.model.ssh_key import SshKeyModel
27 from rhodecode.tests import (
27 from rhodecode.tests import (
28 TestController, clear_cache_regions, assert_session_flash)
28 TestController, clear_cache_regions, assert_session_flash)
29
29
30
30
31 def route_path(name, params=None, **kwargs):
31 def route_path(name, params=None, **kwargs):
32 import urllib
32 import urllib.request, urllib.parse, urllib.error
33 from rhodecode.apps._base import ADMIN_PREFIX
33 from rhodecode.apps._base import ADMIN_PREFIX
34
34
35 base_url = {
35 base_url = {
36 'edit_user_ips':
36 'edit_user_ips':
37 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
37 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
38 'edit_user_ips_add':
38 'edit_user_ips_add':
39 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
39 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
40 'edit_user_ips_delete':
40 'edit_user_ips_delete':
41 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
41 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
42
42
43 'admin_permissions_application':
43 'admin_permissions_application':
44 ADMIN_PREFIX + '/permissions/application',
44 ADMIN_PREFIX + '/permissions/application',
45 'admin_permissions_application_update':
45 'admin_permissions_application_update':
46 ADMIN_PREFIX + '/permissions/application/update',
46 ADMIN_PREFIX + '/permissions/application/update',
47
47
48 'admin_permissions_global':
48 'admin_permissions_global':
49 ADMIN_PREFIX + '/permissions/global',
49 ADMIN_PREFIX + '/permissions/global',
50 'admin_permissions_global_update':
50 'admin_permissions_global_update':
51 ADMIN_PREFIX + '/permissions/global/update',
51 ADMIN_PREFIX + '/permissions/global/update',
52
52
53 'admin_permissions_object':
53 'admin_permissions_object':
54 ADMIN_PREFIX + '/permissions/object',
54 ADMIN_PREFIX + '/permissions/object',
55 'admin_permissions_object_update':
55 'admin_permissions_object_update':
56 ADMIN_PREFIX + '/permissions/object/update',
56 ADMIN_PREFIX + '/permissions/object/update',
57
57
58 'admin_permissions_ips':
58 'admin_permissions_ips':
59 ADMIN_PREFIX + '/permissions/ips',
59 ADMIN_PREFIX + '/permissions/ips',
60 'admin_permissions_overview':
60 'admin_permissions_overview':
61 ADMIN_PREFIX + '/permissions/overview',
61 ADMIN_PREFIX + '/permissions/overview',
62
62
63 'admin_permissions_ssh_keys':
63 'admin_permissions_ssh_keys':
64 ADMIN_PREFIX + '/permissions/ssh_keys',
64 ADMIN_PREFIX + '/permissions/ssh_keys',
65 'admin_permissions_ssh_keys_data':
65 'admin_permissions_ssh_keys_data':
66 ADMIN_PREFIX + '/permissions/ssh_keys/data',
66 ADMIN_PREFIX + '/permissions/ssh_keys/data',
67 'admin_permissions_ssh_keys_update':
67 'admin_permissions_ssh_keys_update':
68 ADMIN_PREFIX + '/permissions/ssh_keys/update'
68 ADMIN_PREFIX + '/permissions/ssh_keys/update'
69
69
70 }[name].format(**kwargs)
70 }[name].format(**kwargs)
71
71
72 if params:
72 if params:
73 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
73 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
74 return base_url
74 return base_url
75
75
76
76
77 class TestAdminPermissionsController(TestController):
77 class TestAdminPermissionsController(TestController):
78
78
79 @pytest.fixture(scope='class', autouse=True)
79 @pytest.fixture(scope='class', autouse=True)
80 def prepare(self, request):
80 def prepare(self, request):
81 # cleanup and reset to default permissions after
81 # cleanup and reset to default permissions after
82 @request.addfinalizer
82 @request.addfinalizer
83 def cleanup():
83 def cleanup():
84 PermissionModel().create_default_user_permissions(
84 PermissionModel().create_default_user_permissions(
85 User.get_default_user(), force=True)
85 User.get_default_user(), force=True)
86
86
87 def test_index_application(self):
87 def test_index_application(self):
88 self.log_user()
88 self.log_user()
89 self.app.get(route_path('admin_permissions_application'))
89 self.app.get(route_path('admin_permissions_application'))
90
90
91 @pytest.mark.parametrize(
91 @pytest.mark.parametrize(
92 'anonymous, default_register, default_register_message, default_password_reset,'
92 'anonymous, default_register, default_register_message, default_password_reset,'
93 'default_extern_activate, expect_error, expect_form_error', [
93 'default_extern_activate, expect_error, expect_form_error', [
94 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
94 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
95 False, False),
95 False, False),
96 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
96 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
97 False, False),
97 False, False),
98 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
98 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
99 False, False),
99 False, False),
100 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
100 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
101 False, False),
101 False, False),
102 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
102 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
103 False, True),
103 False, True),
104 (True, '', '', 'hg.password_reset.enabled', '', True, False),
104 (True, '', '', 'hg.password_reset.enabled', '', True, False),
105 ])
105 ])
106 def test_update_application_permissions(
106 def test_update_application_permissions(
107 self, anonymous, default_register, default_register_message, default_password_reset,
107 self, anonymous, default_register, default_register_message, default_password_reset,
108 default_extern_activate, expect_error, expect_form_error):
108 default_extern_activate, expect_error, expect_form_error):
109
109
110 self.log_user()
110 self.log_user()
111
111
112 # TODO: anonymous access set here to False, breaks some other tests
112 # TODO: anonymous access set here to False, breaks some other tests
113 params = {
113 params = {
114 'csrf_token': self.csrf_token,
114 'csrf_token': self.csrf_token,
115 'anonymous': anonymous,
115 'anonymous': anonymous,
116 'default_register': default_register,
116 'default_register': default_register,
117 'default_register_message': default_register_message,
117 'default_register_message': default_register_message,
118 'default_password_reset': default_password_reset,
118 'default_password_reset': default_password_reset,
119 'default_extern_activate': default_extern_activate,
119 'default_extern_activate': default_extern_activate,
120 }
120 }
121 response = self.app.post(route_path('admin_permissions_application_update'),
121 response = self.app.post(route_path('admin_permissions_application_update'),
122 params=params)
122 params=params)
123 if expect_form_error:
123 if expect_form_error:
124 assert response.status_int == 200
124 assert response.status_int == 200
125 response.mustcontain('Value must be one of')
125 response.mustcontain('Value must be one of')
126 else:
126 else:
127 if expect_error:
127 if expect_error:
128 msg = 'Error occurred during update of permissions'
128 msg = 'Error occurred during update of permissions'
129 else:
129 else:
130 msg = 'Application permissions updated successfully'
130 msg = 'Application permissions updated successfully'
131 assert_session_flash(response, msg)
131 assert_session_flash(response, msg)
132
132
133 def test_index_object(self):
133 def test_index_object(self):
134 self.log_user()
134 self.log_user()
135 self.app.get(route_path('admin_permissions_object'))
135 self.app.get(route_path('admin_permissions_object'))
136
136
137 @pytest.mark.parametrize(
137 @pytest.mark.parametrize(
138 'repo, repo_group, user_group, expect_error, expect_form_error', [
138 'repo, repo_group, user_group, expect_error, expect_form_error', [
139 ('repository.none', 'group.none', 'usergroup.none', False, False),
139 ('repository.none', 'group.none', 'usergroup.none', False, False),
140 ('repository.read', 'group.read', 'usergroup.read', False, False),
140 ('repository.read', 'group.read', 'usergroup.read', False, False),
141 ('repository.write', 'group.write', 'usergroup.write',
141 ('repository.write', 'group.write', 'usergroup.write',
142 False, False),
142 False, False),
143 ('repository.admin', 'group.admin', 'usergroup.admin',
143 ('repository.admin', 'group.admin', 'usergroup.admin',
144 False, False),
144 False, False),
145 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
145 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
146 ('', '', '', True, False),
146 ('', '', '', True, False),
147 ])
147 ])
148 def test_update_object_permissions(self, repo, repo_group, user_group,
148 def test_update_object_permissions(self, repo, repo_group, user_group,
149 expect_error, expect_form_error):
149 expect_error, expect_form_error):
150 self.log_user()
150 self.log_user()
151
151
152 params = {
152 params = {
153 'csrf_token': self.csrf_token,
153 'csrf_token': self.csrf_token,
154 'default_repo_perm': repo,
154 'default_repo_perm': repo,
155 'overwrite_default_repo': False,
155 'overwrite_default_repo': False,
156 'default_group_perm': repo_group,
156 'default_group_perm': repo_group,
157 'overwrite_default_group': False,
157 'overwrite_default_group': False,
158 'default_user_group_perm': user_group,
158 'default_user_group_perm': user_group,
159 'overwrite_default_user_group': False,
159 'overwrite_default_user_group': False,
160 }
160 }
161 response = self.app.post(route_path('admin_permissions_object_update'),
161 response = self.app.post(route_path('admin_permissions_object_update'),
162 params=params)
162 params=params)
163 if expect_form_error:
163 if expect_form_error:
164 assert response.status_int == 200
164 assert response.status_int == 200
165 response.mustcontain('Value must be one of')
165 response.mustcontain('Value must be one of')
166 else:
166 else:
167 if expect_error:
167 if expect_error:
168 msg = 'Error occurred during update of permissions'
168 msg = 'Error occurred during update of permissions'
169 else:
169 else:
170 msg = 'Object permissions updated successfully'
170 msg = 'Object permissions updated successfully'
171 assert_session_flash(response, msg)
171 assert_session_flash(response, msg)
172
172
173 def test_index_global(self):
173 def test_index_global(self):
174 self.log_user()
174 self.log_user()
175 self.app.get(route_path('admin_permissions_global'))
175 self.app.get(route_path('admin_permissions_global'))
176
176
177 @pytest.mark.parametrize(
177 @pytest.mark.parametrize(
178 'repo_create, repo_create_write, user_group_create, repo_group_create,'
178 'repo_create, repo_create_write, user_group_create, repo_group_create,'
179 'fork_create, inherit_default_permissions, expect_error,'
179 'fork_create, inherit_default_permissions, expect_error,'
180 'expect_form_error', [
180 'expect_form_error', [
181 ('hg.create.none', 'hg.create.write_on_repogroup.false',
181 ('hg.create.none', 'hg.create.write_on_repogroup.false',
182 'hg.usergroup.create.false', 'hg.repogroup.create.false',
182 'hg.usergroup.create.false', 'hg.repogroup.create.false',
183 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
183 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
184 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
184 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
185 'hg.usergroup.create.true', 'hg.repogroup.create.true',
185 'hg.usergroup.create.true', 'hg.repogroup.create.true',
186 'hg.fork.repository', 'hg.inherit_default_perms.false',
186 'hg.fork.repository', 'hg.inherit_default_perms.false',
187 False, False),
187 False, False),
188 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
188 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
189 'hg.usergroup.create.true', 'hg.repogroup.create.true',
189 'hg.usergroup.create.true', 'hg.repogroup.create.true',
190 'hg.fork.repository', 'hg.inherit_default_perms.false',
190 'hg.fork.repository', 'hg.inherit_default_perms.false',
191 False, True),
191 False, True),
192 ('', '', '', '', '', '', True, False),
192 ('', '', '', '', '', '', True, False),
193 ])
193 ])
194 def test_update_global_permissions(
194 def test_update_global_permissions(
195 self, repo_create, repo_create_write, user_group_create,
195 self, repo_create, repo_create_write, user_group_create,
196 repo_group_create, fork_create, inherit_default_permissions,
196 repo_group_create, fork_create, inherit_default_permissions,
197 expect_error, expect_form_error):
197 expect_error, expect_form_error):
198 self.log_user()
198 self.log_user()
199
199
200 params = {
200 params = {
201 'csrf_token': self.csrf_token,
201 'csrf_token': self.csrf_token,
202 'default_repo_create': repo_create,
202 'default_repo_create': repo_create,
203 'default_repo_create_on_write': repo_create_write,
203 'default_repo_create_on_write': repo_create_write,
204 'default_user_group_create': user_group_create,
204 'default_user_group_create': user_group_create,
205 'default_repo_group_create': repo_group_create,
205 'default_repo_group_create': repo_group_create,
206 'default_fork_create': fork_create,
206 'default_fork_create': fork_create,
207 'default_inherit_default_permissions': inherit_default_permissions
207 'default_inherit_default_permissions': inherit_default_permissions
208 }
208 }
209 response = self.app.post(route_path('admin_permissions_global_update'),
209 response = self.app.post(route_path('admin_permissions_global_update'),
210 params=params)
210 params=params)
211 if expect_form_error:
211 if expect_form_error:
212 assert response.status_int == 200
212 assert response.status_int == 200
213 response.mustcontain('Value must be one of')
213 response.mustcontain('Value must be one of')
214 else:
214 else:
215 if expect_error:
215 if expect_error:
216 msg = 'Error occurred during update of permissions'
216 msg = 'Error occurred during update of permissions'
217 else:
217 else:
218 msg = 'Global permissions updated successfully'
218 msg = 'Global permissions updated successfully'
219 assert_session_flash(response, msg)
219 assert_session_flash(response, msg)
220
220
221 def test_index_ips(self):
221 def test_index_ips(self):
222 self.log_user()
222 self.log_user()
223 response = self.app.get(route_path('admin_permissions_ips'))
223 response = self.app.get(route_path('admin_permissions_ips'))
224 response.mustcontain('All IP addresses are allowed')
224 response.mustcontain('All IP addresses are allowed')
225
225
226 def test_add_delete_ips(self):
226 def test_add_delete_ips(self):
227 clear_cache_regions(['sql_cache_short'])
227 clear_cache_regions(['sql_cache_short'])
228 self.log_user()
228 self.log_user()
229
229
230 # ADD
230 # ADD
231 default_user_id = User.get_default_user_id()
231 default_user_id = User.get_default_user_id()
232 self.app.post(
232 self.app.post(
233 route_path('edit_user_ips_add', user_id=default_user_id),
233 route_path('edit_user_ips_add', user_id=default_user_id),
234 params={'new_ip': '0.0.0.0/24', 'csrf_token': self.csrf_token})
234 params={'new_ip': '0.0.0.0/24', 'csrf_token': self.csrf_token})
235
235
236 response = self.app.get(route_path('admin_permissions_ips'))
236 response = self.app.get(route_path('admin_permissions_ips'))
237 response.mustcontain('0.0.0.0/24')
237 response.mustcontain('0.0.0.0/24')
238 response.mustcontain('0.0.0.0 - 0.0.0.255')
238 response.mustcontain('0.0.0.0 - 0.0.0.255')
239
239
240 # DELETE
240 # DELETE
241 default_user_id = User.get_default_user_id()
241 default_user_id = User.get_default_user_id()
242 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
242 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
243 default_user_id).first().ip_id
243 default_user_id).first().ip_id
244
244
245 response = self.app.post(
245 response = self.app.post(
246 route_path('edit_user_ips_delete', user_id=default_user_id),
246 route_path('edit_user_ips_delete', user_id=default_user_id),
247 params={'del_ip_id': del_ip_id, 'csrf_token': self.csrf_token})
247 params={'del_ip_id': del_ip_id, 'csrf_token': self.csrf_token})
248
248
249 assert_session_flash(response, 'Removed ip address from user whitelist')
249 assert_session_flash(response, 'Removed ip address from user whitelist')
250
250
251 clear_cache_regions(['sql_cache_short'])
251 clear_cache_regions(['sql_cache_short'])
252 response = self.app.get(route_path('admin_permissions_ips'))
252 response = self.app.get(route_path('admin_permissions_ips'))
253 response.mustcontain('All IP addresses are allowed')
253 response.mustcontain('All IP addresses are allowed')
254 response.mustcontain(no=['0.0.0.0/24'])
254 response.mustcontain(no=['0.0.0.0/24'])
255 response.mustcontain(no=['0.0.0.0 - 0.0.0.255'])
255 response.mustcontain(no=['0.0.0.0 - 0.0.0.255'])
256
256
257 def test_index_overview(self):
257 def test_index_overview(self):
258 self.log_user()
258 self.log_user()
259 self.app.get(route_path('admin_permissions_overview'))
259 self.app.get(route_path('admin_permissions_overview'))
260
260
261 def test_ssh_keys(self):
261 def test_ssh_keys(self):
262 self.log_user()
262 self.log_user()
263 self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
263 self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
264
264
265 def test_ssh_keys_data(self, user_util, xhr_header):
265 def test_ssh_keys_data(self, user_util, xhr_header):
266 self.log_user()
266 self.log_user()
267 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
267 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
268 extra_environ=xhr_header)
268 extra_environ=xhr_header)
269 assert response.json == {u'data': [], u'draw': None,
269 assert response.json == {u'data': [], u'draw': None,
270 u'recordsFiltered': 0, u'recordsTotal': 0}
270 u'recordsFiltered': 0, u'recordsTotal': 0}
271
271
272 dummy_user = user_util.create_user()
272 dummy_user = user_util.create_user()
273 SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
273 SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
274 Session().commit()
274 Session().commit()
275 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
275 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
276 extra_environ=xhr_header)
276 extra_environ=xhr_header)
277 assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
277 assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
278
278
279 def test_ssh_keys_update(self):
279 def test_ssh_keys_update(self):
280 self.log_user()
280 self.log_user()
281 response = self.app.post(
281 response = self.app.post(
282 route_path('admin_permissions_ssh_keys_update'),
282 route_path('admin_permissions_ssh_keys_update'),
283 dict(csrf_token=self.csrf_token), status=302)
283 dict(csrf_token=self.csrf_token), status=302)
284
284
285 assert_session_flash(
285 assert_session_flash(
286 response, 'Updated SSH keys file')
286 response, 'Updated SSH keys file')
287
287
288 def test_ssh_keys_update_disabled(self):
288 def test_ssh_keys_update_disabled(self):
289 self.log_user()
289 self.log_user()
290
290
291 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
291 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
292 with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
292 with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
293 return_value=False):
293 return_value=False):
294 response = self.app.post(
294 response = self.app.post(
295 route_path('admin_permissions_ssh_keys_update'),
295 route_path('admin_permissions_ssh_keys_update'),
296 dict(csrf_token=self.csrf_token), status=302)
296 dict(csrf_token=self.csrf_token), status=302)
297
297
298 assert_session_flash(
298 assert_session_flash(
299 response, 'SSH key support is disabled in .ini file') No newline at end of file
299 response, 'SSH key support is disabled in .ini file')
@@ -1,512 +1,512 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 urllib
21 import urllib.request, urllib.parse, urllib.error
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.apps._base import ADMIN_PREFIX
26 from rhodecode.apps._base import ADMIN_PREFIX
27 from rhodecode.lib import auth
27 from rhodecode.lib import auth
28 from rhodecode.lib.utils2 import safe_str
28 from rhodecode.lib.utils2 import safe_str
29 from rhodecode.lib import helpers as h
29 from rhodecode.lib import helpers as h
30 from rhodecode.model.db import (
30 from rhodecode.model.db import (
31 Repository, RepoGroup, UserRepoToPerm, User, Permission)
31 Repository, RepoGroup, UserRepoToPerm, User, Permission)
32 from rhodecode.model.meta import Session
32 from rhodecode.model.meta import Session
33 from rhodecode.model.repo import RepoModel
33 from rhodecode.model.repo import RepoModel
34 from rhodecode.model.repo_group import RepoGroupModel
34 from rhodecode.model.repo_group import RepoGroupModel
35 from rhodecode.model.user import UserModel
35 from rhodecode.model.user import UserModel
36 from rhodecode.tests import (
36 from rhodecode.tests import (
37 login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
37 login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
38 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
38 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
39 from rhodecode.tests.fixture import Fixture, error_function
39 from rhodecode.tests.fixture import Fixture, error_function
40 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
40 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
41
41
42 fixture = Fixture()
42 fixture = Fixture()
43
43
44
44
45 def route_path(name, params=None, **kwargs):
45 def route_path(name, params=None, **kwargs):
46 import urllib
46 import urllib.request, urllib.parse, urllib.error
47
47
48 base_url = {
48 base_url = {
49 'repos': ADMIN_PREFIX + '/repos',
49 'repos': ADMIN_PREFIX + '/repos',
50 'repos_data': ADMIN_PREFIX + '/repos_data',
50 'repos_data': ADMIN_PREFIX + '/repos_data',
51 'repo_new': ADMIN_PREFIX + '/repos/new',
51 'repo_new': ADMIN_PREFIX + '/repos/new',
52 'repo_create': ADMIN_PREFIX + '/repos/create',
52 'repo_create': ADMIN_PREFIX + '/repos/create',
53
53
54 'repo_creating_check': '/{repo_name}/repo_creating_check',
54 'repo_creating_check': '/{repo_name}/repo_creating_check',
55 }[name].format(**kwargs)
55 }[name].format(**kwargs)
56
56
57 if params:
57 if params:
58 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
58 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
59 return base_url
59 return base_url
60
60
61
61
62 def _get_permission_for_user(user, repo):
62 def _get_permission_for_user(user, repo):
63 perm = UserRepoToPerm.query()\
63 perm = UserRepoToPerm.query()\
64 .filter(UserRepoToPerm.repository ==
64 .filter(UserRepoToPerm.repository ==
65 Repository.get_by_repo_name(repo))\
65 Repository.get_by_repo_name(repo))\
66 .filter(UserRepoToPerm.user == User.get_by_username(user))\
66 .filter(UserRepoToPerm.user == User.get_by_username(user))\
67 .all()
67 .all()
68 return perm
68 return perm
69
69
70
70
71 @pytest.mark.usefixtures("app")
71 @pytest.mark.usefixtures("app")
72 class TestAdminRepos(object):
72 class TestAdminRepos(object):
73
73
74 def test_repo_list(self, autologin_user, user_util, xhr_header):
74 def test_repo_list(self, autologin_user, user_util, xhr_header):
75 repo = user_util.create_repo()
75 repo = user_util.create_repo()
76 repo_name = repo.repo_name
76 repo_name = repo.repo_name
77 response = self.app.get(
77 response = self.app.get(
78 route_path('repos_data'), status=200,
78 route_path('repos_data'), status=200,
79 extra_environ=xhr_header)
79 extra_environ=xhr_header)
80
80
81 response.mustcontain(repo_name)
81 response.mustcontain(repo_name)
82
82
83 def test_create_page_restricted_to_single_backend(self, autologin_user, backend):
83 def test_create_page_restricted_to_single_backend(self, autologin_user, backend):
84 with mock.patch('rhodecode.BACKENDS', {'git': 'git'}):
84 with mock.patch('rhodecode.BACKENDS', {'git': 'git'}):
85 response = self.app.get(route_path('repo_new'), status=200)
85 response = self.app.get(route_path('repo_new'), status=200)
86 assert_response = response.assert_response()
86 assert_response = response.assert_response()
87 element = assert_response.get_element('[name=repo_type]')
87 element = assert_response.get_element('[name=repo_type]')
88 assert element.get('value') == 'git'
88 assert element.get('value') == 'git'
89
89
90 def test_create_page_non_restricted_backends(self, autologin_user, backend):
90 def test_create_page_non_restricted_backends(self, autologin_user, backend):
91 response = self.app.get(route_path('repo_new'), status=200)
91 response = self.app.get(route_path('repo_new'), status=200)
92 assert_response = response.assert_response()
92 assert_response = response.assert_response()
93 assert ['hg', 'git', 'svn'] == [x.get('value') for x in assert_response.get_elements('[name=repo_type]')]
93 assert ['hg', 'git', 'svn'] == [x.get('value') for x in assert_response.get_elements('[name=repo_type]')]
94
94
95 @pytest.mark.parametrize(
95 @pytest.mark.parametrize(
96 "suffix", [u'', u'xxa'], ids=['', 'non-ascii'])
96 "suffix", [u'', u'xxa'], ids=['', 'non-ascii'])
97 def test_create(self, autologin_user, backend, suffix, csrf_token):
97 def test_create(self, autologin_user, backend, suffix, csrf_token):
98 repo_name_unicode = backend.new_repo_name(suffix=suffix)
98 repo_name_unicode = backend.new_repo_name(suffix=suffix)
99 repo_name = repo_name_unicode.encode('utf8')
99 repo_name = repo_name_unicode.encode('utf8')
100 description_unicode = u'description for newly created repo' + suffix
100 description_unicode = u'description for newly created repo' + suffix
101 description = description_unicode.encode('utf8')
101 description = description_unicode.encode('utf8')
102 response = self.app.post(
102 response = self.app.post(
103 route_path('repo_create'),
103 route_path('repo_create'),
104 fixture._get_repo_create_params(
104 fixture._get_repo_create_params(
105 repo_private=False,
105 repo_private=False,
106 repo_name=repo_name,
106 repo_name=repo_name,
107 repo_type=backend.alias,
107 repo_type=backend.alias,
108 repo_description=description,
108 repo_description=description,
109 csrf_token=csrf_token),
109 csrf_token=csrf_token),
110 status=302)
110 status=302)
111
111
112 self.assert_repository_is_created_correctly(
112 self.assert_repository_is_created_correctly(
113 repo_name, description, backend)
113 repo_name, description, backend)
114
114
115 def test_create_numeric_name(self, autologin_user, backend, csrf_token):
115 def test_create_numeric_name(self, autologin_user, backend, csrf_token):
116 numeric_repo = '1234'
116 numeric_repo = '1234'
117 repo_name = numeric_repo
117 repo_name = numeric_repo
118 description = 'description for newly created repo' + numeric_repo
118 description = 'description for newly created repo' + numeric_repo
119 self.app.post(
119 self.app.post(
120 route_path('repo_create'),
120 route_path('repo_create'),
121 fixture._get_repo_create_params(
121 fixture._get_repo_create_params(
122 repo_private=False,
122 repo_private=False,
123 repo_name=repo_name,
123 repo_name=repo_name,
124 repo_type=backend.alias,
124 repo_type=backend.alias,
125 repo_description=description,
125 repo_description=description,
126 csrf_token=csrf_token))
126 csrf_token=csrf_token))
127
127
128 self.assert_repository_is_created_correctly(
128 self.assert_repository_is_created_correctly(
129 repo_name, description, backend)
129 repo_name, description, backend)
130
130
131 @pytest.mark.parametrize("suffix", [u'', u'ąćę'], ids=['', 'non-ascii'])
131 @pytest.mark.parametrize("suffix", [u'', u'ąćę'], ids=['', 'non-ascii'])
132 def test_create_in_group(
132 def test_create_in_group(
133 self, autologin_user, backend, suffix, csrf_token):
133 self, autologin_user, backend, suffix, csrf_token):
134 # create GROUP
134 # create GROUP
135 group_name = 'sometest_%s' % backend.alias
135 group_name = 'sometest_%s' % backend.alias
136 gr = RepoGroupModel().create(group_name=group_name,
136 gr = RepoGroupModel().create(group_name=group_name,
137 group_description='test',
137 group_description='test',
138 owner=TEST_USER_ADMIN_LOGIN)
138 owner=TEST_USER_ADMIN_LOGIN)
139 Session().commit()
139 Session().commit()
140
140
141 repo_name = u'ingroup' + suffix
141 repo_name = u'ingroup' + suffix
142 repo_name_full = RepoGroup.url_sep().join(
142 repo_name_full = RepoGroup.url_sep().join(
143 [group_name, repo_name])
143 [group_name, repo_name])
144 description = u'description for newly created repo'
144 description = u'description for newly created repo'
145 self.app.post(
145 self.app.post(
146 route_path('repo_create'),
146 route_path('repo_create'),
147 fixture._get_repo_create_params(
147 fixture._get_repo_create_params(
148 repo_private=False,
148 repo_private=False,
149 repo_name=safe_str(repo_name),
149 repo_name=safe_str(repo_name),
150 repo_type=backend.alias,
150 repo_type=backend.alias,
151 repo_description=description,
151 repo_description=description,
152 repo_group=gr.group_id,
152 repo_group=gr.group_id,
153 csrf_token=csrf_token))
153 csrf_token=csrf_token))
154
154
155 # TODO: johbo: Cleanup work to fixture
155 # TODO: johbo: Cleanup work to fixture
156 try:
156 try:
157 self.assert_repository_is_created_correctly(
157 self.assert_repository_is_created_correctly(
158 repo_name_full, description, backend)
158 repo_name_full, description, backend)
159
159
160 new_repo = RepoModel().get_by_repo_name(repo_name_full)
160 new_repo = RepoModel().get_by_repo_name(repo_name_full)
161 inherited_perms = UserRepoToPerm.query().filter(
161 inherited_perms = UserRepoToPerm.query().filter(
162 UserRepoToPerm.repository_id == new_repo.repo_id).all()
162 UserRepoToPerm.repository_id == new_repo.repo_id).all()
163 assert len(inherited_perms) == 1
163 assert len(inherited_perms) == 1
164 finally:
164 finally:
165 RepoModel().delete(repo_name_full)
165 RepoModel().delete(repo_name_full)
166 RepoGroupModel().delete(group_name)
166 RepoGroupModel().delete(group_name)
167 Session().commit()
167 Session().commit()
168
168
169 def test_create_in_group_numeric_name(
169 def test_create_in_group_numeric_name(
170 self, autologin_user, backend, csrf_token):
170 self, autologin_user, backend, csrf_token):
171 # create GROUP
171 # create GROUP
172 group_name = 'sometest_%s' % backend.alias
172 group_name = 'sometest_%s' % backend.alias
173 gr = RepoGroupModel().create(group_name=group_name,
173 gr = RepoGroupModel().create(group_name=group_name,
174 group_description='test',
174 group_description='test',
175 owner=TEST_USER_ADMIN_LOGIN)
175 owner=TEST_USER_ADMIN_LOGIN)
176 Session().commit()
176 Session().commit()
177
177
178 repo_name = '12345'
178 repo_name = '12345'
179 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
179 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
180 description = 'description for newly created repo'
180 description = 'description for newly created repo'
181 self.app.post(
181 self.app.post(
182 route_path('repo_create'),
182 route_path('repo_create'),
183 fixture._get_repo_create_params(
183 fixture._get_repo_create_params(
184 repo_private=False,
184 repo_private=False,
185 repo_name=repo_name,
185 repo_name=repo_name,
186 repo_type=backend.alias,
186 repo_type=backend.alias,
187 repo_description=description,
187 repo_description=description,
188 repo_group=gr.group_id,
188 repo_group=gr.group_id,
189 csrf_token=csrf_token))
189 csrf_token=csrf_token))
190
190
191 # TODO: johbo: Cleanup work to fixture
191 # TODO: johbo: Cleanup work to fixture
192 try:
192 try:
193 self.assert_repository_is_created_correctly(
193 self.assert_repository_is_created_correctly(
194 repo_name_full, description, backend)
194 repo_name_full, description, backend)
195
195
196 new_repo = RepoModel().get_by_repo_name(repo_name_full)
196 new_repo = RepoModel().get_by_repo_name(repo_name_full)
197 inherited_perms = UserRepoToPerm.query()\
197 inherited_perms = UserRepoToPerm.query()\
198 .filter(UserRepoToPerm.repository_id == new_repo.repo_id).all()
198 .filter(UserRepoToPerm.repository_id == new_repo.repo_id).all()
199 assert len(inherited_perms) == 1
199 assert len(inherited_perms) == 1
200 finally:
200 finally:
201 RepoModel().delete(repo_name_full)
201 RepoModel().delete(repo_name_full)
202 RepoGroupModel().delete(group_name)
202 RepoGroupModel().delete(group_name)
203 Session().commit()
203 Session().commit()
204
204
205 def test_create_in_group_without_needed_permissions(self, backend):
205 def test_create_in_group_without_needed_permissions(self, backend):
206 session = login_user_session(
206 session = login_user_session(
207 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
207 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
208 csrf_token = auth.get_csrf_token(session)
208 csrf_token = auth.get_csrf_token(session)
209 # revoke
209 # revoke
210 user_model = UserModel()
210 user_model = UserModel()
211 # disable fork and create on default user
211 # disable fork and create on default user
212 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
212 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
213 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
213 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
214 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
214 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
215 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
215 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
216
216
217 # disable on regular user
217 # disable on regular user
218 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
218 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
219 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
219 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
220 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
220 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
221 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
221 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
222 Session().commit()
222 Session().commit()
223
223
224 # create GROUP
224 # create GROUP
225 group_name = 'reg_sometest_%s' % backend.alias
225 group_name = 'reg_sometest_%s' % backend.alias
226 gr = RepoGroupModel().create(group_name=group_name,
226 gr = RepoGroupModel().create(group_name=group_name,
227 group_description='test',
227 group_description='test',
228 owner=TEST_USER_ADMIN_LOGIN)
228 owner=TEST_USER_ADMIN_LOGIN)
229 Session().commit()
229 Session().commit()
230 repo_group_id = gr.group_id
230 repo_group_id = gr.group_id
231
231
232 group_name_allowed = 'reg_sometest_allowed_%s' % backend.alias
232 group_name_allowed = 'reg_sometest_allowed_%s' % backend.alias
233 gr_allowed = RepoGroupModel().create(
233 gr_allowed = RepoGroupModel().create(
234 group_name=group_name_allowed,
234 group_name=group_name_allowed,
235 group_description='test',
235 group_description='test',
236 owner=TEST_USER_REGULAR_LOGIN)
236 owner=TEST_USER_REGULAR_LOGIN)
237 allowed_repo_group_id = gr_allowed.group_id
237 allowed_repo_group_id = gr_allowed.group_id
238 Session().commit()
238 Session().commit()
239
239
240 repo_name = 'ingroup'
240 repo_name = 'ingroup'
241 description = 'description for newly created repo'
241 description = 'description for newly created repo'
242 response = self.app.post(
242 response = self.app.post(
243 route_path('repo_create'),
243 route_path('repo_create'),
244 fixture._get_repo_create_params(
244 fixture._get_repo_create_params(
245 repo_private=False,
245 repo_private=False,
246 repo_name=repo_name,
246 repo_name=repo_name,
247 repo_type=backend.alias,
247 repo_type=backend.alias,
248 repo_description=description,
248 repo_description=description,
249 repo_group=repo_group_id,
249 repo_group=repo_group_id,
250 csrf_token=csrf_token))
250 csrf_token=csrf_token))
251
251
252 response.mustcontain('Invalid value')
252 response.mustcontain('Invalid value')
253
253
254 # user is allowed to create in this group
254 # user is allowed to create in this group
255 repo_name = 'ingroup'
255 repo_name = 'ingroup'
256 repo_name_full = RepoGroup.url_sep().join(
256 repo_name_full = RepoGroup.url_sep().join(
257 [group_name_allowed, repo_name])
257 [group_name_allowed, repo_name])
258 description = 'description for newly created repo'
258 description = 'description for newly created repo'
259 response = self.app.post(
259 response = self.app.post(
260 route_path('repo_create'),
260 route_path('repo_create'),
261 fixture._get_repo_create_params(
261 fixture._get_repo_create_params(
262 repo_private=False,
262 repo_private=False,
263 repo_name=repo_name,
263 repo_name=repo_name,
264 repo_type=backend.alias,
264 repo_type=backend.alias,
265 repo_description=description,
265 repo_description=description,
266 repo_group=allowed_repo_group_id,
266 repo_group=allowed_repo_group_id,
267 csrf_token=csrf_token))
267 csrf_token=csrf_token))
268
268
269 # TODO: johbo: Cleanup in pytest fixture
269 # TODO: johbo: Cleanup in pytest fixture
270 try:
270 try:
271 self.assert_repository_is_created_correctly(
271 self.assert_repository_is_created_correctly(
272 repo_name_full, description, backend)
272 repo_name_full, description, backend)
273
273
274 new_repo = RepoModel().get_by_repo_name(repo_name_full)
274 new_repo = RepoModel().get_by_repo_name(repo_name_full)
275 inherited_perms = UserRepoToPerm.query().filter(
275 inherited_perms = UserRepoToPerm.query().filter(
276 UserRepoToPerm.repository_id == new_repo.repo_id).all()
276 UserRepoToPerm.repository_id == new_repo.repo_id).all()
277 assert len(inherited_perms) == 1
277 assert len(inherited_perms) == 1
278
278
279 assert repo_on_filesystem(repo_name_full)
279 assert repo_on_filesystem(repo_name_full)
280 finally:
280 finally:
281 RepoModel().delete(repo_name_full)
281 RepoModel().delete(repo_name_full)
282 RepoGroupModel().delete(group_name)
282 RepoGroupModel().delete(group_name)
283 RepoGroupModel().delete(group_name_allowed)
283 RepoGroupModel().delete(group_name_allowed)
284 Session().commit()
284 Session().commit()
285
285
286 def test_create_in_group_inherit_permissions(self, autologin_user, backend,
286 def test_create_in_group_inherit_permissions(self, autologin_user, backend,
287 csrf_token):
287 csrf_token):
288 # create GROUP
288 # create GROUP
289 group_name = 'sometest_%s' % backend.alias
289 group_name = 'sometest_%s' % backend.alias
290 gr = RepoGroupModel().create(group_name=group_name,
290 gr = RepoGroupModel().create(group_name=group_name,
291 group_description='test',
291 group_description='test',
292 owner=TEST_USER_ADMIN_LOGIN)
292 owner=TEST_USER_ADMIN_LOGIN)
293 perm = Permission.get_by_key('repository.write')
293 perm = Permission.get_by_key('repository.write')
294 RepoGroupModel().grant_user_permission(
294 RepoGroupModel().grant_user_permission(
295 gr, TEST_USER_REGULAR_LOGIN, perm)
295 gr, TEST_USER_REGULAR_LOGIN, perm)
296
296
297 # add repo permissions
297 # add repo permissions
298 Session().commit()
298 Session().commit()
299 repo_group_id = gr.group_id
299 repo_group_id = gr.group_id
300 repo_name = 'ingroup_inherited_%s' % backend.alias
300 repo_name = 'ingroup_inherited_%s' % backend.alias
301 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
301 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
302 description = 'description for newly created repo'
302 description = 'description for newly created repo'
303 self.app.post(
303 self.app.post(
304 route_path('repo_create'),
304 route_path('repo_create'),
305 fixture._get_repo_create_params(
305 fixture._get_repo_create_params(
306 repo_private=False,
306 repo_private=False,
307 repo_name=repo_name,
307 repo_name=repo_name,
308 repo_type=backend.alias,
308 repo_type=backend.alias,
309 repo_description=description,
309 repo_description=description,
310 repo_group=repo_group_id,
310 repo_group=repo_group_id,
311 repo_copy_permissions=True,
311 repo_copy_permissions=True,
312 csrf_token=csrf_token))
312 csrf_token=csrf_token))
313
313
314 # TODO: johbo: Cleanup to pytest fixture
314 # TODO: johbo: Cleanup to pytest fixture
315 try:
315 try:
316 self.assert_repository_is_created_correctly(
316 self.assert_repository_is_created_correctly(
317 repo_name_full, description, backend)
317 repo_name_full, description, backend)
318 except Exception:
318 except Exception:
319 RepoGroupModel().delete(group_name)
319 RepoGroupModel().delete(group_name)
320 Session().commit()
320 Session().commit()
321 raise
321 raise
322
322
323 # check if inherited permissions are applied
323 # check if inherited permissions are applied
324 new_repo = RepoModel().get_by_repo_name(repo_name_full)
324 new_repo = RepoModel().get_by_repo_name(repo_name_full)
325 inherited_perms = UserRepoToPerm.query().filter(
325 inherited_perms = UserRepoToPerm.query().filter(
326 UserRepoToPerm.repository_id == new_repo.repo_id).all()
326 UserRepoToPerm.repository_id == new_repo.repo_id).all()
327 assert len(inherited_perms) == 2
327 assert len(inherited_perms) == 2
328
328
329 assert TEST_USER_REGULAR_LOGIN in [
329 assert TEST_USER_REGULAR_LOGIN in [
330 x.user.username for x in inherited_perms]
330 x.user.username for x in inherited_perms]
331 assert 'repository.write' in [
331 assert 'repository.write' in [
332 x.permission.permission_name for x in inherited_perms]
332 x.permission.permission_name for x in inherited_perms]
333
333
334 RepoModel().delete(repo_name_full)
334 RepoModel().delete(repo_name_full)
335 RepoGroupModel().delete(group_name)
335 RepoGroupModel().delete(group_name)
336 Session().commit()
336 Session().commit()
337
337
338 @pytest.mark.xfail_backends(
338 @pytest.mark.xfail_backends(
339 "git", "hg", reason="Missing reposerver support")
339 "git", "hg", reason="Missing reposerver support")
340 def test_create_with_clone_uri(self, autologin_user, backend, reposerver,
340 def test_create_with_clone_uri(self, autologin_user, backend, reposerver,
341 csrf_token):
341 csrf_token):
342 source_repo = backend.create_repo(number_of_commits=2)
342 source_repo = backend.create_repo(number_of_commits=2)
343 source_repo_name = source_repo.repo_name
343 source_repo_name = source_repo.repo_name
344 reposerver.serve(source_repo.scm_instance())
344 reposerver.serve(source_repo.scm_instance())
345
345
346 repo_name = backend.new_repo_name()
346 repo_name = backend.new_repo_name()
347 response = self.app.post(
347 response = self.app.post(
348 route_path('repo_create'),
348 route_path('repo_create'),
349 fixture._get_repo_create_params(
349 fixture._get_repo_create_params(
350 repo_private=False,
350 repo_private=False,
351 repo_name=repo_name,
351 repo_name=repo_name,
352 repo_type=backend.alias,
352 repo_type=backend.alias,
353 repo_description='',
353 repo_description='',
354 clone_uri=reposerver.url,
354 clone_uri=reposerver.url,
355 csrf_token=csrf_token),
355 csrf_token=csrf_token),
356 status=302)
356 status=302)
357
357
358 # Should be redirected to the creating page
358 # Should be redirected to the creating page
359 response.mustcontain('repo_creating')
359 response.mustcontain('repo_creating')
360
360
361 # Expecting that both repositories have same history
361 # Expecting that both repositories have same history
362 source_repo = RepoModel().get_by_repo_name(source_repo_name)
362 source_repo = RepoModel().get_by_repo_name(source_repo_name)
363 source_vcs = source_repo.scm_instance()
363 source_vcs = source_repo.scm_instance()
364 repo = RepoModel().get_by_repo_name(repo_name)
364 repo = RepoModel().get_by_repo_name(repo_name)
365 repo_vcs = repo.scm_instance()
365 repo_vcs = repo.scm_instance()
366 assert source_vcs[0].message == repo_vcs[0].message
366 assert source_vcs[0].message == repo_vcs[0].message
367 assert source_vcs.count() == repo_vcs.count()
367 assert source_vcs.count() == repo_vcs.count()
368 assert source_vcs.commit_ids == repo_vcs.commit_ids
368 assert source_vcs.commit_ids == repo_vcs.commit_ids
369
369
370 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
370 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
371 def test_create_remote_repo_wrong_clone_uri(self, autologin_user, backend,
371 def test_create_remote_repo_wrong_clone_uri(self, autologin_user, backend,
372 csrf_token):
372 csrf_token):
373 repo_name = backend.new_repo_name()
373 repo_name = backend.new_repo_name()
374 description = 'description for newly created repo'
374 description = 'description for newly created repo'
375 response = self.app.post(
375 response = self.app.post(
376 route_path('repo_create'),
376 route_path('repo_create'),
377 fixture._get_repo_create_params(
377 fixture._get_repo_create_params(
378 repo_private=False,
378 repo_private=False,
379 repo_name=repo_name,
379 repo_name=repo_name,
380 repo_type=backend.alias,
380 repo_type=backend.alias,
381 repo_description=description,
381 repo_description=description,
382 clone_uri='http://repo.invalid/repo',
382 clone_uri='http://repo.invalid/repo',
383 csrf_token=csrf_token))
383 csrf_token=csrf_token))
384 response.mustcontain('invalid clone url')
384 response.mustcontain('invalid clone url')
385
385
386 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
386 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
387 def test_create_remote_repo_wrong_clone_uri_hg_svn(
387 def test_create_remote_repo_wrong_clone_uri_hg_svn(
388 self, autologin_user, backend, csrf_token):
388 self, autologin_user, backend, csrf_token):
389 repo_name = backend.new_repo_name()
389 repo_name = backend.new_repo_name()
390 description = 'description for newly created repo'
390 description = 'description for newly created repo'
391 response = self.app.post(
391 response = self.app.post(
392 route_path('repo_create'),
392 route_path('repo_create'),
393 fixture._get_repo_create_params(
393 fixture._get_repo_create_params(
394 repo_private=False,
394 repo_private=False,
395 repo_name=repo_name,
395 repo_name=repo_name,
396 repo_type=backend.alias,
396 repo_type=backend.alias,
397 repo_description=description,
397 repo_description=description,
398 clone_uri='svn+http://svn.invalid/repo',
398 clone_uri='svn+http://svn.invalid/repo',
399 csrf_token=csrf_token))
399 csrf_token=csrf_token))
400 response.mustcontain('invalid clone url')
400 response.mustcontain('invalid clone url')
401
401
402 def test_create_with_git_suffix(
402 def test_create_with_git_suffix(
403 self, autologin_user, backend, csrf_token):
403 self, autologin_user, backend, csrf_token):
404 repo_name = backend.new_repo_name() + ".git"
404 repo_name = backend.new_repo_name() + ".git"
405 description = 'description for newly created repo'
405 description = 'description for newly created repo'
406 response = self.app.post(
406 response = self.app.post(
407 route_path('repo_create'),
407 route_path('repo_create'),
408 fixture._get_repo_create_params(
408 fixture._get_repo_create_params(
409 repo_private=False,
409 repo_private=False,
410 repo_name=repo_name,
410 repo_name=repo_name,
411 repo_type=backend.alias,
411 repo_type=backend.alias,
412 repo_description=description,
412 repo_description=description,
413 csrf_token=csrf_token))
413 csrf_token=csrf_token))
414 response.mustcontain('Repository name cannot end with .git')
414 response.mustcontain('Repository name cannot end with .git')
415
415
416 def test_default_user_cannot_access_private_repo_in_a_group(
416 def test_default_user_cannot_access_private_repo_in_a_group(
417 self, autologin_user, user_util, backend):
417 self, autologin_user, user_util, backend):
418
418
419 group = user_util.create_repo_group()
419 group = user_util.create_repo_group()
420
420
421 repo = backend.create_repo(
421 repo = backend.create_repo(
422 repo_private=True, repo_group=group, repo_copy_permissions=True)
422 repo_private=True, repo_group=group, repo_copy_permissions=True)
423
423
424 permissions = _get_permission_for_user(
424 permissions = _get_permission_for_user(
425 user='default', repo=repo.repo_name)
425 user='default', repo=repo.repo_name)
426 assert len(permissions) == 1
426 assert len(permissions) == 1
427 assert permissions[0].permission.permission_name == 'repository.none'
427 assert permissions[0].permission.permission_name == 'repository.none'
428 assert permissions[0].repository.private is True
428 assert permissions[0].repository.private is True
429
429
430 def test_create_on_top_level_without_permissions(self, backend):
430 def test_create_on_top_level_without_permissions(self, backend):
431 session = login_user_session(
431 session = login_user_session(
432 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
432 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
433 csrf_token = auth.get_csrf_token(session)
433 csrf_token = auth.get_csrf_token(session)
434
434
435 # revoke
435 # revoke
436 user_model = UserModel()
436 user_model = UserModel()
437 # disable fork and create on default user
437 # disable fork and create on default user
438 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
438 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
439 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
439 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
440 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
440 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
441 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
441 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
442
442
443 # disable on regular user
443 # disable on regular user
444 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
444 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
445 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
445 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
446 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
446 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
447 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
447 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
448 Session().commit()
448 Session().commit()
449
449
450 repo_name = backend.new_repo_name()
450 repo_name = backend.new_repo_name()
451 description = 'description for newly created repo'
451 description = 'description for newly created repo'
452 response = self.app.post(
452 response = self.app.post(
453 route_path('repo_create'),
453 route_path('repo_create'),
454 fixture._get_repo_create_params(
454 fixture._get_repo_create_params(
455 repo_private=False,
455 repo_private=False,
456 repo_name=repo_name,
456 repo_name=repo_name,
457 repo_type=backend.alias,
457 repo_type=backend.alias,
458 repo_description=description,
458 repo_description=description,
459 csrf_token=csrf_token))
459 csrf_token=csrf_token))
460
460
461 response.mustcontain(
461 response.mustcontain(
462 u"You do not have the permission to store repositories in "
462 u"You do not have the permission to store repositories in "
463 u"the root location.")
463 u"the root location.")
464
464
465 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
465 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
466 def test_create_repo_when_filesystem_op_fails(
466 def test_create_repo_when_filesystem_op_fails(
467 self, autologin_user, backend, csrf_token):
467 self, autologin_user, backend, csrf_token):
468 repo_name = backend.new_repo_name()
468 repo_name = backend.new_repo_name()
469 description = 'description for newly created repo'
469 description = 'description for newly created repo'
470
470
471 response = self.app.post(
471 response = self.app.post(
472 route_path('repo_create'),
472 route_path('repo_create'),
473 fixture._get_repo_create_params(
473 fixture._get_repo_create_params(
474 repo_private=False,
474 repo_private=False,
475 repo_name=repo_name,
475 repo_name=repo_name,
476 repo_type=backend.alias,
476 repo_type=backend.alias,
477 repo_description=description,
477 repo_description=description,
478 csrf_token=csrf_token))
478 csrf_token=csrf_token))
479
479
480 assert_session_flash(
480 assert_session_flash(
481 response, 'Error creating repository %s' % repo_name)
481 response, 'Error creating repository %s' % repo_name)
482 # repo must not be in db
482 # repo must not be in db
483 assert backend.repo is None
483 assert backend.repo is None
484 # repo must not be in filesystem !
484 # repo must not be in filesystem !
485 assert not repo_on_filesystem(repo_name)
485 assert not repo_on_filesystem(repo_name)
486
486
487 def assert_repository_is_created_correctly(
487 def assert_repository_is_created_correctly(
488 self, repo_name, description, backend):
488 self, repo_name, description, backend):
489 repo_name_utf8 = safe_str(repo_name)
489 repo_name_utf8 = safe_str(repo_name)
490
490
491 # run the check page that triggers the flash message
491 # run the check page that triggers the flash message
492 response = self.app.get(
492 response = self.app.get(
493 route_path('repo_creating_check', repo_name=safe_str(repo_name)))
493 route_path('repo_creating_check', repo_name=safe_str(repo_name)))
494 assert response.json == {u'result': True}
494 assert response.json == {u'result': True}
495
495
496 flash_msg = u'Created repository <a href="/{}">{}</a>'.format(
496 flash_msg = u'Created repository <a href="/{}">{}</a>'.format(
497 urllib.quote(repo_name_utf8), repo_name)
497 urllib.parse.quote(repo_name_utf8), repo_name)
498 assert_session_flash(response, flash_msg)
498 assert_session_flash(response, flash_msg)
499
499
500 # test if the repo was created in the database
500 # test if the repo was created in the database
501 new_repo = RepoModel().get_by_repo_name(repo_name)
501 new_repo = RepoModel().get_by_repo_name(repo_name)
502
502
503 assert new_repo.repo_name == repo_name
503 assert new_repo.repo_name == repo_name
504 assert new_repo.description == description
504 assert new_repo.description == description
505
505
506 # test if the repository is visible in the list ?
506 # test if the repository is visible in the list ?
507 response = self.app.get(
507 response = self.app.get(
508 h.route_path('repo_summary', repo_name=safe_str(repo_name)))
508 h.route_path('repo_summary', repo_name=safe_str(repo_name)))
509 response.mustcontain(repo_name)
509 response.mustcontain(repo_name)
510 response.mustcontain(backend.alias)
510 response.mustcontain(backend.alias)
511
511
512 assert repo_on_filesystem(repo_name)
512 assert repo_on_filesystem(repo_name)
@@ -1,194 +1,194 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 pytest
22 import pytest
23
23
24 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.lib import helpers as h
25 from rhodecode.lib import helpers as h
26 from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
26 from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.model.repo_group import RepoGroupModel
28 from rhodecode.model.repo_group import RepoGroupModel
29 from rhodecode.tests import (
29 from rhodecode.tests import (
30 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
30 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
31 from rhodecode.tests.fixture import Fixture
31 from rhodecode.tests.fixture import Fixture
32
32
33 fixture = Fixture()
33 fixture = Fixture()
34
34
35
35
36 def route_path(name, params=None, **kwargs):
36 def route_path(name, params=None, **kwargs):
37 import urllib
37 import urllib.request, urllib.parse, urllib.error
38
38
39 base_url = {
39 base_url = {
40 'repo_groups': ADMIN_PREFIX + '/repo_groups',
40 'repo_groups': ADMIN_PREFIX + '/repo_groups',
41 'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data',
41 'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data',
42 'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
42 'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
43 'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
43 'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
44
44
45 }[name].format(**kwargs)
45 }[name].format(**kwargs)
46
46
47 if params:
47 if params:
48 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 return base_url
49 return base_url
50
50
51
51
52 def _get_permission_for_user(user, repo):
52 def _get_permission_for_user(user, repo):
53 perm = UserRepoToPerm.query()\
53 perm = UserRepoToPerm.query()\
54 .filter(UserRepoToPerm.repository ==
54 .filter(UserRepoToPerm.repository ==
55 Repository.get_by_repo_name(repo))\
55 Repository.get_by_repo_name(repo))\
56 .filter(UserRepoToPerm.user == User.get_by_username(user))\
56 .filter(UserRepoToPerm.user == User.get_by_username(user))\
57 .all()
57 .all()
58 return perm
58 return perm
59
59
60
60
61 @pytest.mark.usefixtures("app")
61 @pytest.mark.usefixtures("app")
62 class TestAdminRepositoryGroups(object):
62 class TestAdminRepositoryGroups(object):
63
63
64 def test_show_repo_groups(self, autologin_user):
64 def test_show_repo_groups(self, autologin_user):
65 self.app.get(route_path('repo_groups'))
65 self.app.get(route_path('repo_groups'))
66
66
67 def test_show_repo_groups_data(self, autologin_user, xhr_header):
67 def test_show_repo_groups_data(self, autologin_user, xhr_header):
68 response = self.app.get(route_path(
68 response = self.app.get(route_path(
69 'repo_groups_data'), extra_environ=xhr_header)
69 'repo_groups_data'), extra_environ=xhr_header)
70
70
71 all_repo_groups = RepoGroup.query().count()
71 all_repo_groups = RepoGroup.query().count()
72 assert response.json['recordsTotal'] == all_repo_groups
72 assert response.json['recordsTotal'] == all_repo_groups
73
73
74 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
74 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
75 response = self.app.get(route_path(
75 response = self.app.get(route_path(
76 'repo_groups_data', params={'search[value]': 'empty_search'}),
76 'repo_groups_data', params={'search[value]': 'empty_search'}),
77 extra_environ=xhr_header)
77 extra_environ=xhr_header)
78
78
79 all_repo_groups = RepoGroup.query().count()
79 all_repo_groups = RepoGroup.query().count()
80 assert response.json['recordsTotal'] == all_repo_groups
80 assert response.json['recordsTotal'] == all_repo_groups
81 assert response.json['recordsFiltered'] == 0
81 assert response.json['recordsFiltered'] == 0
82
82
83 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
83 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
84 fixture.create_repo_group('test_repo_group')
84 fixture.create_repo_group('test_repo_group')
85 response = self.app.get(route_path(
85 response = self.app.get(route_path(
86 'repo_groups_data'), extra_environ=xhr_header)
86 'repo_groups_data'), extra_environ=xhr_header)
87 response.mustcontain('<a href=\\"/{}/_edit\\" title=\\"Edit\\">Edit</a>'.format('test_repo_group'))
87 response.mustcontain('<a href=\\"/{}/_edit\\" title=\\"Edit\\">Edit</a>'.format('test_repo_group'))
88 fixture.destroy_repo_group('test_repo_group')
88 fixture.destroy_repo_group('test_repo_group')
89
89
90 def test_new(self, autologin_user):
90 def test_new(self, autologin_user):
91 self.app.get(route_path('repo_group_new'))
91 self.app.get(route_path('repo_group_new'))
92
92
93 def test_new_with_parent_group(self, autologin_user, user_util):
93 def test_new_with_parent_group(self, autologin_user, user_util):
94 gr = user_util.create_repo_group()
94 gr = user_util.create_repo_group()
95
95
96 self.app.get(route_path('repo_group_new'),
96 self.app.get(route_path('repo_group_new'),
97 params=dict(parent_group=gr.group_name))
97 params=dict(parent_group=gr.group_name))
98
98
99 def test_new_by_regular_user_no_permission(self, autologin_regular_user):
99 def test_new_by_regular_user_no_permission(self, autologin_regular_user):
100 self.app.get(route_path('repo_group_new'), status=403)
100 self.app.get(route_path('repo_group_new'), status=403)
101
101
102 @pytest.mark.parametrize('repo_group_name', [
102 @pytest.mark.parametrize('repo_group_name', [
103 'git_repo',
103 'git_repo',
104 'git_repo_ąć',
104 'git_repo_ąć',
105 'hg_repo',
105 'hg_repo',
106 '12345',
106 '12345',
107 'hg_repo_ąć',
107 'hg_repo_ąć',
108 ])
108 ])
109 def test_create(self, autologin_user, repo_group_name, csrf_token):
109 def test_create(self, autologin_user, repo_group_name, csrf_token):
110 repo_group_name_unicode = repo_group_name.decode('utf8')
110 repo_group_name_unicode = repo_group_name.decode('utf8')
111 description = 'description for newly created repo group'
111 description = 'description for newly created repo group'
112
112
113 response = self.app.post(
113 response = self.app.post(
114 route_path('repo_group_create'),
114 route_path('repo_group_create'),
115 fixture._get_group_create_params(
115 fixture._get_group_create_params(
116 group_name=repo_group_name,
116 group_name=repo_group_name,
117 group_description=description,
117 group_description=description,
118 csrf_token=csrf_token))
118 csrf_token=csrf_token))
119
119
120 # run the check page that triggers the flash message
120 # run the check page that triggers the flash message
121 repo_gr_url = h.route_path(
121 repo_gr_url = h.route_path(
122 'repo_group_home', repo_group_name=repo_group_name)
122 'repo_group_home', repo_group_name=repo_group_name)
123
123
124 assert_session_flash(
124 assert_session_flash(
125 response,
125 response,
126 'Created repository group <a href="%s">%s</a>' % (
126 'Created repository group <a href="%s">%s</a>' % (
127 repo_gr_url, repo_group_name_unicode))
127 repo_gr_url, repo_group_name_unicode))
128
128
129 # # test if the repo group was created in the database
129 # # test if the repo group was created in the database
130 new_repo_group = RepoGroupModel()._get_repo_group(
130 new_repo_group = RepoGroupModel()._get_repo_group(
131 repo_group_name_unicode)
131 repo_group_name_unicode)
132 assert new_repo_group is not None
132 assert new_repo_group is not None
133
133
134 assert new_repo_group.group_name == repo_group_name_unicode
134 assert new_repo_group.group_name == repo_group_name_unicode
135 assert new_repo_group.group_description == description
135 assert new_repo_group.group_description == description
136
136
137 # test if the repository is visible in the list ?
137 # test if the repository is visible in the list ?
138 response = self.app.get(repo_gr_url)
138 response = self.app.get(repo_gr_url)
139 response.mustcontain(repo_group_name)
139 response.mustcontain(repo_group_name)
140
140
141 # test if the repository group was created on filesystem
141 # test if the repository group was created on filesystem
142 is_on_filesystem = os.path.isdir(
142 is_on_filesystem = os.path.isdir(
143 os.path.join(TESTS_TMP_PATH, repo_group_name))
143 os.path.join(TESTS_TMP_PATH, repo_group_name))
144 if not is_on_filesystem:
144 if not is_on_filesystem:
145 self.fail('no repo group %s in filesystem' % repo_group_name)
145 self.fail('no repo group %s in filesystem' % repo_group_name)
146
146
147 RepoGroupModel().delete(repo_group_name_unicode)
147 RepoGroupModel().delete(repo_group_name_unicode)
148 Session().commit()
148 Session().commit()
149
149
150 @pytest.mark.parametrize('repo_group_name', [
150 @pytest.mark.parametrize('repo_group_name', [
151 'git_repo',
151 'git_repo',
152 'git_repo_ąć',
152 'git_repo_ąć',
153 'hg_repo',
153 'hg_repo',
154 '12345',
154 '12345',
155 'hg_repo_ąć',
155 'hg_repo_ąć',
156 ])
156 ])
157 def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token):
157 def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token):
158 parent_group = user_util.create_repo_group()
158 parent_group = user_util.create_repo_group()
159 parent_group_name = parent_group.group_name
159 parent_group_name = parent_group.group_name
160
160
161 expected_group_name = '{}/{}'.format(
161 expected_group_name = '{}/{}'.format(
162 parent_group_name, repo_group_name)
162 parent_group_name, repo_group_name)
163 expected_group_name_unicode = expected_group_name.decode('utf8')
163 expected_group_name_unicode = expected_group_name.decode('utf8')
164
164
165 try:
165 try:
166 response = self.app.post(
166 response = self.app.post(
167 route_path('repo_group_create'),
167 route_path('repo_group_create'),
168 fixture._get_group_create_params(
168 fixture._get_group_create_params(
169 group_name=repo_group_name,
169 group_name=repo_group_name,
170 group_parent_id=parent_group.group_id,
170 group_parent_id=parent_group.group_id,
171 group_description='Test desciption',
171 group_description='Test desciption',
172 csrf_token=csrf_token))
172 csrf_token=csrf_token))
173
173
174 assert_session_flash(
174 assert_session_flash(
175 response,
175 response,
176 u'Created repository group <a href="%s">%s</a>' % (
176 u'Created repository group <a href="%s">%s</a>' % (
177 h.route_path('repo_group_home',
177 h.route_path('repo_group_home',
178 repo_group_name=expected_group_name),
178 repo_group_name=expected_group_name),
179 expected_group_name_unicode))
179 expected_group_name_unicode))
180 finally:
180 finally:
181 RepoGroupModel().delete(expected_group_name_unicode)
181 RepoGroupModel().delete(expected_group_name_unicode)
182 Session().commit()
182 Session().commit()
183
183
184 def test_user_with_creation_permissions_cannot_create_subgroups(
184 def test_user_with_creation_permissions_cannot_create_subgroups(
185 self, autologin_regular_user, user_util):
185 self, autologin_regular_user, user_util):
186
186
187 user_util.grant_user_permission(
187 user_util.grant_user_permission(
188 TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true')
188 TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true')
189 parent_group = user_util.create_repo_group()
189 parent_group = user_util.create_repo_group()
190 parent_group_id = parent_group.group_id
190 parent_group_id = parent_group.group_id
191 self.app.get(
191 self.app.get(
192 route_path('repo_group_new',
192 route_path('repo_group_new',
193 params=dict(parent_group=parent_group_id), ),
193 params=dict(parent_group=parent_group_id), ),
194 status=403)
194 status=403)
@@ -1,767 +1,767 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.apps._base import ADMIN_PREFIX
26 from rhodecode.lib.utils2 import md5
26 from rhodecode.lib.utils2 import md5
27 from rhodecode.model.db import RhodeCodeUi
27 from rhodecode.model.db import RhodeCodeUi
28 from rhodecode.model.meta import Session
28 from rhodecode.model.meta import Session
29 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
29 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
30 from rhodecode.tests import assert_session_flash
30 from rhodecode.tests import assert_session_flash
31 from rhodecode.tests.utils import AssertResponse
31 from rhodecode.tests.utils import AssertResponse
32
32
33
33
34 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
34 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
35
35
36
36
37 def route_path(name, params=None, **kwargs):
37 def route_path(name, params=None, **kwargs):
38 import urllib
38 import urllib.request, urllib.parse, urllib.error
39 from rhodecode.apps._base import ADMIN_PREFIX
39 from rhodecode.apps._base import ADMIN_PREFIX
40
40
41 base_url = {
41 base_url = {
42
42
43 'admin_settings':
43 'admin_settings':
44 ADMIN_PREFIX +'/settings',
44 ADMIN_PREFIX +'/settings',
45 'admin_settings_update':
45 'admin_settings_update':
46 ADMIN_PREFIX + '/settings/update',
46 ADMIN_PREFIX + '/settings/update',
47 'admin_settings_global':
47 'admin_settings_global':
48 ADMIN_PREFIX + '/settings/global',
48 ADMIN_PREFIX + '/settings/global',
49 'admin_settings_global_update':
49 'admin_settings_global_update':
50 ADMIN_PREFIX + '/settings/global/update',
50 ADMIN_PREFIX + '/settings/global/update',
51 'admin_settings_vcs':
51 'admin_settings_vcs':
52 ADMIN_PREFIX + '/settings/vcs',
52 ADMIN_PREFIX + '/settings/vcs',
53 'admin_settings_vcs_update':
53 'admin_settings_vcs_update':
54 ADMIN_PREFIX + '/settings/vcs/update',
54 ADMIN_PREFIX + '/settings/vcs/update',
55 'admin_settings_vcs_svn_pattern_delete':
55 'admin_settings_vcs_svn_pattern_delete':
56 ADMIN_PREFIX + '/settings/vcs/svn_pattern_delete',
56 ADMIN_PREFIX + '/settings/vcs/svn_pattern_delete',
57 'admin_settings_mapping':
57 'admin_settings_mapping':
58 ADMIN_PREFIX + '/settings/mapping',
58 ADMIN_PREFIX + '/settings/mapping',
59 'admin_settings_mapping_update':
59 'admin_settings_mapping_update':
60 ADMIN_PREFIX + '/settings/mapping/update',
60 ADMIN_PREFIX + '/settings/mapping/update',
61 'admin_settings_visual':
61 'admin_settings_visual':
62 ADMIN_PREFIX + '/settings/visual',
62 ADMIN_PREFIX + '/settings/visual',
63 'admin_settings_visual_update':
63 'admin_settings_visual_update':
64 ADMIN_PREFIX + '/settings/visual/update',
64 ADMIN_PREFIX + '/settings/visual/update',
65 'admin_settings_issuetracker':
65 'admin_settings_issuetracker':
66 ADMIN_PREFIX + '/settings/issue-tracker',
66 ADMIN_PREFIX + '/settings/issue-tracker',
67 'admin_settings_issuetracker_update':
67 'admin_settings_issuetracker_update':
68 ADMIN_PREFIX + '/settings/issue-tracker/update',
68 ADMIN_PREFIX + '/settings/issue-tracker/update',
69 'admin_settings_issuetracker_test':
69 'admin_settings_issuetracker_test':
70 ADMIN_PREFIX + '/settings/issue-tracker/test',
70 ADMIN_PREFIX + '/settings/issue-tracker/test',
71 'admin_settings_issuetracker_delete':
71 'admin_settings_issuetracker_delete':
72 ADMIN_PREFIX + '/settings/issue-tracker/delete',
72 ADMIN_PREFIX + '/settings/issue-tracker/delete',
73 'admin_settings_email':
73 'admin_settings_email':
74 ADMIN_PREFIX + '/settings/email',
74 ADMIN_PREFIX + '/settings/email',
75 'admin_settings_email_update':
75 'admin_settings_email_update':
76 ADMIN_PREFIX + '/settings/email/update',
76 ADMIN_PREFIX + '/settings/email/update',
77 'admin_settings_hooks':
77 'admin_settings_hooks':
78 ADMIN_PREFIX + '/settings/hooks',
78 ADMIN_PREFIX + '/settings/hooks',
79 'admin_settings_hooks_update':
79 'admin_settings_hooks_update':
80 ADMIN_PREFIX + '/settings/hooks/update',
80 ADMIN_PREFIX + '/settings/hooks/update',
81 'admin_settings_hooks_delete':
81 'admin_settings_hooks_delete':
82 ADMIN_PREFIX + '/settings/hooks/delete',
82 ADMIN_PREFIX + '/settings/hooks/delete',
83 'admin_settings_search':
83 'admin_settings_search':
84 ADMIN_PREFIX + '/settings/search',
84 ADMIN_PREFIX + '/settings/search',
85 'admin_settings_labs':
85 'admin_settings_labs':
86 ADMIN_PREFIX + '/settings/labs',
86 ADMIN_PREFIX + '/settings/labs',
87 'admin_settings_labs_update':
87 'admin_settings_labs_update':
88 ADMIN_PREFIX + '/settings/labs/update',
88 ADMIN_PREFIX + '/settings/labs/update',
89
89
90 'admin_settings_sessions':
90 'admin_settings_sessions':
91 ADMIN_PREFIX + '/settings/sessions',
91 ADMIN_PREFIX + '/settings/sessions',
92 'admin_settings_sessions_cleanup':
92 'admin_settings_sessions_cleanup':
93 ADMIN_PREFIX + '/settings/sessions/cleanup',
93 ADMIN_PREFIX + '/settings/sessions/cleanup',
94 'admin_settings_system':
94 'admin_settings_system':
95 ADMIN_PREFIX + '/settings/system',
95 ADMIN_PREFIX + '/settings/system',
96 'admin_settings_system_update':
96 'admin_settings_system_update':
97 ADMIN_PREFIX + '/settings/system/updates',
97 ADMIN_PREFIX + '/settings/system/updates',
98 'admin_settings_open_source':
98 'admin_settings_open_source':
99 ADMIN_PREFIX + '/settings/open_source',
99 ADMIN_PREFIX + '/settings/open_source',
100
100
101
101
102 }[name].format(**kwargs)
102 }[name].format(**kwargs)
103
103
104 if params:
104 if params:
105 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
105 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
106 return base_url
106 return base_url
107
107
108
108
109 @pytest.mark.usefixtures('autologin_user', 'app')
109 @pytest.mark.usefixtures('autologin_user', 'app')
110 class TestAdminSettingsController(object):
110 class TestAdminSettingsController(object):
111
111
112 @pytest.mark.parametrize('urlname', [
112 @pytest.mark.parametrize('urlname', [
113 'admin_settings_vcs',
113 'admin_settings_vcs',
114 'admin_settings_mapping',
114 'admin_settings_mapping',
115 'admin_settings_global',
115 'admin_settings_global',
116 'admin_settings_visual',
116 'admin_settings_visual',
117 'admin_settings_email',
117 'admin_settings_email',
118 'admin_settings_hooks',
118 'admin_settings_hooks',
119 'admin_settings_search',
119 'admin_settings_search',
120 ])
120 ])
121 def test_simple_get(self, urlname):
121 def test_simple_get(self, urlname):
122 self.app.get(route_path(urlname))
122 self.app.get(route_path(urlname))
123
123
124 def test_create_custom_hook(self, csrf_token):
124 def test_create_custom_hook(self, csrf_token):
125 response = self.app.post(
125 response = self.app.post(
126 route_path('admin_settings_hooks_update'),
126 route_path('admin_settings_hooks_update'),
127 params={
127 params={
128 'new_hook_ui_key': 'test_hooks_1',
128 'new_hook_ui_key': 'test_hooks_1',
129 'new_hook_ui_value': 'cd /tmp',
129 'new_hook_ui_value': 'cd /tmp',
130 'csrf_token': csrf_token})
130 'csrf_token': csrf_token})
131
131
132 response = response.follow()
132 response = response.follow()
133 response.mustcontain('test_hooks_1')
133 response.mustcontain('test_hooks_1')
134 response.mustcontain('cd /tmp')
134 response.mustcontain('cd /tmp')
135
135
136 def test_create_custom_hook_delete(self, csrf_token):
136 def test_create_custom_hook_delete(self, csrf_token):
137 response = self.app.post(
137 response = self.app.post(
138 route_path('admin_settings_hooks_update'),
138 route_path('admin_settings_hooks_update'),
139 params={
139 params={
140 'new_hook_ui_key': 'test_hooks_2',
140 'new_hook_ui_key': 'test_hooks_2',
141 'new_hook_ui_value': 'cd /tmp2',
141 'new_hook_ui_value': 'cd /tmp2',
142 'csrf_token': csrf_token})
142 'csrf_token': csrf_token})
143
143
144 response = response.follow()
144 response = response.follow()
145 response.mustcontain('test_hooks_2')
145 response.mustcontain('test_hooks_2')
146 response.mustcontain('cd /tmp2')
146 response.mustcontain('cd /tmp2')
147
147
148 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
148 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
149
149
150 # delete
150 # delete
151 self.app.post(
151 self.app.post(
152 route_path('admin_settings_hooks_delete'),
152 route_path('admin_settings_hooks_delete'),
153 params={'hook_id': hook_id, 'csrf_token': csrf_token})
153 params={'hook_id': hook_id, 'csrf_token': csrf_token})
154 response = self.app.get(route_path('admin_settings_hooks'))
154 response = self.app.get(route_path('admin_settings_hooks'))
155 response.mustcontain(no=['test_hooks_2'])
155 response.mustcontain(no=['test_hooks_2'])
156 response.mustcontain(no=['cd /tmp2'])
156 response.mustcontain(no=['cd /tmp2'])
157
157
158
158
159 @pytest.mark.usefixtures('autologin_user', 'app')
159 @pytest.mark.usefixtures('autologin_user', 'app')
160 class TestAdminSettingsGlobal(object):
160 class TestAdminSettingsGlobal(object):
161
161
162 def test_pre_post_code_code_active(self, csrf_token):
162 def test_pre_post_code_code_active(self, csrf_token):
163 pre_code = 'rc-pre-code-187652122'
163 pre_code = 'rc-pre-code-187652122'
164 post_code = 'rc-postcode-98165231'
164 post_code = 'rc-postcode-98165231'
165
165
166 response = self.post_and_verify_settings({
166 response = self.post_and_verify_settings({
167 'rhodecode_pre_code': pre_code,
167 'rhodecode_pre_code': pre_code,
168 'rhodecode_post_code': post_code,
168 'rhodecode_post_code': post_code,
169 'csrf_token': csrf_token,
169 'csrf_token': csrf_token,
170 })
170 })
171
171
172 response = response.follow()
172 response = response.follow()
173 response.mustcontain(pre_code, post_code)
173 response.mustcontain(pre_code, post_code)
174
174
175 def test_pre_post_code_code_inactive(self, csrf_token):
175 def test_pre_post_code_code_inactive(self, csrf_token):
176 pre_code = 'rc-pre-code-187652122'
176 pre_code = 'rc-pre-code-187652122'
177 post_code = 'rc-postcode-98165231'
177 post_code = 'rc-postcode-98165231'
178 response = self.post_and_verify_settings({
178 response = self.post_and_verify_settings({
179 'rhodecode_pre_code': '',
179 'rhodecode_pre_code': '',
180 'rhodecode_post_code': '',
180 'rhodecode_post_code': '',
181 'csrf_token': csrf_token,
181 'csrf_token': csrf_token,
182 })
182 })
183
183
184 response = response.follow()
184 response = response.follow()
185 response.mustcontain(no=[pre_code, post_code])
185 response.mustcontain(no=[pre_code, post_code])
186
186
187 def test_captcha_activate(self, csrf_token):
187 def test_captcha_activate(self, csrf_token):
188 self.post_and_verify_settings({
188 self.post_and_verify_settings({
189 'rhodecode_captcha_private_key': '1234567890',
189 'rhodecode_captcha_private_key': '1234567890',
190 'rhodecode_captcha_public_key': '1234567890',
190 'rhodecode_captcha_public_key': '1234567890',
191 'csrf_token': csrf_token,
191 'csrf_token': csrf_token,
192 })
192 })
193
193
194 response = self.app.get(ADMIN_PREFIX + '/register')
194 response = self.app.get(ADMIN_PREFIX + '/register')
195 response.mustcontain('captcha')
195 response.mustcontain('captcha')
196
196
197 def test_captcha_deactivate(self, csrf_token):
197 def test_captcha_deactivate(self, csrf_token):
198 self.post_and_verify_settings({
198 self.post_and_verify_settings({
199 'rhodecode_captcha_private_key': '',
199 'rhodecode_captcha_private_key': '',
200 'rhodecode_captcha_public_key': '1234567890',
200 'rhodecode_captcha_public_key': '1234567890',
201 'csrf_token': csrf_token,
201 'csrf_token': csrf_token,
202 })
202 })
203
203
204 response = self.app.get(ADMIN_PREFIX + '/register')
204 response = self.app.get(ADMIN_PREFIX + '/register')
205 response.mustcontain(no=['captcha'])
205 response.mustcontain(no=['captcha'])
206
206
207 def test_title_change(self, csrf_token):
207 def test_title_change(self, csrf_token):
208 old_title = 'RhodeCode'
208 old_title = 'RhodeCode'
209
209
210 for new_title in ['Changed', 'Żółwik', old_title]:
210 for new_title in ['Changed', 'Żółwik', old_title]:
211 response = self.post_and_verify_settings({
211 response = self.post_and_verify_settings({
212 'rhodecode_title': new_title,
212 'rhodecode_title': new_title,
213 'csrf_token': csrf_token,
213 'csrf_token': csrf_token,
214 })
214 })
215
215
216 response = response.follow()
216 response = response.follow()
217 response.mustcontain(new_title)
217 response.mustcontain(new_title)
218
218
219 def post_and_verify_settings(self, settings):
219 def post_and_verify_settings(self, settings):
220 old_title = 'RhodeCode'
220 old_title = 'RhodeCode'
221 old_realm = 'RhodeCode authentication'
221 old_realm = 'RhodeCode authentication'
222 params = {
222 params = {
223 'rhodecode_title': old_title,
223 'rhodecode_title': old_title,
224 'rhodecode_realm': old_realm,
224 'rhodecode_realm': old_realm,
225 'rhodecode_pre_code': '',
225 'rhodecode_pre_code': '',
226 'rhodecode_post_code': '',
226 'rhodecode_post_code': '',
227 'rhodecode_captcha_private_key': '',
227 'rhodecode_captcha_private_key': '',
228 'rhodecode_captcha_public_key': '',
228 'rhodecode_captcha_public_key': '',
229 'rhodecode_create_personal_repo_group': False,
229 'rhodecode_create_personal_repo_group': False,
230 'rhodecode_personal_repo_group_pattern': '${username}',
230 'rhodecode_personal_repo_group_pattern': '${username}',
231 }
231 }
232 params.update(settings)
232 params.update(settings)
233 response = self.app.post(
233 response = self.app.post(
234 route_path('admin_settings_global_update'), params=params)
234 route_path('admin_settings_global_update'), params=params)
235
235
236 assert_session_flash(response, 'Updated application settings')
236 assert_session_flash(response, 'Updated application settings')
237 app_settings = SettingsModel().get_all_settings()
237 app_settings = SettingsModel().get_all_settings()
238 del settings['csrf_token']
238 del settings['csrf_token']
239 for key, value in settings.iteritems():
239 for key, value in settings.iteritems():
240 assert app_settings[key] == value.decode('utf-8')
240 assert app_settings[key] == value.decode('utf-8')
241
241
242 return response
242 return response
243
243
244
244
245 @pytest.mark.usefixtures('autologin_user', 'app')
245 @pytest.mark.usefixtures('autologin_user', 'app')
246 class TestAdminSettingsVcs(object):
246 class TestAdminSettingsVcs(object):
247
247
248 def test_contains_svn_default_patterns(self):
248 def test_contains_svn_default_patterns(self):
249 response = self.app.get(route_path('admin_settings_vcs'))
249 response = self.app.get(route_path('admin_settings_vcs'))
250 expected_patterns = [
250 expected_patterns = [
251 '/trunk',
251 '/trunk',
252 '/branches/*',
252 '/branches/*',
253 '/tags/*',
253 '/tags/*',
254 ]
254 ]
255 for pattern in expected_patterns:
255 for pattern in expected_patterns:
256 response.mustcontain(pattern)
256 response.mustcontain(pattern)
257
257
258 def test_add_new_svn_branch_and_tag_pattern(
258 def test_add_new_svn_branch_and_tag_pattern(
259 self, backend_svn, form_defaults, disable_sql_cache,
259 self, backend_svn, form_defaults, disable_sql_cache,
260 csrf_token):
260 csrf_token):
261 form_defaults.update({
261 form_defaults.update({
262 'new_svn_branch': '/exp/branches/*',
262 'new_svn_branch': '/exp/branches/*',
263 'new_svn_tag': '/important_tags/*',
263 'new_svn_tag': '/important_tags/*',
264 'csrf_token': csrf_token,
264 'csrf_token': csrf_token,
265 })
265 })
266
266
267 response = self.app.post(
267 response = self.app.post(
268 route_path('admin_settings_vcs_update'),
268 route_path('admin_settings_vcs_update'),
269 params=form_defaults, status=302)
269 params=form_defaults, status=302)
270 response = response.follow()
270 response = response.follow()
271
271
272 # Expect to find the new values on the page
272 # Expect to find the new values on the page
273 response.mustcontain('/exp/branches/*')
273 response.mustcontain('/exp/branches/*')
274 response.mustcontain('/important_tags/*')
274 response.mustcontain('/important_tags/*')
275
275
276 # Expect that those patterns are used to match branches and tags now
276 # Expect that those patterns are used to match branches and tags now
277 repo = backend_svn['svn-simple-layout'].scm_instance()
277 repo = backend_svn['svn-simple-layout'].scm_instance()
278 assert 'exp/branches/exp-sphinx-docs' in repo.branches
278 assert 'exp/branches/exp-sphinx-docs' in repo.branches
279 assert 'important_tags/v0.5' in repo.tags
279 assert 'important_tags/v0.5' in repo.tags
280
280
281 def test_add_same_svn_value_twice_shows_an_error_message(
281 def test_add_same_svn_value_twice_shows_an_error_message(
282 self, form_defaults, csrf_token, settings_util):
282 self, form_defaults, csrf_token, settings_util):
283 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
283 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
284 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
284 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
285
285
286 response = self.app.post(
286 response = self.app.post(
287 route_path('admin_settings_vcs_update'),
287 route_path('admin_settings_vcs_update'),
288 params={
288 params={
289 'paths_root_path': form_defaults['paths_root_path'],
289 'paths_root_path': form_defaults['paths_root_path'],
290 'new_svn_branch': '/test',
290 'new_svn_branch': '/test',
291 'new_svn_tag': '/test',
291 'new_svn_tag': '/test',
292 'csrf_token': csrf_token,
292 'csrf_token': csrf_token,
293 },
293 },
294 status=200)
294 status=200)
295
295
296 response.mustcontain("Pattern already exists")
296 response.mustcontain("Pattern already exists")
297 response.mustcontain("Some form inputs contain invalid data.")
297 response.mustcontain("Some form inputs contain invalid data.")
298
298
299 @pytest.mark.parametrize('section', [
299 @pytest.mark.parametrize('section', [
300 'vcs_svn_branch',
300 'vcs_svn_branch',
301 'vcs_svn_tag',
301 'vcs_svn_tag',
302 ])
302 ])
303 def test_delete_svn_patterns(
303 def test_delete_svn_patterns(
304 self, section, csrf_token, settings_util):
304 self, section, csrf_token, settings_util):
305 setting = settings_util.create_rhodecode_ui(
305 setting = settings_util.create_rhodecode_ui(
306 section, '/test_delete', cleanup=False)
306 section, '/test_delete', cleanup=False)
307
307
308 self.app.post(
308 self.app.post(
309 route_path('admin_settings_vcs_svn_pattern_delete'),
309 route_path('admin_settings_vcs_svn_pattern_delete'),
310 params={
310 params={
311 'delete_svn_pattern': setting.ui_id,
311 'delete_svn_pattern': setting.ui_id,
312 'csrf_token': csrf_token},
312 'csrf_token': csrf_token},
313 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
313 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
314
314
315 @pytest.mark.parametrize('section', [
315 @pytest.mark.parametrize('section', [
316 'vcs_svn_branch',
316 'vcs_svn_branch',
317 'vcs_svn_tag',
317 'vcs_svn_tag',
318 ])
318 ])
319 def test_delete_svn_patterns_raises_404_when_no_xhr(
319 def test_delete_svn_patterns_raises_404_when_no_xhr(
320 self, section, csrf_token, settings_util):
320 self, section, csrf_token, settings_util):
321 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
321 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
322
322
323 self.app.post(
323 self.app.post(
324 route_path('admin_settings_vcs_svn_pattern_delete'),
324 route_path('admin_settings_vcs_svn_pattern_delete'),
325 params={
325 params={
326 'delete_svn_pattern': setting.ui_id,
326 'delete_svn_pattern': setting.ui_id,
327 'csrf_token': csrf_token},
327 'csrf_token': csrf_token},
328 status=404)
328 status=404)
329
329
330 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
330 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
331 form_defaults.update({
331 form_defaults.update({
332 'csrf_token': csrf_token,
332 'csrf_token': csrf_token,
333 'extensions_hgsubversion': 'True',
333 'extensions_hgsubversion': 'True',
334 })
334 })
335 response = self.app.post(
335 response = self.app.post(
336 route_path('admin_settings_vcs_update'),
336 route_path('admin_settings_vcs_update'),
337 params=form_defaults,
337 params=form_defaults,
338 status=302)
338 status=302)
339
339
340 response = response.follow()
340 response = response.follow()
341 extensions_input = (
341 extensions_input = (
342 '<input id="extensions_hgsubversion" '
342 '<input id="extensions_hgsubversion" '
343 'name="extensions_hgsubversion" type="checkbox" '
343 'name="extensions_hgsubversion" type="checkbox" '
344 'value="True" checked="checked" />')
344 'value="True" checked="checked" />')
345 response.mustcontain(extensions_input)
345 response.mustcontain(extensions_input)
346
346
347 def test_extensions_hgevolve(self, form_defaults, csrf_token):
347 def test_extensions_hgevolve(self, form_defaults, csrf_token):
348 form_defaults.update({
348 form_defaults.update({
349 'csrf_token': csrf_token,
349 'csrf_token': csrf_token,
350 'extensions_evolve': 'True',
350 'extensions_evolve': 'True',
351 })
351 })
352 response = self.app.post(
352 response = self.app.post(
353 route_path('admin_settings_vcs_update'),
353 route_path('admin_settings_vcs_update'),
354 params=form_defaults,
354 params=form_defaults,
355 status=302)
355 status=302)
356
356
357 response = response.follow()
357 response = response.follow()
358 extensions_input = (
358 extensions_input = (
359 '<input id="extensions_evolve" '
359 '<input id="extensions_evolve" '
360 'name="extensions_evolve" type="checkbox" '
360 'name="extensions_evolve" type="checkbox" '
361 'value="True" checked="checked" />')
361 'value="True" checked="checked" />')
362 response.mustcontain(extensions_input)
362 response.mustcontain(extensions_input)
363
363
364 def test_has_a_section_for_pull_request_settings(self):
364 def test_has_a_section_for_pull_request_settings(self):
365 response = self.app.get(route_path('admin_settings_vcs'))
365 response = self.app.get(route_path('admin_settings_vcs'))
366 response.mustcontain('Pull Request Settings')
366 response.mustcontain('Pull Request Settings')
367
367
368 def test_has_an_input_for_invalidation_of_inline_comments(self):
368 def test_has_an_input_for_invalidation_of_inline_comments(self):
369 response = self.app.get(route_path('admin_settings_vcs'))
369 response = self.app.get(route_path('admin_settings_vcs'))
370 assert_response = response.assert_response()
370 assert_response = response.assert_response()
371 assert_response.one_element_exists(
371 assert_response.one_element_exists(
372 '[name=rhodecode_use_outdated_comments]')
372 '[name=rhodecode_use_outdated_comments]')
373
373
374 @pytest.mark.parametrize('new_value', [True, False])
374 @pytest.mark.parametrize('new_value', [True, False])
375 def test_allows_to_change_invalidation_of_inline_comments(
375 def test_allows_to_change_invalidation_of_inline_comments(
376 self, form_defaults, csrf_token, new_value):
376 self, form_defaults, csrf_token, new_value):
377 setting_key = 'use_outdated_comments'
377 setting_key = 'use_outdated_comments'
378 setting = SettingsModel().create_or_update_setting(
378 setting = SettingsModel().create_or_update_setting(
379 setting_key, not new_value, 'bool')
379 setting_key, not new_value, 'bool')
380 Session().add(setting)
380 Session().add(setting)
381 Session().commit()
381 Session().commit()
382
382
383 form_defaults.update({
383 form_defaults.update({
384 'csrf_token': csrf_token,
384 'csrf_token': csrf_token,
385 'rhodecode_use_outdated_comments': str(new_value),
385 'rhodecode_use_outdated_comments': str(new_value),
386 })
386 })
387 response = self.app.post(
387 response = self.app.post(
388 route_path('admin_settings_vcs_update'),
388 route_path('admin_settings_vcs_update'),
389 params=form_defaults,
389 params=form_defaults,
390 status=302)
390 status=302)
391 response = response.follow()
391 response = response.follow()
392 setting = SettingsModel().get_setting_by_name(setting_key)
392 setting = SettingsModel().get_setting_by_name(setting_key)
393 assert setting.app_settings_value is new_value
393 assert setting.app_settings_value is new_value
394
394
395 @pytest.mark.parametrize('new_value', [True, False])
395 @pytest.mark.parametrize('new_value', [True, False])
396 def test_allows_to_change_hg_rebase_merge_strategy(
396 def test_allows_to_change_hg_rebase_merge_strategy(
397 self, form_defaults, csrf_token, new_value):
397 self, form_defaults, csrf_token, new_value):
398 setting_key = 'hg_use_rebase_for_merging'
398 setting_key = 'hg_use_rebase_for_merging'
399
399
400 form_defaults.update({
400 form_defaults.update({
401 'csrf_token': csrf_token,
401 'csrf_token': csrf_token,
402 'rhodecode_' + setting_key: str(new_value),
402 'rhodecode_' + setting_key: str(new_value),
403 })
403 })
404
404
405 with mock.patch.dict(
405 with mock.patch.dict(
406 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
406 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
407 self.app.post(
407 self.app.post(
408 route_path('admin_settings_vcs_update'),
408 route_path('admin_settings_vcs_update'),
409 params=form_defaults,
409 params=form_defaults,
410 status=302)
410 status=302)
411
411
412 setting = SettingsModel().get_setting_by_name(setting_key)
412 setting = SettingsModel().get_setting_by_name(setting_key)
413 assert setting.app_settings_value is new_value
413 assert setting.app_settings_value is new_value
414
414
415 @pytest.fixture()
415 @pytest.fixture()
416 def disable_sql_cache(self, request):
416 def disable_sql_cache(self, request):
417 patcher = mock.patch(
417 patcher = mock.patch(
418 'rhodecode.lib.caching_query.FromCache.process_query')
418 'rhodecode.lib.caching_query.FromCache.process_query')
419 request.addfinalizer(patcher.stop)
419 request.addfinalizer(patcher.stop)
420 patcher.start()
420 patcher.start()
421
421
422 @pytest.fixture()
422 @pytest.fixture()
423 def form_defaults(self):
423 def form_defaults(self):
424 from rhodecode.apps.admin.views.settings import AdminSettingsView
424 from rhodecode.apps.admin.views.settings import AdminSettingsView
425 return AdminSettingsView._form_defaults()
425 return AdminSettingsView._form_defaults()
426
426
427 # TODO: johbo: What we really want is to checkpoint before a test run and
427 # TODO: johbo: What we really want is to checkpoint before a test run and
428 # reset the session afterwards.
428 # reset the session afterwards.
429 @pytest.fixture(scope='class', autouse=True)
429 @pytest.fixture(scope='class', autouse=True)
430 def cleanup_settings(self, request, baseapp):
430 def cleanup_settings(self, request, baseapp):
431 ui_id = RhodeCodeUi.ui_id
431 ui_id = RhodeCodeUi.ui_id
432 original_ids = list(
432 original_ids = list(
433 r.ui_id for r in RhodeCodeUi.query().values(ui_id))
433 r.ui_id for r in RhodeCodeUi.query().values(ui_id))
434
434
435 @request.addfinalizer
435 @request.addfinalizer
436 def cleanup():
436 def cleanup():
437 RhodeCodeUi.query().filter(
437 RhodeCodeUi.query().filter(
438 ui_id.notin_(original_ids)).delete(False)
438 ui_id.notin_(original_ids)).delete(False)
439
439
440
440
441 @pytest.mark.usefixtures('autologin_user', 'app')
441 @pytest.mark.usefixtures('autologin_user', 'app')
442 class TestLabsSettings(object):
442 class TestLabsSettings(object):
443 def test_get_settings_page_disabled(self):
443 def test_get_settings_page_disabled(self):
444 with mock.patch.dict(
444 with mock.patch.dict(
445 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
445 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
446
446
447 response = self.app.get(
447 response = self.app.get(
448 route_path('admin_settings_labs'), status=302)
448 route_path('admin_settings_labs'), status=302)
449
449
450 assert response.location.endswith(route_path('admin_settings'))
450 assert response.location.endswith(route_path('admin_settings'))
451
451
452 def test_get_settings_page_enabled(self):
452 def test_get_settings_page_enabled(self):
453 from rhodecode.apps.admin.views import settings
453 from rhodecode.apps.admin.views import settings
454 lab_settings = [
454 lab_settings = [
455 settings.LabSetting(
455 settings.LabSetting(
456 key='rhodecode_bool',
456 key='rhodecode_bool',
457 type='bool',
457 type='bool',
458 group='bool group',
458 group='bool group',
459 label='bool label',
459 label='bool label',
460 help='bool help'
460 help='bool help'
461 ),
461 ),
462 settings.LabSetting(
462 settings.LabSetting(
463 key='rhodecode_text',
463 key='rhodecode_text',
464 type='unicode',
464 type='unicode',
465 group='text group',
465 group='text group',
466 label='text label',
466 label='text label',
467 help='text help'
467 help='text help'
468 ),
468 ),
469 ]
469 ]
470 with mock.patch.dict(rhodecode.CONFIG,
470 with mock.patch.dict(rhodecode.CONFIG,
471 {'labs_settings_active': 'true'}):
471 {'labs_settings_active': 'true'}):
472 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
472 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
473 response = self.app.get(route_path('admin_settings_labs'))
473 response = self.app.get(route_path('admin_settings_labs'))
474
474
475 assert '<label>bool group:</label>' in response
475 assert '<label>bool group:</label>' in response
476 assert '<label for="rhodecode_bool">bool label</label>' in response
476 assert '<label for="rhodecode_bool">bool label</label>' in response
477 assert '<p class="help-block">bool help</p>' in response
477 assert '<p class="help-block">bool help</p>' in response
478 assert 'name="rhodecode_bool" type="checkbox"' in response
478 assert 'name="rhodecode_bool" type="checkbox"' in response
479
479
480 assert '<label>text group:</label>' in response
480 assert '<label>text group:</label>' in response
481 assert '<label for="rhodecode_text">text label</label>' in response
481 assert '<label for="rhodecode_text">text label</label>' in response
482 assert '<p class="help-block">text help</p>' in response
482 assert '<p class="help-block">text help</p>' in response
483 assert 'name="rhodecode_text" size="60" type="text"' in response
483 assert 'name="rhodecode_text" size="60" type="text"' in response
484
484
485
485
486 @pytest.mark.usefixtures('app')
486 @pytest.mark.usefixtures('app')
487 class TestOpenSourceLicenses(object):
487 class TestOpenSourceLicenses(object):
488
488
489 def test_records_are_displayed(self, autologin_user):
489 def test_records_are_displayed(self, autologin_user):
490 sample_licenses = [
490 sample_licenses = [
491 {
491 {
492 "license": [
492 "license": [
493 {
493 {
494 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
494 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
495 "shortName": "bsdOriginal",
495 "shortName": "bsdOriginal",
496 "spdxId": "BSD-4-Clause",
496 "spdxId": "BSD-4-Clause",
497 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
497 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
498 }
498 }
499 ],
499 ],
500 "name": "python2.7-coverage-3.7.1"
500 "name": "python2.7-coverage-3.7.1"
501 },
501 },
502 {
502 {
503 "license": [
503 "license": [
504 {
504 {
505 "fullName": "MIT License",
505 "fullName": "MIT License",
506 "shortName": "mit",
506 "shortName": "mit",
507 "spdxId": "MIT",
507 "spdxId": "MIT",
508 "url": "http://spdx.org/licenses/MIT.html"
508 "url": "http://spdx.org/licenses/MIT.html"
509 }
509 }
510 ],
510 ],
511 "name": "python2.7-bootstrapped-pip-9.0.1"
511 "name": "python2.7-bootstrapped-pip-9.0.1"
512 },
512 },
513 ]
513 ]
514 read_licenses_patch = mock.patch(
514 read_licenses_patch = mock.patch(
515 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
515 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
516 return_value=sample_licenses)
516 return_value=sample_licenses)
517 with read_licenses_patch:
517 with read_licenses_patch:
518 response = self.app.get(
518 response = self.app.get(
519 route_path('admin_settings_open_source'), status=200)
519 route_path('admin_settings_open_source'), status=200)
520
520
521 assert_response = response.assert_response()
521 assert_response = response.assert_response()
522 assert_response.element_contains(
522 assert_response.element_contains(
523 '.panel-heading', 'Licenses of Third Party Packages')
523 '.panel-heading', 'Licenses of Third Party Packages')
524 for license_data in sample_licenses:
524 for license_data in sample_licenses:
525 response.mustcontain(license_data["license"][0]["spdxId"])
525 response.mustcontain(license_data["license"][0]["spdxId"])
526 assert_response.element_contains('.panel-body', license_data["name"])
526 assert_response.element_contains('.panel-body', license_data["name"])
527
527
528 def test_records_can_be_read(self, autologin_user):
528 def test_records_can_be_read(self, autologin_user):
529 response = self.app.get(
529 response = self.app.get(
530 route_path('admin_settings_open_source'), status=200)
530 route_path('admin_settings_open_source'), status=200)
531 assert_response = response.assert_response()
531 assert_response = response.assert_response()
532 assert_response.element_contains(
532 assert_response.element_contains(
533 '.panel-heading', 'Licenses of Third Party Packages')
533 '.panel-heading', 'Licenses of Third Party Packages')
534
534
535 def test_forbidden_when_normal_user(self, autologin_regular_user):
535 def test_forbidden_when_normal_user(self, autologin_regular_user):
536 self.app.get(
536 self.app.get(
537 route_path('admin_settings_open_source'), status=404)
537 route_path('admin_settings_open_source'), status=404)
538
538
539
539
540 @pytest.mark.usefixtures('app')
540 @pytest.mark.usefixtures('app')
541 class TestUserSessions(object):
541 class TestUserSessions(object):
542
542
543 def test_forbidden_when_normal_user(self, autologin_regular_user):
543 def test_forbidden_when_normal_user(self, autologin_regular_user):
544 self.app.get(route_path('admin_settings_sessions'), status=404)
544 self.app.get(route_path('admin_settings_sessions'), status=404)
545
545
546 def test_show_sessions_page(self, autologin_user):
546 def test_show_sessions_page(self, autologin_user):
547 response = self.app.get(route_path('admin_settings_sessions'), status=200)
547 response = self.app.get(route_path('admin_settings_sessions'), status=200)
548 response.mustcontain('file')
548 response.mustcontain('file')
549
549
550 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
550 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
551
551
552 post_data = {
552 post_data = {
553 'csrf_token': csrf_token,
553 'csrf_token': csrf_token,
554 'expire_days': '60'
554 'expire_days': '60'
555 }
555 }
556 response = self.app.post(
556 response = self.app.post(
557 route_path('admin_settings_sessions_cleanup'), params=post_data,
557 route_path('admin_settings_sessions_cleanup'), params=post_data,
558 status=302)
558 status=302)
559 assert_session_flash(response, 'Cleaned up old sessions')
559 assert_session_flash(response, 'Cleaned up old sessions')
560
560
561
561
562 @pytest.mark.usefixtures('app')
562 @pytest.mark.usefixtures('app')
563 class TestAdminSystemInfo(object):
563 class TestAdminSystemInfo(object):
564
564
565 def test_forbidden_when_normal_user(self, autologin_regular_user):
565 def test_forbidden_when_normal_user(self, autologin_regular_user):
566 self.app.get(route_path('admin_settings_system'), status=404)
566 self.app.get(route_path('admin_settings_system'), status=404)
567
567
568 def test_system_info_page(self, autologin_user):
568 def test_system_info_page(self, autologin_user):
569 response = self.app.get(route_path('admin_settings_system'))
569 response = self.app.get(route_path('admin_settings_system'))
570 response.mustcontain('RhodeCode Community Edition, version {}'.format(
570 response.mustcontain('RhodeCode Community Edition, version {}'.format(
571 rhodecode.__version__))
571 rhodecode.__version__))
572
572
573 def test_system_update_new_version(self, autologin_user):
573 def test_system_update_new_version(self, autologin_user):
574 update_data = {
574 update_data = {
575 'versions': [
575 'versions': [
576 {
576 {
577 'version': '100.3.1415926535',
577 'version': '100.3.1415926535',
578 'general': 'The latest version we are ever going to ship'
578 'general': 'The latest version we are ever going to ship'
579 },
579 },
580 {
580 {
581 'version': '0.0.0',
581 'version': '0.0.0',
582 'general': 'The first version we ever shipped'
582 'general': 'The first version we ever shipped'
583 }
583 }
584 ]
584 ]
585 }
585 }
586 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
586 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
587 response = self.app.get(route_path('admin_settings_system_update'))
587 response = self.app.get(route_path('admin_settings_system_update'))
588 response.mustcontain('A <b>new version</b> is available')
588 response.mustcontain('A <b>new version</b> is available')
589
589
590 def test_system_update_nothing_new(self, autologin_user):
590 def test_system_update_nothing_new(self, autologin_user):
591 update_data = {
591 update_data = {
592 'versions': [
592 'versions': [
593 {
593 {
594 'version': '0.0.0',
594 'version': '0.0.0',
595 'general': 'The first version we ever shipped'
595 'general': 'The first version we ever shipped'
596 }
596 }
597 ]
597 ]
598 }
598 }
599 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
599 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
600 response = self.app.get(route_path('admin_settings_system_update'))
600 response = self.app.get(route_path('admin_settings_system_update'))
601 response.mustcontain(
601 response.mustcontain(
602 'This instance is already running the <b>latest</b> stable version')
602 'This instance is already running the <b>latest</b> stable version')
603
603
604 def test_system_update_bad_response(self, autologin_user):
604 def test_system_update_bad_response(self, autologin_user):
605 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
605 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
606 response = self.app.get(route_path('admin_settings_system_update'))
606 response = self.app.get(route_path('admin_settings_system_update'))
607 response.mustcontain(
607 response.mustcontain(
608 'Bad data sent from update server')
608 'Bad data sent from update server')
609
609
610
610
611 @pytest.mark.usefixtures("app")
611 @pytest.mark.usefixtures("app")
612 class TestAdminSettingsIssueTracker(object):
612 class TestAdminSettingsIssueTracker(object):
613 RC_PREFIX = 'rhodecode_'
613 RC_PREFIX = 'rhodecode_'
614 SHORT_PATTERN_KEY = 'issuetracker_pat_'
614 SHORT_PATTERN_KEY = 'issuetracker_pat_'
615 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
615 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
616 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
616 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
617
617
618 def test_issuetracker_index(self, autologin_user):
618 def test_issuetracker_index(self, autologin_user):
619 response = self.app.get(route_path('admin_settings_issuetracker'))
619 response = self.app.get(route_path('admin_settings_issuetracker'))
620 assert response.status_code == 200
620 assert response.status_code == 200
621
621
622 def test_add_empty_issuetracker_pattern(
622 def test_add_empty_issuetracker_pattern(
623 self, request, autologin_user, csrf_token):
623 self, request, autologin_user, csrf_token):
624 post_url = route_path('admin_settings_issuetracker_update')
624 post_url = route_path('admin_settings_issuetracker_update')
625 post_data = {
625 post_data = {
626 'csrf_token': csrf_token
626 'csrf_token': csrf_token
627 }
627 }
628 self.app.post(post_url, post_data, status=302)
628 self.app.post(post_url, post_data, status=302)
629
629
630 def test_add_issuetracker_pattern(
630 def test_add_issuetracker_pattern(
631 self, request, autologin_user, csrf_token):
631 self, request, autologin_user, csrf_token):
632 pattern = 'issuetracker_pat'
632 pattern = 'issuetracker_pat'
633 another_pattern = pattern+'1'
633 another_pattern = pattern+'1'
634 post_url = route_path('admin_settings_issuetracker_update')
634 post_url = route_path('admin_settings_issuetracker_update')
635 post_data = {
635 post_data = {
636 'new_pattern_pattern_0': pattern,
636 'new_pattern_pattern_0': pattern,
637 'new_pattern_url_0': 'http://url',
637 'new_pattern_url_0': 'http://url',
638 'new_pattern_prefix_0': 'prefix',
638 'new_pattern_prefix_0': 'prefix',
639 'new_pattern_description_0': 'description',
639 'new_pattern_description_0': 'description',
640 'new_pattern_pattern_1': another_pattern,
640 'new_pattern_pattern_1': another_pattern,
641 'new_pattern_url_1': 'https://url1',
641 'new_pattern_url_1': 'https://url1',
642 'new_pattern_prefix_1': 'prefix1',
642 'new_pattern_prefix_1': 'prefix1',
643 'new_pattern_description_1': 'description1',
643 'new_pattern_description_1': 'description1',
644 'csrf_token': csrf_token
644 'csrf_token': csrf_token
645 }
645 }
646 self.app.post(post_url, post_data, status=302)
646 self.app.post(post_url, post_data, status=302)
647 settings = SettingsModel().get_all_settings()
647 settings = SettingsModel().get_all_settings()
648 self.uid = md5(pattern)
648 self.uid = md5(pattern)
649 assert settings[self.PATTERN_KEY+self.uid] == pattern
649 assert settings[self.PATTERN_KEY+self.uid] == pattern
650 self.another_uid = md5(another_pattern)
650 self.another_uid = md5(another_pattern)
651 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
651 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
652
652
653 @request.addfinalizer
653 @request.addfinalizer
654 def cleanup():
654 def cleanup():
655 defaults = SettingsModel().get_all_settings()
655 defaults = SettingsModel().get_all_settings()
656
656
657 entries = [name for name in defaults if (
657 entries = [name for name in defaults if (
658 (self.uid in name) or (self.another_uid) in name)]
658 (self.uid in name) or (self.another_uid) in name)]
659 start = len(self.RC_PREFIX)
659 start = len(self.RC_PREFIX)
660 for del_key in entries:
660 for del_key in entries:
661 # TODO: anderson: get_by_name needs name without prefix
661 # TODO: anderson: get_by_name needs name without prefix
662 entry = SettingsModel().get_setting_by_name(del_key[start:])
662 entry = SettingsModel().get_setting_by_name(del_key[start:])
663 Session().delete(entry)
663 Session().delete(entry)
664
664
665 Session().commit()
665 Session().commit()
666
666
667 def test_edit_issuetracker_pattern(
667 def test_edit_issuetracker_pattern(
668 self, autologin_user, backend, csrf_token, request):
668 self, autologin_user, backend, csrf_token, request):
669
669
670 old_pattern = 'issuetracker_pat1'
670 old_pattern = 'issuetracker_pat1'
671 old_uid = md5(old_pattern)
671 old_uid = md5(old_pattern)
672
672
673 post_url = route_path('admin_settings_issuetracker_update')
673 post_url = route_path('admin_settings_issuetracker_update')
674 post_data = {
674 post_data = {
675 'new_pattern_pattern_0': old_pattern,
675 'new_pattern_pattern_0': old_pattern,
676 'new_pattern_url_0': 'http://url',
676 'new_pattern_url_0': 'http://url',
677 'new_pattern_prefix_0': 'prefix',
677 'new_pattern_prefix_0': 'prefix',
678 'new_pattern_description_0': 'description',
678 'new_pattern_description_0': 'description',
679
679
680 'csrf_token': csrf_token
680 'csrf_token': csrf_token
681 }
681 }
682 self.app.post(post_url, post_data, status=302)
682 self.app.post(post_url, post_data, status=302)
683
683
684 new_pattern = 'issuetracker_pat1_edited'
684 new_pattern = 'issuetracker_pat1_edited'
685 self.new_uid = md5(new_pattern)
685 self.new_uid = md5(new_pattern)
686
686
687 post_url = route_path('admin_settings_issuetracker_update')
687 post_url = route_path('admin_settings_issuetracker_update')
688 post_data = {
688 post_data = {
689 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
689 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
690 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
690 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
691 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
691 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
692 'new_pattern_description_{}'.format(old_uid): 'description_edited',
692 'new_pattern_description_{}'.format(old_uid): 'description_edited',
693 'uid': old_uid,
693 'uid': old_uid,
694 'csrf_token': csrf_token
694 'csrf_token': csrf_token
695 }
695 }
696 self.app.post(post_url, post_data, status=302)
696 self.app.post(post_url, post_data, status=302)
697
697
698 settings = SettingsModel().get_all_settings()
698 settings = SettingsModel().get_all_settings()
699 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
699 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
700 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
700 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
701 assert self.PATTERN_KEY+old_uid not in settings
701 assert self.PATTERN_KEY+old_uid not in settings
702
702
703 @request.addfinalizer
703 @request.addfinalizer
704 def cleanup():
704 def cleanup():
705 IssueTrackerSettingsModel().delete_entries(old_uid)
705 IssueTrackerSettingsModel().delete_entries(old_uid)
706 IssueTrackerSettingsModel().delete_entries(self.new_uid)
706 IssueTrackerSettingsModel().delete_entries(self.new_uid)
707
707
708 def test_replace_issuetracker_pattern_description(
708 def test_replace_issuetracker_pattern_description(
709 self, autologin_user, csrf_token, request, settings_util):
709 self, autologin_user, csrf_token, request, settings_util):
710 prefix = 'issuetracker'
710 prefix = 'issuetracker'
711 pattern = 'issuetracker_pat'
711 pattern = 'issuetracker_pat'
712 self.uid = md5(pattern)
712 self.uid = md5(pattern)
713 pattern_key = '_'.join([prefix, 'pat', self.uid])
713 pattern_key = '_'.join([prefix, 'pat', self.uid])
714 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
714 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
715 desc_key = '_'.join([prefix, 'desc', self.uid])
715 desc_key = '_'.join([prefix, 'desc', self.uid])
716 rc_desc_key = '_'.join(['rhodecode', desc_key])
716 rc_desc_key = '_'.join(['rhodecode', desc_key])
717 new_description = 'new_description'
717 new_description = 'new_description'
718
718
719 settings_util.create_rhodecode_setting(
719 settings_util.create_rhodecode_setting(
720 pattern_key, pattern, 'unicode', cleanup=False)
720 pattern_key, pattern, 'unicode', cleanup=False)
721 settings_util.create_rhodecode_setting(
721 settings_util.create_rhodecode_setting(
722 desc_key, 'old description', 'unicode', cleanup=False)
722 desc_key, 'old description', 'unicode', cleanup=False)
723
723
724 post_url = route_path('admin_settings_issuetracker_update')
724 post_url = route_path('admin_settings_issuetracker_update')
725 post_data = {
725 post_data = {
726 'new_pattern_pattern_0': pattern,
726 'new_pattern_pattern_0': pattern,
727 'new_pattern_url_0': 'https://url',
727 'new_pattern_url_0': 'https://url',
728 'new_pattern_prefix_0': 'prefix',
728 'new_pattern_prefix_0': 'prefix',
729 'new_pattern_description_0': new_description,
729 'new_pattern_description_0': new_description,
730 'uid': self.uid,
730 'uid': self.uid,
731 'csrf_token': csrf_token
731 'csrf_token': csrf_token
732 }
732 }
733 self.app.post(post_url, post_data, status=302)
733 self.app.post(post_url, post_data, status=302)
734 settings = SettingsModel().get_all_settings()
734 settings = SettingsModel().get_all_settings()
735 assert settings[rc_pattern_key] == pattern
735 assert settings[rc_pattern_key] == pattern
736 assert settings[rc_desc_key] == new_description
736 assert settings[rc_desc_key] == new_description
737
737
738 @request.addfinalizer
738 @request.addfinalizer
739 def cleanup():
739 def cleanup():
740 IssueTrackerSettingsModel().delete_entries(self.uid)
740 IssueTrackerSettingsModel().delete_entries(self.uid)
741
741
742 def test_delete_issuetracker_pattern(
742 def test_delete_issuetracker_pattern(
743 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
743 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
744
744
745 old_pattern = 'issuetracker_pat_deleted'
745 old_pattern = 'issuetracker_pat_deleted'
746 old_uid = md5(old_pattern)
746 old_uid = md5(old_pattern)
747
747
748 post_url = route_path('admin_settings_issuetracker_update')
748 post_url = route_path('admin_settings_issuetracker_update')
749 post_data = {
749 post_data = {
750 'new_pattern_pattern_0': old_pattern,
750 'new_pattern_pattern_0': old_pattern,
751 'new_pattern_url_0': 'http://url',
751 'new_pattern_url_0': 'http://url',
752 'new_pattern_prefix_0': 'prefix',
752 'new_pattern_prefix_0': 'prefix',
753 'new_pattern_description_0': 'description',
753 'new_pattern_description_0': 'description',
754
754
755 'csrf_token': csrf_token
755 'csrf_token': csrf_token
756 }
756 }
757 self.app.post(post_url, post_data, status=302)
757 self.app.post(post_url, post_data, status=302)
758
758
759 post_url = route_path('admin_settings_issuetracker_delete')
759 post_url = route_path('admin_settings_issuetracker_delete')
760 post_data = {
760 post_data = {
761 'uid': old_uid,
761 'uid': old_uid,
762 'csrf_token': csrf_token
762 'csrf_token': csrf_token
763 }
763 }
764 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
764 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
765 settings = SettingsModel().get_all_settings()
765 settings = SettingsModel().get_all_settings()
766 assert self.PATTERN_KEY+old_uid not in settings
766 assert self.PATTERN_KEY+old_uid not in settings
767 assert self.DESC_KEY + old_uid not in settings
767 assert self.DESC_KEY + old_uid not in settings
@@ -1,170 +1,170 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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.model.db import UserGroup, User
23 from rhodecode.model.db import UserGroup, User
24 from rhodecode.model.meta import Session
24 from rhodecode.model.meta import Session
25
25
26 from rhodecode.tests import (
26 from rhodecode.tests import (
27 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
27 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
28 from rhodecode.tests.fixture import Fixture
28 from rhodecode.tests.fixture import Fixture
29
29
30 fixture = Fixture()
30 fixture = Fixture()
31
31
32
32
33 def route_path(name, params=None, **kwargs):
33 def route_path(name, params=None, **kwargs):
34 import urllib
34 import urllib.request, urllib.parse, urllib.error
35 from rhodecode.apps._base import ADMIN_PREFIX
35 from rhodecode.apps._base import ADMIN_PREFIX
36
36
37 base_url = {
37 base_url = {
38 'user_groups': ADMIN_PREFIX + '/user_groups',
38 'user_groups': ADMIN_PREFIX + '/user_groups',
39 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
39 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
40 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
40 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
41 'user_groups_new': ADMIN_PREFIX + '/user_groups/new',
41 'user_groups_new': ADMIN_PREFIX + '/user_groups/new',
42 'user_groups_create': ADMIN_PREFIX + '/user_groups/create',
42 'user_groups_create': ADMIN_PREFIX + '/user_groups/create',
43 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit',
43 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit',
44 }[name].format(**kwargs)
44 }[name].format(**kwargs)
45
45
46 if params:
46 if params:
47 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 return base_url
48 return base_url
49
49
50
50
51 class TestAdminUserGroupsView(TestController):
51 class TestAdminUserGroupsView(TestController):
52
52
53 def test_show_users(self):
53 def test_show_users(self):
54 self.log_user()
54 self.log_user()
55 self.app.get(route_path('user_groups'))
55 self.app.get(route_path('user_groups'))
56
56
57 def test_show_user_groups_data(self, xhr_header):
57 def test_show_user_groups_data(self, xhr_header):
58 self.log_user()
58 self.log_user()
59 response = self.app.get(route_path(
59 response = self.app.get(route_path(
60 'user_groups_data'), extra_environ=xhr_header)
60 'user_groups_data'), extra_environ=xhr_header)
61
61
62 all_user_groups = UserGroup.query().count()
62 all_user_groups = UserGroup.query().count()
63 assert response.json['recordsTotal'] == all_user_groups
63 assert response.json['recordsTotal'] == all_user_groups
64
64
65 def test_show_user_groups_data_filtered(self, xhr_header):
65 def test_show_user_groups_data_filtered(self, xhr_header):
66 self.log_user()
66 self.log_user()
67 response = self.app.get(route_path(
67 response = self.app.get(route_path(
68 'user_groups_data', params={'search[value]': 'empty_search'}),
68 'user_groups_data', params={'search[value]': 'empty_search'}),
69 extra_environ=xhr_header)
69 extra_environ=xhr_header)
70
70
71 all_user_groups = UserGroup.query().count()
71 all_user_groups = UserGroup.query().count()
72 assert response.json['recordsTotal'] == all_user_groups
72 assert response.json['recordsTotal'] == all_user_groups
73 assert response.json['recordsFiltered'] == 0
73 assert response.json['recordsFiltered'] == 0
74
74
75 def test_usergroup_escape(self, user_util, xhr_header):
75 def test_usergroup_escape(self, user_util, xhr_header):
76 self.log_user()
76 self.log_user()
77
77
78 xss_img = '<img src="/image1" onload="alert(\'Hello, World!\');">'
78 xss_img = '<img src="/image1" onload="alert(\'Hello, World!\');">'
79 user = user_util.create_user()
79 user = user_util.create_user()
80 user.name = xss_img
80 user.name = xss_img
81 user.lastname = xss_img
81 user.lastname = xss_img
82 Session().add(user)
82 Session().add(user)
83 Session().commit()
83 Session().commit()
84
84
85 user_group = user_util.create_user_group()
85 user_group = user_util.create_user_group()
86
86
87 user_group.users_group_name = xss_img
87 user_group.users_group_name = xss_img
88 user_group.user_group_description = '<strong onload="alert();">DESC</strong>'
88 user_group.user_group_description = '<strong onload="alert();">DESC</strong>'
89
89
90 response = self.app.get(
90 response = self.app.get(
91 route_path('user_groups_data'), extra_environ=xhr_header)
91 route_path('user_groups_data'), extra_environ=xhr_header)
92
92
93 response.mustcontain(
93 response.mustcontain(
94 '&lt;strong onload=&#34;alert();&#34;&gt;DESC&lt;/strong&gt;')
94 '&lt;strong onload=&#34;alert();&#34;&gt;DESC&lt;/strong&gt;')
95 response.mustcontain(
95 response.mustcontain(
96 '&lt;img src=&#34;/image1&#34; onload=&#34;'
96 '&lt;img src=&#34;/image1&#34; onload=&#34;'
97 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
97 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
98
98
99 def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
99 def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
100 self.log_user()
100 self.log_user()
101 ug = user_util.create_user_group()
101 ug = user_util.create_user_group()
102 response = self.app.get(
102 response = self.app.get(
103 route_path('user_group_members_data', user_group_id=ug.users_group_id),
103 route_path('user_group_members_data', user_group_id=ug.users_group_id),
104 extra_environ=xhr_header)
104 extra_environ=xhr_header)
105
105
106 assert response.json == {'members': []}
106 assert response.json == {'members': []}
107
107
108 def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
108 def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
109 self.log_user()
109 self.log_user()
110 members = [u.user_id for u in User.get_all()]
110 members = [u.user_id for u in User.get_all()]
111 ug = user_util.create_user_group(members=members)
111 ug = user_util.create_user_group(members=members)
112 response = self.app.get(
112 response = self.app.get(
113 route_path('user_group_members_data',
113 route_path('user_group_members_data',
114 user_group_id=ug.users_group_id),
114 user_group_id=ug.users_group_id),
115 extra_environ=xhr_header)
115 extra_environ=xhr_header)
116
116
117 assert len(response.json['members']) == len(members)
117 assert len(response.json['members']) == len(members)
118
118
119 def test_creation_page(self):
119 def test_creation_page(self):
120 self.log_user()
120 self.log_user()
121 self.app.get(route_path('user_groups_new'), status=200)
121 self.app.get(route_path('user_groups_new'), status=200)
122
122
123 def test_create(self):
123 def test_create(self):
124 from rhodecode.lib import helpers as h
124 from rhodecode.lib import helpers as h
125
125
126 self.log_user()
126 self.log_user()
127 users_group_name = 'test_user_group'
127 users_group_name = 'test_user_group'
128 response = self.app.post(route_path('user_groups_create'), {
128 response = self.app.post(route_path('user_groups_create'), {
129 'users_group_name': users_group_name,
129 'users_group_name': users_group_name,
130 'user_group_description': 'DESC',
130 'user_group_description': 'DESC',
131 'active': True,
131 'active': True,
132 'csrf_token': self.csrf_token})
132 'csrf_token': self.csrf_token})
133
133
134 user_group_id = UserGroup.get_by_group_name(
134 user_group_id = UserGroup.get_by_group_name(
135 users_group_name).users_group_id
135 users_group_name).users_group_id
136
136
137 user_group_link = h.link_to(
137 user_group_link = h.link_to(
138 users_group_name,
138 users_group_name,
139 route_path('edit_user_group', user_group_id=user_group_id))
139 route_path('edit_user_group', user_group_id=user_group_id))
140
140
141 assert_session_flash(
141 assert_session_flash(
142 response,
142 response,
143 'Created user group %s' % user_group_link)
143 'Created user group %s' % user_group_link)
144
144
145 fixture.destroy_user_group(users_group_name)
145 fixture.destroy_user_group(users_group_name)
146
146
147 def test_create_with_empty_name(self):
147 def test_create_with_empty_name(self):
148 self.log_user()
148 self.log_user()
149
149
150 response = self.app.post(route_path('user_groups_create'), {
150 response = self.app.post(route_path('user_groups_create'), {
151 'users_group_name': '',
151 'users_group_name': '',
152 'user_group_description': 'DESC',
152 'user_group_description': 'DESC',
153 'active': True,
153 'active': True,
154 'csrf_token': self.csrf_token}, status=200)
154 'csrf_token': self.csrf_token}, status=200)
155
155
156 response.mustcontain('Please enter a value')
156 response.mustcontain('Please enter a value')
157
157
158 def test_create_duplicate(self, user_util):
158 def test_create_duplicate(self, user_util):
159 self.log_user()
159 self.log_user()
160
160
161 user_group = user_util.create_user_group()
161 user_group = user_util.create_user_group()
162 duplicate_name = user_group.users_group_name
162 duplicate_name = user_group.users_group_name
163 response = self.app.post(route_path('user_groups_create'), {
163 response = self.app.post(route_path('user_groups_create'), {
164 'users_group_name': duplicate_name,
164 'users_group_name': duplicate_name,
165 'user_group_description': 'DESC',
165 'user_group_description': 'DESC',
166 'active': True,
166 'active': True,
167 'csrf_token': self.csrf_token}, status=200)
167 'csrf_token': self.csrf_token}, status=200)
168
168
169 response.mustcontain(
169 response.mustcontain(
170 'User group `{}` already exists'.format(duplicate_name))
170 'User group `{}` already exists'.format(duplicate_name))
@@ -1,794 +1,794 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 sqlalchemy.orm.exc import NoResultFound
22 from sqlalchemy.orm.exc import NoResultFound
23
23
24 from rhodecode.lib import auth
24 from rhodecode.lib import auth
25 from rhodecode.lib import helpers as h
25 from rhodecode.lib import helpers as h
26 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
26 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.model.user import UserModel
28 from rhodecode.model.user import UserModel
29
29
30 from rhodecode.tests import (
30 from rhodecode.tests import (
31 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
31 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
32 from rhodecode.tests.fixture import Fixture
32 from rhodecode.tests.fixture import Fixture
33
33
34 fixture = Fixture()
34 fixture = Fixture()
35
35
36
36
37 def route_path(name, params=None, **kwargs):
37 def route_path(name, params=None, **kwargs):
38 import urllib
38 import urllib.request, urllib.parse, urllib.error
39 from rhodecode.apps._base import ADMIN_PREFIX
39 from rhodecode.apps._base import ADMIN_PREFIX
40
40
41 base_url = {
41 base_url = {
42 'users':
42 'users':
43 ADMIN_PREFIX + '/users',
43 ADMIN_PREFIX + '/users',
44 'users_data':
44 'users_data':
45 ADMIN_PREFIX + '/users_data',
45 ADMIN_PREFIX + '/users_data',
46 'users_create':
46 'users_create':
47 ADMIN_PREFIX + '/users/create',
47 ADMIN_PREFIX + '/users/create',
48 'users_new':
48 'users_new':
49 ADMIN_PREFIX + '/users/new',
49 ADMIN_PREFIX + '/users/new',
50 'user_edit':
50 'user_edit':
51 ADMIN_PREFIX + '/users/{user_id}/edit',
51 ADMIN_PREFIX + '/users/{user_id}/edit',
52 'user_edit_advanced':
52 'user_edit_advanced':
53 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
53 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
54 'user_edit_global_perms':
54 'user_edit_global_perms':
55 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
55 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
56 'user_edit_global_perms_update':
56 'user_edit_global_perms_update':
57 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
57 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
58 'user_update':
58 'user_update':
59 ADMIN_PREFIX + '/users/{user_id}/update',
59 ADMIN_PREFIX + '/users/{user_id}/update',
60 'user_delete':
60 'user_delete':
61 ADMIN_PREFIX + '/users/{user_id}/delete',
61 ADMIN_PREFIX + '/users/{user_id}/delete',
62 'user_create_personal_repo_group':
62 'user_create_personal_repo_group':
63 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
63 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
64
64
65 'edit_user_auth_tokens':
65 'edit_user_auth_tokens':
66 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
66 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
67 'edit_user_auth_tokens_add':
67 'edit_user_auth_tokens_add':
68 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
68 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
69 'edit_user_auth_tokens_delete':
69 'edit_user_auth_tokens_delete':
70 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
70 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
71
71
72 'edit_user_emails':
72 'edit_user_emails':
73 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
73 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
74 'edit_user_emails_add':
74 'edit_user_emails_add':
75 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
75 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
76 'edit_user_emails_delete':
76 'edit_user_emails_delete':
77 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
77 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
78
78
79 'edit_user_ips':
79 'edit_user_ips':
80 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
80 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
81 'edit_user_ips_add':
81 'edit_user_ips_add':
82 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
82 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
83 'edit_user_ips_delete':
83 'edit_user_ips_delete':
84 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
84 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
85
85
86 'edit_user_perms_summary':
86 'edit_user_perms_summary':
87 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
87 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
88 'edit_user_perms_summary_json':
88 'edit_user_perms_summary_json':
89 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
89 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
90
90
91 'edit_user_audit_logs':
91 'edit_user_audit_logs':
92 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
92 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
93
93
94 'edit_user_audit_logs_download':
94 'edit_user_audit_logs_download':
95 ADMIN_PREFIX + '/users/{user_id}/edit/audit/download',
95 ADMIN_PREFIX + '/users/{user_id}/edit/audit/download',
96
96
97 }[name].format(**kwargs)
97 }[name].format(**kwargs)
98
98
99 if params:
99 if params:
100 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
100 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
101 return base_url
101 return base_url
102
102
103
103
104 class TestAdminUsersView(TestController):
104 class TestAdminUsersView(TestController):
105
105
106 def test_show_users(self):
106 def test_show_users(self):
107 self.log_user()
107 self.log_user()
108 self.app.get(route_path('users'))
108 self.app.get(route_path('users'))
109
109
110 def test_show_users_data(self, xhr_header):
110 def test_show_users_data(self, xhr_header):
111 self.log_user()
111 self.log_user()
112 response = self.app.get(route_path(
112 response = self.app.get(route_path(
113 'users_data'), extra_environ=xhr_header)
113 'users_data'), extra_environ=xhr_header)
114
114
115 all_users = User.query().filter(
115 all_users = User.query().filter(
116 User.username != User.DEFAULT_USER).count()
116 User.username != User.DEFAULT_USER).count()
117 assert response.json['recordsTotal'] == all_users
117 assert response.json['recordsTotal'] == all_users
118
118
119 def test_show_users_data_filtered(self, xhr_header):
119 def test_show_users_data_filtered(self, xhr_header):
120 self.log_user()
120 self.log_user()
121 response = self.app.get(route_path(
121 response = self.app.get(route_path(
122 'users_data', params={'search[value]': 'empty_search'}),
122 'users_data', params={'search[value]': 'empty_search'}),
123 extra_environ=xhr_header)
123 extra_environ=xhr_header)
124
124
125 all_users = User.query().filter(
125 all_users = User.query().filter(
126 User.username != User.DEFAULT_USER).count()
126 User.username != User.DEFAULT_USER).count()
127 assert response.json['recordsTotal'] == all_users
127 assert response.json['recordsTotal'] == all_users
128 assert response.json['recordsFiltered'] == 0
128 assert response.json['recordsFiltered'] == 0
129
129
130 def test_auth_tokens_default_user(self):
130 def test_auth_tokens_default_user(self):
131 self.log_user()
131 self.log_user()
132 user = User.get_default_user()
132 user = User.get_default_user()
133 response = self.app.get(
133 response = self.app.get(
134 route_path('edit_user_auth_tokens', user_id=user.user_id),
134 route_path('edit_user_auth_tokens', user_id=user.user_id),
135 status=302)
135 status=302)
136
136
137 def test_auth_tokens(self):
137 def test_auth_tokens(self):
138 self.log_user()
138 self.log_user()
139
139
140 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
140 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
141 user_id = user.user_id
141 user_id = user.user_id
142 auth_tokens = user.auth_tokens
142 auth_tokens = user.auth_tokens
143 response = self.app.get(
143 response = self.app.get(
144 route_path('edit_user_auth_tokens', user_id=user_id))
144 route_path('edit_user_auth_tokens', user_id=user_id))
145 for token in auth_tokens:
145 for token in auth_tokens:
146 response.mustcontain(token[:4])
146 response.mustcontain(token[:4])
147 response.mustcontain('never')
147 response.mustcontain('never')
148
148
149 @pytest.mark.parametrize("desc, lifetime", [
149 @pytest.mark.parametrize("desc, lifetime", [
150 ('forever', -1),
150 ('forever', -1),
151 ('5mins', 60*5),
151 ('5mins', 60*5),
152 ('30days', 60*60*24*30),
152 ('30days', 60*60*24*30),
153 ])
153 ])
154 def test_add_auth_token(self, desc, lifetime, user_util):
154 def test_add_auth_token(self, desc, lifetime, user_util):
155 self.log_user()
155 self.log_user()
156 user = user_util.create_user()
156 user = user_util.create_user()
157 user_id = user.user_id
157 user_id = user.user_id
158
158
159 response = self.app.post(
159 response = self.app.post(
160 route_path('edit_user_auth_tokens_add', user_id=user_id),
160 route_path('edit_user_auth_tokens_add', user_id=user_id),
161 {'description': desc, 'lifetime': lifetime,
161 {'description': desc, 'lifetime': lifetime,
162 'csrf_token': self.csrf_token})
162 'csrf_token': self.csrf_token})
163 assert_session_flash(response, 'Auth token successfully created')
163 assert_session_flash(response, 'Auth token successfully created')
164
164
165 response = response.follow()
165 response = response.follow()
166 user = User.get(user_id)
166 user = User.get(user_id)
167 for auth_token in user.auth_tokens:
167 for auth_token in user.auth_tokens:
168 response.mustcontain(auth_token[:4])
168 response.mustcontain(auth_token[:4])
169
169
170 def test_delete_auth_token(self, user_util):
170 def test_delete_auth_token(self, user_util):
171 self.log_user()
171 self.log_user()
172 user = user_util.create_user()
172 user = user_util.create_user()
173 user_id = user.user_id
173 user_id = user.user_id
174 keys = user.auth_tokens
174 keys = user.auth_tokens
175 assert 2 == len(keys)
175 assert 2 == len(keys)
176
176
177 response = self.app.post(
177 response = self.app.post(
178 route_path('edit_user_auth_tokens_add', user_id=user_id),
178 route_path('edit_user_auth_tokens_add', user_id=user_id),
179 {'description': 'desc', 'lifetime': -1,
179 {'description': 'desc', 'lifetime': -1,
180 'csrf_token': self.csrf_token})
180 'csrf_token': self.csrf_token})
181 assert_session_flash(response, 'Auth token successfully created')
181 assert_session_flash(response, 'Auth token successfully created')
182 response.follow()
182 response.follow()
183
183
184 # now delete our key
184 # now delete our key
185 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
185 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
186 assert 3 == len(keys)
186 assert 3 == len(keys)
187
187
188 response = self.app.post(
188 response = self.app.post(
189 route_path('edit_user_auth_tokens_delete', user_id=user_id),
189 route_path('edit_user_auth_tokens_delete', user_id=user_id),
190 {'del_auth_token': keys[0].user_api_key_id,
190 {'del_auth_token': keys[0].user_api_key_id,
191 'csrf_token': self.csrf_token})
191 'csrf_token': self.csrf_token})
192
192
193 assert_session_flash(response, 'Auth token successfully deleted')
193 assert_session_flash(response, 'Auth token successfully deleted')
194 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
194 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
195 assert 2 == len(keys)
195 assert 2 == len(keys)
196
196
197 def test_ips(self):
197 def test_ips(self):
198 self.log_user()
198 self.log_user()
199 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
199 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
200 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
200 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
201 response.mustcontain('All IP addresses are allowed')
201 response.mustcontain('All IP addresses are allowed')
202
202
203 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
203 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
204 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
204 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
205 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
205 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
206 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
206 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
207 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
207 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
208 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
208 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
209 ('127_bad_ip', 'foobar', 'foobar', True),
209 ('127_bad_ip', 'foobar', 'foobar', True),
210 ])
210 ])
211 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
211 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
212 self.log_user()
212 self.log_user()
213 user = user_util.create_user(username=test_name)
213 user = user_util.create_user(username=test_name)
214 user_id = user.user_id
214 user_id = user.user_id
215
215
216 response = self.app.post(
216 response = self.app.post(
217 route_path('edit_user_ips_add', user_id=user_id),
217 route_path('edit_user_ips_add', user_id=user_id),
218 params={'new_ip': ip, 'csrf_token': self.csrf_token})
218 params={'new_ip': ip, 'csrf_token': self.csrf_token})
219
219
220 if failure:
220 if failure:
221 assert_session_flash(
221 assert_session_flash(
222 response, 'Please enter a valid IPv4 or IpV6 address')
222 response, 'Please enter a valid IPv4 or IpV6 address')
223 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
223 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
224
224
225 response.mustcontain(no=[ip])
225 response.mustcontain(no=[ip])
226 response.mustcontain(no=[ip_range])
226 response.mustcontain(no=[ip_range])
227
227
228 else:
228 else:
229 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
229 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
230 response.mustcontain(ip)
230 response.mustcontain(ip)
231 response.mustcontain(ip_range)
231 response.mustcontain(ip_range)
232
232
233 def test_ips_delete(self, user_util):
233 def test_ips_delete(self, user_util):
234 self.log_user()
234 self.log_user()
235 user = user_util.create_user()
235 user = user_util.create_user()
236 user_id = user.user_id
236 user_id = user.user_id
237 ip = '127.0.0.1/32'
237 ip = '127.0.0.1/32'
238 ip_range = '127.0.0.1 - 127.0.0.1'
238 ip_range = '127.0.0.1 - 127.0.0.1'
239 new_ip = UserModel().add_extra_ip(user_id, ip)
239 new_ip = UserModel().add_extra_ip(user_id, ip)
240 Session().commit()
240 Session().commit()
241 new_ip_id = new_ip.ip_id
241 new_ip_id = new_ip.ip_id
242
242
243 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
243 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
244 response.mustcontain(ip)
244 response.mustcontain(ip)
245 response.mustcontain(ip_range)
245 response.mustcontain(ip_range)
246
246
247 self.app.post(
247 self.app.post(
248 route_path('edit_user_ips_delete', user_id=user_id),
248 route_path('edit_user_ips_delete', user_id=user_id),
249 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
249 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
250
250
251 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
251 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
252 response.mustcontain('All IP addresses are allowed')
252 response.mustcontain('All IP addresses are allowed')
253 response.mustcontain(no=[ip])
253 response.mustcontain(no=[ip])
254 response.mustcontain(no=[ip_range])
254 response.mustcontain(no=[ip_range])
255
255
256 def test_emails(self):
256 def test_emails(self):
257 self.log_user()
257 self.log_user()
258 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
258 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
259 response = self.app.get(
259 response = self.app.get(
260 route_path('edit_user_emails', user_id=user.user_id))
260 route_path('edit_user_emails', user_id=user.user_id))
261 response.mustcontain('No additional emails specified')
261 response.mustcontain('No additional emails specified')
262
262
263 def test_emails_add(self, user_util):
263 def test_emails_add(self, user_util):
264 self.log_user()
264 self.log_user()
265 user = user_util.create_user()
265 user = user_util.create_user()
266 user_id = user.user_id
266 user_id = user.user_id
267
267
268 self.app.post(
268 self.app.post(
269 route_path('edit_user_emails_add', user_id=user_id),
269 route_path('edit_user_emails_add', user_id=user_id),
270 params={'new_email': 'example@rhodecode.com',
270 params={'new_email': 'example@rhodecode.com',
271 'csrf_token': self.csrf_token})
271 'csrf_token': self.csrf_token})
272
272
273 response = self.app.get(
273 response = self.app.get(
274 route_path('edit_user_emails', user_id=user_id))
274 route_path('edit_user_emails', user_id=user_id))
275 response.mustcontain('example@rhodecode.com')
275 response.mustcontain('example@rhodecode.com')
276
276
277 def test_emails_add_existing_email(self, user_util, user_regular):
277 def test_emails_add_existing_email(self, user_util, user_regular):
278 existing_email = user_regular.email
278 existing_email = user_regular.email
279
279
280 self.log_user()
280 self.log_user()
281 user = user_util.create_user()
281 user = user_util.create_user()
282 user_id = user.user_id
282 user_id = user.user_id
283
283
284 response = self.app.post(
284 response = self.app.post(
285 route_path('edit_user_emails_add', user_id=user_id),
285 route_path('edit_user_emails_add', user_id=user_id),
286 params={'new_email': existing_email,
286 params={'new_email': existing_email,
287 'csrf_token': self.csrf_token})
287 'csrf_token': self.csrf_token})
288 assert_session_flash(
288 assert_session_flash(
289 response, 'This e-mail address is already taken')
289 response, 'This e-mail address is already taken')
290
290
291 response = self.app.get(
291 response = self.app.get(
292 route_path('edit_user_emails', user_id=user_id))
292 route_path('edit_user_emails', user_id=user_id))
293 response.mustcontain(no=[existing_email])
293 response.mustcontain(no=[existing_email])
294
294
295 def test_emails_delete(self, user_util):
295 def test_emails_delete(self, user_util):
296 self.log_user()
296 self.log_user()
297 user = user_util.create_user()
297 user = user_util.create_user()
298 user_id = user.user_id
298 user_id = user.user_id
299
299
300 self.app.post(
300 self.app.post(
301 route_path('edit_user_emails_add', user_id=user_id),
301 route_path('edit_user_emails_add', user_id=user_id),
302 params={'new_email': 'example@rhodecode.com',
302 params={'new_email': 'example@rhodecode.com',
303 'csrf_token': self.csrf_token})
303 'csrf_token': self.csrf_token})
304
304
305 response = self.app.get(
305 response = self.app.get(
306 route_path('edit_user_emails', user_id=user_id))
306 route_path('edit_user_emails', user_id=user_id))
307 response.mustcontain('example@rhodecode.com')
307 response.mustcontain('example@rhodecode.com')
308
308
309 user_email = UserEmailMap.query()\
309 user_email = UserEmailMap.query()\
310 .filter(UserEmailMap.email == 'example@rhodecode.com') \
310 .filter(UserEmailMap.email == 'example@rhodecode.com') \
311 .filter(UserEmailMap.user_id == user_id)\
311 .filter(UserEmailMap.user_id == user_id)\
312 .one()
312 .one()
313
313
314 del_email_id = user_email.email_id
314 del_email_id = user_email.email_id
315 self.app.post(
315 self.app.post(
316 route_path('edit_user_emails_delete', user_id=user_id),
316 route_path('edit_user_emails_delete', user_id=user_id),
317 params={'del_email_id': del_email_id,
317 params={'del_email_id': del_email_id,
318 'csrf_token': self.csrf_token})
318 'csrf_token': self.csrf_token})
319
319
320 response = self.app.get(
320 response = self.app.get(
321 route_path('edit_user_emails', user_id=user_id))
321 route_path('edit_user_emails', user_id=user_id))
322 response.mustcontain(no=['example@rhodecode.com'])
322 response.mustcontain(no=['example@rhodecode.com'])
323
323
324 def test_create(self, request, xhr_header):
324 def test_create(self, request, xhr_header):
325 self.log_user()
325 self.log_user()
326 username = 'newtestuser'
326 username = 'newtestuser'
327 password = 'test12'
327 password = 'test12'
328 password_confirmation = password
328 password_confirmation = password
329 name = 'name'
329 name = 'name'
330 lastname = 'lastname'
330 lastname = 'lastname'
331 email = 'mail@mail.com'
331 email = 'mail@mail.com'
332
332
333 self.app.get(route_path('users_new'))
333 self.app.get(route_path('users_new'))
334
334
335 response = self.app.post(route_path('users_create'), params={
335 response = self.app.post(route_path('users_create'), params={
336 'username': username,
336 'username': username,
337 'password': password,
337 'password': password,
338 'description': 'mr CTO',
338 'description': 'mr CTO',
339 'password_confirmation': password_confirmation,
339 'password_confirmation': password_confirmation,
340 'firstname': name,
340 'firstname': name,
341 'active': True,
341 'active': True,
342 'lastname': lastname,
342 'lastname': lastname,
343 'extern_name': 'rhodecode',
343 'extern_name': 'rhodecode',
344 'extern_type': 'rhodecode',
344 'extern_type': 'rhodecode',
345 'email': email,
345 'email': email,
346 'csrf_token': self.csrf_token,
346 'csrf_token': self.csrf_token,
347 })
347 })
348 user_link = h.link_to(
348 user_link = h.link_to(
349 username,
349 username,
350 route_path(
350 route_path(
351 'user_edit', user_id=User.get_by_username(username).user_id))
351 'user_edit', user_id=User.get_by_username(username).user_id))
352 assert_session_flash(response, 'Created user %s' % (user_link,))
352 assert_session_flash(response, 'Created user %s' % (user_link,))
353
353
354 @request.addfinalizer
354 @request.addfinalizer
355 def cleanup():
355 def cleanup():
356 fixture.destroy_user(username)
356 fixture.destroy_user(username)
357 Session().commit()
357 Session().commit()
358
358
359 new_user = User.query().filter(User.username == username).one()
359 new_user = User.query().filter(User.username == username).one()
360
360
361 assert new_user.username == username
361 assert new_user.username == username
362 assert auth.check_password(password, new_user.password)
362 assert auth.check_password(password, new_user.password)
363 assert new_user.name == name
363 assert new_user.name == name
364 assert new_user.lastname == lastname
364 assert new_user.lastname == lastname
365 assert new_user.email == email
365 assert new_user.email == email
366
366
367 response = self.app.get(route_path('users_data'),
367 response = self.app.get(route_path('users_data'),
368 extra_environ=xhr_header)
368 extra_environ=xhr_header)
369 response.mustcontain(username)
369 response.mustcontain(username)
370
370
371 def test_create_err(self):
371 def test_create_err(self):
372 self.log_user()
372 self.log_user()
373 username = 'new_user'
373 username = 'new_user'
374 password = ''
374 password = ''
375 name = 'name'
375 name = 'name'
376 lastname = 'lastname'
376 lastname = 'lastname'
377 email = 'errmail.com'
377 email = 'errmail.com'
378
378
379 self.app.get(route_path('users_new'))
379 self.app.get(route_path('users_new'))
380
380
381 response = self.app.post(route_path('users_create'), params={
381 response = self.app.post(route_path('users_create'), params={
382 'username': username,
382 'username': username,
383 'password': password,
383 'password': password,
384 'name': name,
384 'name': name,
385 'active': False,
385 'active': False,
386 'lastname': lastname,
386 'lastname': lastname,
387 'description': 'mr CTO',
387 'description': 'mr CTO',
388 'email': email,
388 'email': email,
389 'csrf_token': self.csrf_token,
389 'csrf_token': self.csrf_token,
390 })
390 })
391
391
392 msg = u'Username "%(username)s" is forbidden'
392 msg = u'Username "%(username)s" is forbidden'
393 msg = h.html_escape(msg % {'username': 'new_user'})
393 msg = h.html_escape(msg % {'username': 'new_user'})
394 response.mustcontain('<span class="error-message">%s</span>' % msg)
394 response.mustcontain('<span class="error-message">%s</span>' % msg)
395 response.mustcontain(
395 response.mustcontain(
396 '<span class="error-message">Please enter a value</span>')
396 '<span class="error-message">Please enter a value</span>')
397 response.mustcontain(
397 response.mustcontain(
398 '<span class="error-message">An email address must contain a'
398 '<span class="error-message">An email address must contain a'
399 ' single @</span>')
399 ' single @</span>')
400
400
401 def get_user():
401 def get_user():
402 Session().query(User).filter(User.username == username).one()
402 Session().query(User).filter(User.username == username).one()
403
403
404 with pytest.raises(NoResultFound):
404 with pytest.raises(NoResultFound):
405 get_user()
405 get_user()
406
406
407 def test_new(self):
407 def test_new(self):
408 self.log_user()
408 self.log_user()
409 self.app.get(route_path('users_new'))
409 self.app.get(route_path('users_new'))
410
410
411 @pytest.mark.parametrize("name, attrs", [
411 @pytest.mark.parametrize("name, attrs", [
412 ('firstname', {'firstname': 'new_username'}),
412 ('firstname', {'firstname': 'new_username'}),
413 ('lastname', {'lastname': 'new_username'}),
413 ('lastname', {'lastname': 'new_username'}),
414 ('admin', {'admin': True}),
414 ('admin', {'admin': True}),
415 ('admin', {'admin': False}),
415 ('admin', {'admin': False}),
416 ('extern_type', {'extern_type': 'ldap'}),
416 ('extern_type', {'extern_type': 'ldap'}),
417 ('extern_type', {'extern_type': None}),
417 ('extern_type', {'extern_type': None}),
418 ('extern_name', {'extern_name': 'test'}),
418 ('extern_name', {'extern_name': 'test'}),
419 ('extern_name', {'extern_name': None}),
419 ('extern_name', {'extern_name': None}),
420 ('active', {'active': False}),
420 ('active', {'active': False}),
421 ('active', {'active': True}),
421 ('active', {'active': True}),
422 ('email', {'email': 'some@email.com'}),
422 ('email', {'email': 'some@email.com'}),
423 ('language', {'language': 'de'}),
423 ('language', {'language': 'de'}),
424 ('language', {'language': 'en'}),
424 ('language', {'language': 'en'}),
425 ('description', {'description': 'hello CTO'}),
425 ('description', {'description': 'hello CTO'}),
426 # ('new_password', {'new_password': 'foobar123',
426 # ('new_password', {'new_password': 'foobar123',
427 # 'password_confirmation': 'foobar123'})
427 # 'password_confirmation': 'foobar123'})
428 ])
428 ])
429 def test_update(self, name, attrs, user_util):
429 def test_update(self, name, attrs, user_util):
430 self.log_user()
430 self.log_user()
431 usr = user_util.create_user(
431 usr = user_util.create_user(
432 password='qweqwe',
432 password='qweqwe',
433 email='testme@rhodecode.org',
433 email='testme@rhodecode.org',
434 extern_type='rhodecode',
434 extern_type='rhodecode',
435 extern_name='xxx',
435 extern_name='xxx',
436 )
436 )
437 user_id = usr.user_id
437 user_id = usr.user_id
438 Session().commit()
438 Session().commit()
439
439
440 params = usr.get_api_data()
440 params = usr.get_api_data()
441 cur_lang = params['language'] or 'en'
441 cur_lang = params['language'] or 'en'
442 params.update({
442 params.update({
443 'password_confirmation': '',
443 'password_confirmation': '',
444 'new_password': '',
444 'new_password': '',
445 'language': cur_lang,
445 'language': cur_lang,
446 'csrf_token': self.csrf_token,
446 'csrf_token': self.csrf_token,
447 })
447 })
448 params.update({'new_password': ''})
448 params.update({'new_password': ''})
449 params.update(attrs)
449 params.update(attrs)
450 if name == 'email':
450 if name == 'email':
451 params['emails'] = [attrs['email']]
451 params['emails'] = [attrs['email']]
452 elif name == 'extern_type':
452 elif name == 'extern_type':
453 # cannot update this via form, expected value is original one
453 # cannot update this via form, expected value is original one
454 params['extern_type'] = "rhodecode"
454 params['extern_type'] = "rhodecode"
455 elif name == 'extern_name':
455 elif name == 'extern_name':
456 # cannot update this via form, expected value is original one
456 # cannot update this via form, expected value is original one
457 params['extern_name'] = 'xxx'
457 params['extern_name'] = 'xxx'
458 # special case since this user is not
458 # special case since this user is not
459 # logged in yet his data is not filled
459 # logged in yet his data is not filled
460 # so we use creation data
460 # so we use creation data
461
461
462 response = self.app.post(
462 response = self.app.post(
463 route_path('user_update', user_id=usr.user_id), params)
463 route_path('user_update', user_id=usr.user_id), params)
464 assert response.status_int == 302
464 assert response.status_int == 302
465 assert_session_flash(response, 'User updated successfully')
465 assert_session_flash(response, 'User updated successfully')
466
466
467 updated_user = User.get(user_id)
467 updated_user = User.get(user_id)
468 updated_params = updated_user.get_api_data()
468 updated_params = updated_user.get_api_data()
469 updated_params.update({'password_confirmation': ''})
469 updated_params.update({'password_confirmation': ''})
470 updated_params.update({'new_password': ''})
470 updated_params.update({'new_password': ''})
471
471
472 del params['csrf_token']
472 del params['csrf_token']
473 assert params == updated_params
473 assert params == updated_params
474
474
475 def test_update_and_migrate_password(
475 def test_update_and_migrate_password(
476 self, autologin_user, real_crypto_backend, user_util):
476 self, autologin_user, real_crypto_backend, user_util):
477
477
478 user = user_util.create_user()
478 user = user_util.create_user()
479 temp_user = user.username
479 temp_user = user.username
480 user.password = auth._RhodeCodeCryptoSha256().hash_create(
480 user.password = auth._RhodeCodeCryptoSha256().hash_create(
481 b'test123')
481 b'test123')
482 Session().add(user)
482 Session().add(user)
483 Session().commit()
483 Session().commit()
484
484
485 params = user.get_api_data()
485 params = user.get_api_data()
486
486
487 params.update({
487 params.update({
488 'password_confirmation': 'qweqwe123',
488 'password_confirmation': 'qweqwe123',
489 'new_password': 'qweqwe123',
489 'new_password': 'qweqwe123',
490 'language': 'en',
490 'language': 'en',
491 'csrf_token': autologin_user.csrf_token,
491 'csrf_token': autologin_user.csrf_token,
492 })
492 })
493
493
494 response = self.app.post(
494 response = self.app.post(
495 route_path('user_update', user_id=user.user_id), params)
495 route_path('user_update', user_id=user.user_id), params)
496 assert response.status_int == 302
496 assert response.status_int == 302
497 assert_session_flash(response, 'User updated successfully')
497 assert_session_flash(response, 'User updated successfully')
498
498
499 # new password should be bcrypted, after log-in and transfer
499 # new password should be bcrypted, after log-in and transfer
500 user = User.get_by_username(temp_user)
500 user = User.get_by_username(temp_user)
501 assert user.password.startswith('$')
501 assert user.password.startswith('$')
502
502
503 updated_user = User.get_by_username(temp_user)
503 updated_user = User.get_by_username(temp_user)
504 updated_params = updated_user.get_api_data()
504 updated_params = updated_user.get_api_data()
505 updated_params.update({'password_confirmation': 'qweqwe123'})
505 updated_params.update({'password_confirmation': 'qweqwe123'})
506 updated_params.update({'new_password': 'qweqwe123'})
506 updated_params.update({'new_password': 'qweqwe123'})
507
507
508 del params['csrf_token']
508 del params['csrf_token']
509 assert params == updated_params
509 assert params == updated_params
510
510
511 def test_delete(self):
511 def test_delete(self):
512 self.log_user()
512 self.log_user()
513 username = 'newtestuserdeleteme'
513 username = 'newtestuserdeleteme'
514
514
515 fixture.create_user(name=username)
515 fixture.create_user(name=username)
516
516
517 new_user = Session().query(User)\
517 new_user = Session().query(User)\
518 .filter(User.username == username).one()
518 .filter(User.username == username).one()
519 response = self.app.post(
519 response = self.app.post(
520 route_path('user_delete', user_id=new_user.user_id),
520 route_path('user_delete', user_id=new_user.user_id),
521 params={'csrf_token': self.csrf_token})
521 params={'csrf_token': self.csrf_token})
522
522
523 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
523 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
524
524
525 def test_delete_owner_of_repository(self, request, user_util):
525 def test_delete_owner_of_repository(self, request, user_util):
526 self.log_user()
526 self.log_user()
527 obj_name = 'test_repo'
527 obj_name = 'test_repo'
528 usr = user_util.create_user()
528 usr = user_util.create_user()
529 username = usr.username
529 username = usr.username
530 fixture.create_repo(obj_name, cur_user=usr.username)
530 fixture.create_repo(obj_name, cur_user=usr.username)
531
531
532 new_user = Session().query(User)\
532 new_user = Session().query(User)\
533 .filter(User.username == username).one()
533 .filter(User.username == username).one()
534 response = self.app.post(
534 response = self.app.post(
535 route_path('user_delete', user_id=new_user.user_id),
535 route_path('user_delete', user_id=new_user.user_id),
536 params={'csrf_token': self.csrf_token})
536 params={'csrf_token': self.csrf_token})
537
537
538 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
538 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
539 'Switch owners or remove those repositories:%s' % (username, obj_name)
539 'Switch owners or remove those repositories:%s' % (username, obj_name)
540 assert_session_flash(response, msg)
540 assert_session_flash(response, msg)
541 fixture.destroy_repo(obj_name)
541 fixture.destroy_repo(obj_name)
542
542
543 def test_delete_owner_of_repository_detaching(self, request, user_util):
543 def test_delete_owner_of_repository_detaching(self, request, user_util):
544 self.log_user()
544 self.log_user()
545 obj_name = 'test_repo'
545 obj_name = 'test_repo'
546 usr = user_util.create_user(auto_cleanup=False)
546 usr = user_util.create_user(auto_cleanup=False)
547 username = usr.username
547 username = usr.username
548 fixture.create_repo(obj_name, cur_user=usr.username)
548 fixture.create_repo(obj_name, cur_user=usr.username)
549 Session().commit()
549 Session().commit()
550
550
551 new_user = Session().query(User)\
551 new_user = Session().query(User)\
552 .filter(User.username == username).one()
552 .filter(User.username == username).one()
553 response = self.app.post(
553 response = self.app.post(
554 route_path('user_delete', user_id=new_user.user_id),
554 route_path('user_delete', user_id=new_user.user_id),
555 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
555 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
556
556
557 msg = 'Detached 1 repositories'
557 msg = 'Detached 1 repositories'
558 assert_session_flash(response, msg)
558 assert_session_flash(response, msg)
559 fixture.destroy_repo(obj_name)
559 fixture.destroy_repo(obj_name)
560
560
561 def test_delete_owner_of_repository_deleting(self, request, user_util):
561 def test_delete_owner_of_repository_deleting(self, request, user_util):
562 self.log_user()
562 self.log_user()
563 obj_name = 'test_repo'
563 obj_name = 'test_repo'
564 usr = user_util.create_user(auto_cleanup=False)
564 usr = user_util.create_user(auto_cleanup=False)
565 username = usr.username
565 username = usr.username
566 fixture.create_repo(obj_name, cur_user=usr.username)
566 fixture.create_repo(obj_name, cur_user=usr.username)
567
567
568 new_user = Session().query(User)\
568 new_user = Session().query(User)\
569 .filter(User.username == username).one()
569 .filter(User.username == username).one()
570 response = self.app.post(
570 response = self.app.post(
571 route_path('user_delete', user_id=new_user.user_id),
571 route_path('user_delete', user_id=new_user.user_id),
572 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
572 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
573
573
574 msg = 'Deleted 1 repositories'
574 msg = 'Deleted 1 repositories'
575 assert_session_flash(response, msg)
575 assert_session_flash(response, msg)
576
576
577 def test_delete_owner_of_repository_group(self, request, user_util):
577 def test_delete_owner_of_repository_group(self, request, user_util):
578 self.log_user()
578 self.log_user()
579 obj_name = 'test_group'
579 obj_name = 'test_group'
580 usr = user_util.create_user()
580 usr = user_util.create_user()
581 username = usr.username
581 username = usr.username
582 fixture.create_repo_group(obj_name, cur_user=usr.username)
582 fixture.create_repo_group(obj_name, cur_user=usr.username)
583
583
584 new_user = Session().query(User)\
584 new_user = Session().query(User)\
585 .filter(User.username == username).one()
585 .filter(User.username == username).one()
586 response = self.app.post(
586 response = self.app.post(
587 route_path('user_delete', user_id=new_user.user_id),
587 route_path('user_delete', user_id=new_user.user_id),
588 params={'csrf_token': self.csrf_token})
588 params={'csrf_token': self.csrf_token})
589
589
590 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
590 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
591 'Switch owners or remove those repository groups:%s' % (username, obj_name)
591 'Switch owners or remove those repository groups:%s' % (username, obj_name)
592 assert_session_flash(response, msg)
592 assert_session_flash(response, msg)
593 fixture.destroy_repo_group(obj_name)
593 fixture.destroy_repo_group(obj_name)
594
594
595 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
595 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
596 self.log_user()
596 self.log_user()
597 obj_name = 'test_group'
597 obj_name = 'test_group'
598 usr = user_util.create_user(auto_cleanup=False)
598 usr = user_util.create_user(auto_cleanup=False)
599 username = usr.username
599 username = usr.username
600 fixture.create_repo_group(obj_name, cur_user=usr.username)
600 fixture.create_repo_group(obj_name, cur_user=usr.username)
601
601
602 new_user = Session().query(User)\
602 new_user = Session().query(User)\
603 .filter(User.username == username).one()
603 .filter(User.username == username).one()
604 response = self.app.post(
604 response = self.app.post(
605 route_path('user_delete', user_id=new_user.user_id),
605 route_path('user_delete', user_id=new_user.user_id),
606 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
606 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
607
607
608 msg = 'Deleted 1 repository groups'
608 msg = 'Deleted 1 repository groups'
609 assert_session_flash(response, msg)
609 assert_session_flash(response, msg)
610
610
611 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
611 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
612 self.log_user()
612 self.log_user()
613 obj_name = 'test_group'
613 obj_name = 'test_group'
614 usr = user_util.create_user(auto_cleanup=False)
614 usr = user_util.create_user(auto_cleanup=False)
615 username = usr.username
615 username = usr.username
616 fixture.create_repo_group(obj_name, cur_user=usr.username)
616 fixture.create_repo_group(obj_name, cur_user=usr.username)
617
617
618 new_user = Session().query(User)\
618 new_user = Session().query(User)\
619 .filter(User.username == username).one()
619 .filter(User.username == username).one()
620 response = self.app.post(
620 response = self.app.post(
621 route_path('user_delete', user_id=new_user.user_id),
621 route_path('user_delete', user_id=new_user.user_id),
622 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
622 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
623
623
624 msg = 'Detached 1 repository groups'
624 msg = 'Detached 1 repository groups'
625 assert_session_flash(response, msg)
625 assert_session_flash(response, msg)
626 fixture.destroy_repo_group(obj_name)
626 fixture.destroy_repo_group(obj_name)
627
627
628 def test_delete_owner_of_user_group(self, request, user_util):
628 def test_delete_owner_of_user_group(self, request, user_util):
629 self.log_user()
629 self.log_user()
630 obj_name = 'test_user_group'
630 obj_name = 'test_user_group'
631 usr = user_util.create_user()
631 usr = user_util.create_user()
632 username = usr.username
632 username = usr.username
633 fixture.create_user_group(obj_name, cur_user=usr.username)
633 fixture.create_user_group(obj_name, cur_user=usr.username)
634
634
635 new_user = Session().query(User)\
635 new_user = Session().query(User)\
636 .filter(User.username == username).one()
636 .filter(User.username == username).one()
637 response = self.app.post(
637 response = self.app.post(
638 route_path('user_delete', user_id=new_user.user_id),
638 route_path('user_delete', user_id=new_user.user_id),
639 params={'csrf_token': self.csrf_token})
639 params={'csrf_token': self.csrf_token})
640
640
641 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
641 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
642 'Switch owners or remove those user groups:%s' % (username, obj_name)
642 'Switch owners or remove those user groups:%s' % (username, obj_name)
643 assert_session_flash(response, msg)
643 assert_session_flash(response, msg)
644 fixture.destroy_user_group(obj_name)
644 fixture.destroy_user_group(obj_name)
645
645
646 def test_delete_owner_of_user_group_detaching(self, request, user_util):
646 def test_delete_owner_of_user_group_detaching(self, request, user_util):
647 self.log_user()
647 self.log_user()
648 obj_name = 'test_user_group'
648 obj_name = 'test_user_group'
649 usr = user_util.create_user(auto_cleanup=False)
649 usr = user_util.create_user(auto_cleanup=False)
650 username = usr.username
650 username = usr.username
651 fixture.create_user_group(obj_name, cur_user=usr.username)
651 fixture.create_user_group(obj_name, cur_user=usr.username)
652
652
653 new_user = Session().query(User)\
653 new_user = Session().query(User)\
654 .filter(User.username == username).one()
654 .filter(User.username == username).one()
655 try:
655 try:
656 response = self.app.post(
656 response = self.app.post(
657 route_path('user_delete', user_id=new_user.user_id),
657 route_path('user_delete', user_id=new_user.user_id),
658 params={'user_user_groups': 'detach',
658 params={'user_user_groups': 'detach',
659 'csrf_token': self.csrf_token})
659 'csrf_token': self.csrf_token})
660
660
661 msg = 'Detached 1 user groups'
661 msg = 'Detached 1 user groups'
662 assert_session_flash(response, msg)
662 assert_session_flash(response, msg)
663 finally:
663 finally:
664 fixture.destroy_user_group(obj_name)
664 fixture.destroy_user_group(obj_name)
665
665
666 def test_delete_owner_of_user_group_deleting(self, request, user_util):
666 def test_delete_owner_of_user_group_deleting(self, request, user_util):
667 self.log_user()
667 self.log_user()
668 obj_name = 'test_user_group'
668 obj_name = 'test_user_group'
669 usr = user_util.create_user(auto_cleanup=False)
669 usr = user_util.create_user(auto_cleanup=False)
670 username = usr.username
670 username = usr.username
671 fixture.create_user_group(obj_name, cur_user=usr.username)
671 fixture.create_user_group(obj_name, cur_user=usr.username)
672
672
673 new_user = Session().query(User)\
673 new_user = Session().query(User)\
674 .filter(User.username == username).one()
674 .filter(User.username == username).one()
675 response = self.app.post(
675 response = self.app.post(
676 route_path('user_delete', user_id=new_user.user_id),
676 route_path('user_delete', user_id=new_user.user_id),
677 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
677 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
678
678
679 msg = 'Deleted 1 user groups'
679 msg = 'Deleted 1 user groups'
680 assert_session_flash(response, msg)
680 assert_session_flash(response, msg)
681
681
682 def test_edit(self, user_util):
682 def test_edit(self, user_util):
683 self.log_user()
683 self.log_user()
684 user = user_util.create_user()
684 user = user_util.create_user()
685 self.app.get(route_path('user_edit', user_id=user.user_id))
685 self.app.get(route_path('user_edit', user_id=user.user_id))
686
686
687 def test_edit_default_user_redirect(self):
687 def test_edit_default_user_redirect(self):
688 self.log_user()
688 self.log_user()
689 user = User.get_default_user()
689 user = User.get_default_user()
690 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
690 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
691
691
692 @pytest.mark.parametrize(
692 @pytest.mark.parametrize(
693 'repo_create, repo_create_write, user_group_create, repo_group_create,'
693 'repo_create, repo_create_write, user_group_create, repo_group_create,'
694 'fork_create, inherit_default_permissions, expect_error,'
694 'fork_create, inherit_default_permissions, expect_error,'
695 'expect_form_error', [
695 'expect_form_error', [
696 ('hg.create.none', 'hg.create.write_on_repogroup.false',
696 ('hg.create.none', 'hg.create.write_on_repogroup.false',
697 'hg.usergroup.create.false', 'hg.repogroup.create.false',
697 'hg.usergroup.create.false', 'hg.repogroup.create.false',
698 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
698 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
699 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
699 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
700 'hg.usergroup.create.false', 'hg.repogroup.create.false',
700 'hg.usergroup.create.false', 'hg.repogroup.create.false',
701 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
701 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
702 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
702 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
703 'hg.usergroup.create.true', 'hg.repogroup.create.true',
703 'hg.usergroup.create.true', 'hg.repogroup.create.true',
704 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
704 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
705 False),
705 False),
706 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
706 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
707 'hg.usergroup.create.true', 'hg.repogroup.create.true',
707 'hg.usergroup.create.true', 'hg.repogroup.create.true',
708 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
708 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
709 True),
709 True),
710 ('', '', '', '', '', '', True, False),
710 ('', '', '', '', '', '', True, False),
711 ])
711 ])
712 def test_global_perms_on_user(
712 def test_global_perms_on_user(
713 self, repo_create, repo_create_write, user_group_create,
713 self, repo_create, repo_create_write, user_group_create,
714 repo_group_create, fork_create, expect_error, expect_form_error,
714 repo_group_create, fork_create, expect_error, expect_form_error,
715 inherit_default_permissions, user_util):
715 inherit_default_permissions, user_util):
716 self.log_user()
716 self.log_user()
717 user = user_util.create_user()
717 user = user_util.create_user()
718 uid = user.user_id
718 uid = user.user_id
719
719
720 # ENABLE REPO CREATE ON A GROUP
720 # ENABLE REPO CREATE ON A GROUP
721 perm_params = {
721 perm_params = {
722 'inherit_default_permissions': False,
722 'inherit_default_permissions': False,
723 'default_repo_create': repo_create,
723 'default_repo_create': repo_create,
724 'default_repo_create_on_write': repo_create_write,
724 'default_repo_create_on_write': repo_create_write,
725 'default_user_group_create': user_group_create,
725 'default_user_group_create': user_group_create,
726 'default_repo_group_create': repo_group_create,
726 'default_repo_group_create': repo_group_create,
727 'default_fork_create': fork_create,
727 'default_fork_create': fork_create,
728 'default_inherit_default_permissions': inherit_default_permissions,
728 'default_inherit_default_permissions': inherit_default_permissions,
729 'csrf_token': self.csrf_token,
729 'csrf_token': self.csrf_token,
730 }
730 }
731 response = self.app.post(
731 response = self.app.post(
732 route_path('user_edit_global_perms_update', user_id=uid),
732 route_path('user_edit_global_perms_update', user_id=uid),
733 params=perm_params)
733 params=perm_params)
734
734
735 if expect_form_error:
735 if expect_form_error:
736 assert response.status_int == 200
736 assert response.status_int == 200
737 response.mustcontain('Value must be one of')
737 response.mustcontain('Value must be one of')
738 else:
738 else:
739 if expect_error:
739 if expect_error:
740 msg = 'An error occurred during permissions saving'
740 msg = 'An error occurred during permissions saving'
741 else:
741 else:
742 msg = 'User global permissions updated successfully'
742 msg = 'User global permissions updated successfully'
743 ug = User.get(uid)
743 ug = User.get(uid)
744 del perm_params['inherit_default_permissions']
744 del perm_params['inherit_default_permissions']
745 del perm_params['csrf_token']
745 del perm_params['csrf_token']
746 assert perm_params == ug.get_default_perms()
746 assert perm_params == ug.get_default_perms()
747 assert_session_flash(response, msg)
747 assert_session_flash(response, msg)
748
748
749 def test_global_permissions_initial_values(self, user_util):
749 def test_global_permissions_initial_values(self, user_util):
750 self.log_user()
750 self.log_user()
751 user = user_util.create_user()
751 user = user_util.create_user()
752 uid = user.user_id
752 uid = user.user_id
753 response = self.app.get(
753 response = self.app.get(
754 route_path('user_edit_global_perms', user_id=uid))
754 route_path('user_edit_global_perms', user_id=uid))
755 default_user = User.get_default_user()
755 default_user = User.get_default_user()
756 default_permissions = default_user.get_default_perms()
756 default_permissions = default_user.get_default_perms()
757 assert_response = response.assert_response()
757 assert_response = response.assert_response()
758 expected_permissions = (
758 expected_permissions = (
759 'default_repo_create', 'default_repo_create_on_write',
759 'default_repo_create', 'default_repo_create_on_write',
760 'default_fork_create', 'default_repo_group_create',
760 'default_fork_create', 'default_repo_group_create',
761 'default_user_group_create', 'default_inherit_default_permissions')
761 'default_user_group_create', 'default_inherit_default_permissions')
762 for permission in expected_permissions:
762 for permission in expected_permissions:
763 css_selector = '[name={}][checked=checked]'.format(permission)
763 css_selector = '[name={}][checked=checked]'.format(permission)
764 element = assert_response.get_element(css_selector)
764 element = assert_response.get_element(css_selector)
765 assert element.value == default_permissions[permission]
765 assert element.value == default_permissions[permission]
766
766
767 def test_perms_summary_page(self):
767 def test_perms_summary_page(self):
768 user = self.log_user()
768 user = self.log_user()
769 response = self.app.get(
769 response = self.app.get(
770 route_path('edit_user_perms_summary', user_id=user['user_id']))
770 route_path('edit_user_perms_summary', user_id=user['user_id']))
771 for repo in Repository.query().all():
771 for repo in Repository.query().all():
772 response.mustcontain(repo.repo_name)
772 response.mustcontain(repo.repo_name)
773
773
774 def test_perms_summary_page_json(self):
774 def test_perms_summary_page_json(self):
775 user = self.log_user()
775 user = self.log_user()
776 response = self.app.get(
776 response = self.app.get(
777 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
777 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
778 for repo in Repository.query().all():
778 for repo in Repository.query().all():
779 response.mustcontain(repo.repo_name)
779 response.mustcontain(repo.repo_name)
780
780
781 def test_audit_log_page(self):
781 def test_audit_log_page(self):
782 user = self.log_user()
782 user = self.log_user()
783 self.app.get(
783 self.app.get(
784 route_path('edit_user_audit_logs', user_id=user['user_id']))
784 route_path('edit_user_audit_logs', user_id=user['user_id']))
785
785
786 def test_audit_log_page_download(self):
786 def test_audit_log_page_download(self):
787 user = self.log_user()
787 user = self.log_user()
788 user_id = user['user_id']
788 user_id = user['user_id']
789 response = self.app.get(
789 response = self.app.get(
790 route_path('edit_user_audit_logs_download', user_id=user_id))
790 route_path('edit_user_audit_logs_download', user_id=user_id))
791
791
792 assert response.content_disposition == \
792 assert response.content_disposition == \
793 'attachment; filename=user_{}_audit_logs.json'.format(user_id)
793 'attachment; filename=user_{}_audit_logs.json'.format(user_id)
794 assert response.content_type == "application/json"
794 assert response.content_type == "application/json"
@@ -1,176 +1,176 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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.model.db import User, UserSshKeys
23 from rhodecode.model.db import User, UserSshKeys
24
24
25 from rhodecode.tests import TestController, assert_session_flash
25 from rhodecode.tests import TestController, assert_session_flash
26 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.fixture import Fixture
27
27
28 fixture = Fixture()
28 fixture = Fixture()
29
29
30
30
31 def route_path(name, params=None, **kwargs):
31 def route_path(name, params=None, **kwargs):
32 import urllib
32 import urllib.request, urllib.parse, urllib.error
33 from rhodecode.apps._base import ADMIN_PREFIX
33 from rhodecode.apps._base import ADMIN_PREFIX
34
34
35 base_url = {
35 base_url = {
36 'edit_user_ssh_keys':
36 'edit_user_ssh_keys':
37 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys',
37 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys',
38 'edit_user_ssh_keys_generate_keypair':
38 'edit_user_ssh_keys_generate_keypair':
39 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate',
39 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate',
40 'edit_user_ssh_keys_add':
40 'edit_user_ssh_keys_add':
41 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new',
41 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new',
42 'edit_user_ssh_keys_delete':
42 'edit_user_ssh_keys_delete':
43 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete',
43 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete',
44
44
45 }[name].format(**kwargs)
45 }[name].format(**kwargs)
46
46
47 if params:
47 if params:
48 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 return base_url
49 return base_url
50
50
51
51
52 class TestAdminUsersSshKeysView(TestController):
52 class TestAdminUsersSshKeysView(TestController):
53 INVALID_KEY = """\
53 INVALID_KEY = """\
54 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
54 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
55 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
55 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
56 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
56 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
57 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
57 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
58 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
58 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
59 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
59 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
60 your_email@example.com
60 your_email@example.com
61 """
61 """
62 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
62 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
63 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
63 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
64 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
64 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
65 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
65 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
66 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
66 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
67 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
67 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
68 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
68 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
69 'your_email@example.com'
69 'your_email@example.com'
70 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
70 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
71
71
72 def test_ssh_keys_default_user(self):
72 def test_ssh_keys_default_user(self):
73 self.log_user()
73 self.log_user()
74 user = User.get_default_user()
74 user = User.get_default_user()
75 self.app.get(
75 self.app.get(
76 route_path('edit_user_ssh_keys', user_id=user.user_id),
76 route_path('edit_user_ssh_keys', user_id=user.user_id),
77 status=302)
77 status=302)
78
78
79 def test_add_ssh_key_error(self, user_util):
79 def test_add_ssh_key_error(self, user_util):
80 self.log_user()
80 self.log_user()
81 user = user_util.create_user()
81 user = user_util.create_user()
82 user_id = user.user_id
82 user_id = user.user_id
83
83
84 key_data = self.INVALID_KEY
84 key_data = self.INVALID_KEY
85
85
86 desc = 'MY SSH KEY'
86 desc = 'MY SSH KEY'
87 response = self.app.post(
87 response = self.app.post(
88 route_path('edit_user_ssh_keys_add', user_id=user_id),
88 route_path('edit_user_ssh_keys_add', user_id=user_id),
89 {'description': desc, 'key_data': key_data,
89 {'description': desc, 'key_data': key_data,
90 'csrf_token': self.csrf_token})
90 'csrf_token': self.csrf_token})
91 assert_session_flash(response, 'An error occurred during ssh '
91 assert_session_flash(response, 'An error occurred during ssh '
92 'key saving: Unable to decode the key')
92 'key saving: Unable to decode the key')
93
93
94 def test_ssh_key_duplicate(self, user_util):
94 def test_ssh_key_duplicate(self, user_util):
95 self.log_user()
95 self.log_user()
96 user = user_util.create_user()
96 user = user_util.create_user()
97 user_id = user.user_id
97 user_id = user.user_id
98
98
99 key_data = self.VALID_KEY
99 key_data = self.VALID_KEY
100
100
101 desc = 'MY SSH KEY'
101 desc = 'MY SSH KEY'
102 response = self.app.post(
102 response = self.app.post(
103 route_path('edit_user_ssh_keys_add', user_id=user_id),
103 route_path('edit_user_ssh_keys_add', user_id=user_id),
104 {'description': desc, 'key_data': key_data,
104 {'description': desc, 'key_data': key_data,
105 'csrf_token': self.csrf_token})
105 'csrf_token': self.csrf_token})
106 assert_session_flash(response, 'Ssh Key successfully created')
106 assert_session_flash(response, 'Ssh Key successfully created')
107 response.follow() # flush session flash
107 response.follow() # flush session flash
108
108
109 # add the same key AGAIN
109 # add the same key AGAIN
110 desc = 'MY SSH KEY'
110 desc = 'MY SSH KEY'
111 response = self.app.post(
111 response = self.app.post(
112 route_path('edit_user_ssh_keys_add', user_id=user_id),
112 route_path('edit_user_ssh_keys_add', user_id=user_id),
113 {'description': desc, 'key_data': key_data,
113 {'description': desc, 'key_data': key_data,
114 'csrf_token': self.csrf_token})
114 'csrf_token': self.csrf_token})
115
115
116 err = 'Such key with fingerprint `{}` already exists, ' \
116 err = 'Such key with fingerprint `{}` already exists, ' \
117 'please use a different one'.format(self.FINGERPRINT)
117 'please use a different one'.format(self.FINGERPRINT)
118 assert_session_flash(response, 'An error occurred during ssh key '
118 assert_session_flash(response, 'An error occurred during ssh key '
119 'saving: {}'.format(err))
119 'saving: {}'.format(err))
120
120
121 def test_add_ssh_key(self, user_util):
121 def test_add_ssh_key(self, user_util):
122 self.log_user()
122 self.log_user()
123 user = user_util.create_user()
123 user = user_util.create_user()
124 user_id = user.user_id
124 user_id = user.user_id
125
125
126 key_data = self.VALID_KEY
126 key_data = self.VALID_KEY
127
127
128 desc = 'MY SSH KEY'
128 desc = 'MY SSH KEY'
129 response = self.app.post(
129 response = self.app.post(
130 route_path('edit_user_ssh_keys_add', user_id=user_id),
130 route_path('edit_user_ssh_keys_add', user_id=user_id),
131 {'description': desc, 'key_data': key_data,
131 {'description': desc, 'key_data': key_data,
132 'csrf_token': self.csrf_token})
132 'csrf_token': self.csrf_token})
133 assert_session_flash(response, 'Ssh Key successfully created')
133 assert_session_flash(response, 'Ssh Key successfully created')
134
134
135 response = response.follow()
135 response = response.follow()
136 response.mustcontain(desc)
136 response.mustcontain(desc)
137
137
138 def test_delete_ssh_key(self, user_util):
138 def test_delete_ssh_key(self, user_util):
139 self.log_user()
139 self.log_user()
140 user = user_util.create_user()
140 user = user_util.create_user()
141 user_id = user.user_id
141 user_id = user.user_id
142
142
143 key_data = self.VALID_KEY
143 key_data = self.VALID_KEY
144
144
145 desc = 'MY SSH KEY'
145 desc = 'MY SSH KEY'
146 response = self.app.post(
146 response = self.app.post(
147 route_path('edit_user_ssh_keys_add', user_id=user_id),
147 route_path('edit_user_ssh_keys_add', user_id=user_id),
148 {'description': desc, 'key_data': key_data,
148 {'description': desc, 'key_data': key_data,
149 'csrf_token': self.csrf_token})
149 'csrf_token': self.csrf_token})
150 assert_session_flash(response, 'Ssh Key successfully created')
150 assert_session_flash(response, 'Ssh Key successfully created')
151 response = response.follow() # flush the Session flash
151 response = response.follow() # flush the Session flash
152
152
153 # now delete our key
153 # now delete our key
154 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
154 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
155 assert 1 == len(keys)
155 assert 1 == len(keys)
156
156
157 response = self.app.post(
157 response = self.app.post(
158 route_path('edit_user_ssh_keys_delete', user_id=user_id),
158 route_path('edit_user_ssh_keys_delete', user_id=user_id),
159 {'del_ssh_key': keys[0].ssh_key_id,
159 {'del_ssh_key': keys[0].ssh_key_id,
160 'csrf_token': self.csrf_token})
160 'csrf_token': self.csrf_token})
161
161
162 assert_session_flash(response, 'Ssh key successfully deleted')
162 assert_session_flash(response, 'Ssh key successfully deleted')
163 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
163 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
164 assert 0 == len(keys)
164 assert 0 == len(keys)
165
165
166 def test_generate_keypair(self, user_util):
166 def test_generate_keypair(self, user_util):
167 self.log_user()
167 self.log_user()
168 user = user_util.create_user()
168 user = user_util.create_user()
169 user_id = user.user_id
169 user_id = user.user_id
170
170
171 response = self.app.get(
171 response = self.app.get(
172 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
172 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
173
173
174 response.mustcontain('Private key')
174 response.mustcontain('Private key')
175 response.mustcontain('Public key')
175 response.mustcontain('Public key')
176 response.mustcontain('-----BEGIN PRIVATE KEY-----')
176 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,234 +1,234 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 logging
21 import logging
22 import urllib2
22 import urllib.request, urllib.error, urllib.parse
23 import os
23 import os
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base.navigation import navigation_list
27 from rhodecode.apps._base.navigation import navigation_list
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
30 from rhodecode.lib.utils2 import str2bool
30 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.lib import system_info
31 from rhodecode.lib import system_info
32 from rhodecode.model.update import UpdateModel
32 from rhodecode.model.update import UpdateModel
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 class AdminSystemInfoSettingsView(BaseAppView):
37 class AdminSystemInfoSettingsView(BaseAppView):
38 def load_default_context(self):
38 def load_default_context(self):
39 c = self._get_local_tmpl_context()
39 c = self._get_local_tmpl_context()
40 return c
40 return c
41
41
42 def get_env_data(self):
42 def get_env_data(self):
43 black_list = [
43 black_list = [
44 'NIX_LDFLAGS',
44 'NIX_LDFLAGS',
45 'NIX_CFLAGS_COMPILE',
45 'NIX_CFLAGS_COMPILE',
46 'propagatedBuildInputs',
46 'propagatedBuildInputs',
47 'propagatedNativeBuildInputs',
47 'propagatedNativeBuildInputs',
48 'postInstall',
48 'postInstall',
49 'buildInputs',
49 'buildInputs',
50 'buildPhase',
50 'buildPhase',
51 'preShellHook',
51 'preShellHook',
52 'preShellHook',
52 'preShellHook',
53 'preCheck',
53 'preCheck',
54 'preBuild',
54 'preBuild',
55 'postShellHook',
55 'postShellHook',
56 'postFixup',
56 'postFixup',
57 'postCheck',
57 'postCheck',
58 'nativeBuildInputs',
58 'nativeBuildInputs',
59 'installPhase',
59 'installPhase',
60 'installCheckPhase',
60 'installCheckPhase',
61 'checkPhase',
61 'checkPhase',
62 'configurePhase',
62 'configurePhase',
63 'shellHook'
63 'shellHook'
64 ]
64 ]
65 secret_list = [
65 secret_list = [
66 'RHODECODE_USER_PASS'
66 'RHODECODE_USER_PASS'
67 ]
67 ]
68
68
69 for k, v in sorted(os.environ.items()):
69 for k, v in sorted(os.environ.items()):
70 if k in black_list:
70 if k in black_list:
71 continue
71 continue
72 if k in secret_list:
72 if k in secret_list:
73 v = '*****'
73 v = '*****'
74 yield k, v
74 yield k, v
75
75
76 @LoginRequired()
76 @LoginRequired()
77 @HasPermissionAllDecorator('hg.admin')
77 @HasPermissionAllDecorator('hg.admin')
78 def settings_system_info(self):
78 def settings_system_info(self):
79 _ = self.request.translate
79 _ = self.request.translate
80 c = self.load_default_context()
80 c = self.load_default_context()
81
81
82 c.active = 'system'
82 c.active = 'system'
83 c.navlist = navigation_list(self.request)
83 c.navlist = navigation_list(self.request)
84
84
85 # TODO(marcink), figure out how to allow only selected users to do this
85 # TODO(marcink), figure out how to allow only selected users to do this
86 c.allowed_to_snapshot = self._rhodecode_user.admin
86 c.allowed_to_snapshot = self._rhodecode_user.admin
87
87
88 snapshot = str2bool(self.request.params.get('snapshot'))
88 snapshot = str2bool(self.request.params.get('snapshot'))
89
89
90 c.rhodecode_update_url = UpdateModel().get_update_url()
90 c.rhodecode_update_url = UpdateModel().get_update_url()
91 c.env_data = self.get_env_data()
91 c.env_data = self.get_env_data()
92 server_info = system_info.get_system_info(self.request.environ)
92 server_info = system_info.get_system_info(self.request.environ)
93
93
94 for key, val in server_info.items():
94 for key, val in server_info.items():
95 setattr(c, key, val)
95 setattr(c, key, val)
96
96
97 def val(name, subkey='human_value'):
97 def val(name, subkey='human_value'):
98 return server_info[name][subkey]
98 return server_info[name][subkey]
99
99
100 def state(name):
100 def state(name):
101 return server_info[name]['state']
101 return server_info[name]['state']
102
102
103 def val2(name):
103 def val2(name):
104 val = server_info[name]['human_value']
104 val = server_info[name]['human_value']
105 state = server_info[name]['state']
105 state = server_info[name]['state']
106 return val, state
106 return val, state
107
107
108 update_info_msg = _('Note: please make sure this server can '
108 update_info_msg = _('Note: please make sure this server can '
109 'access `${url}` for the update link to work',
109 'access `${url}` for the update link to work',
110 mapping=dict(url=c.rhodecode_update_url))
110 mapping=dict(url=c.rhodecode_update_url))
111 version = UpdateModel().get_stored_version()
111 version = UpdateModel().get_stored_version()
112 is_outdated = UpdateModel().is_outdated(
112 is_outdated = UpdateModel().is_outdated(
113 rhodecode.__version__, version)
113 rhodecode.__version__, version)
114 update_state = {
114 update_state = {
115 'type': 'warning',
115 'type': 'warning',
116 'message': 'New version available: {}'.format(version)
116 'message': 'New version available: {}'.format(version)
117 } \
117 } \
118 if is_outdated else {}
118 if is_outdated else {}
119 c.data_items = [
119 c.data_items = [
120 # update info
120 # update info
121 (_('Update info'), h.literal(
121 (_('Update info'), h.literal(
122 '<span class="link" id="check_for_update" >%s.</span>' % (
122 '<span class="link" id="check_for_update" >%s.</span>' % (
123 _('Check for updates')) +
123 _('Check for updates')) +
124 '<br/> <span >%s.</span>' % (update_info_msg)
124 '<br/> <span >%s.</span>' % (update_info_msg)
125 ), ''),
125 ), ''),
126
126
127 # RhodeCode specific
127 # RhodeCode specific
128 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
128 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
129 (_('Latest version'), version, update_state),
129 (_('Latest version'), version, update_state),
130 (_('RhodeCode Base URL'), val('rhodecode_config')['config'].get('app.base_url'), state('rhodecode_config')),
130 (_('RhodeCode Base URL'), val('rhodecode_config')['config'].get('app.base_url'), state('rhodecode_config')),
131 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
131 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
132 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
132 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
133 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
133 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
134 (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
134 (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
135 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
135 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
136 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
136 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
137 ('', '', ''), # spacer
137 ('', '', ''), # spacer
138
138
139 # Database
139 # Database
140 (_('Database'), val('database')['url'], state('database')),
140 (_('Database'), val('database')['url'], state('database')),
141 (_('Database version'), val('database')['version'], state('database')),
141 (_('Database version'), val('database')['version'], state('database')),
142 ('', '', ''), # spacer
142 ('', '', ''), # spacer
143
143
144 # Platform/Python
144 # Platform/Python
145 (_('Platform'), val('platform')['name'], state('platform')),
145 (_('Platform'), val('platform')['name'], state('platform')),
146 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
146 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
147 (_('Lang'), val('locale'), state('locale')),
147 (_('Lang'), val('locale'), state('locale')),
148 (_('Python version'), val('python')['version'], state('python')),
148 (_('Python version'), val('python')['version'], state('python')),
149 (_('Python path'), val('python')['executable'], state('python')),
149 (_('Python path'), val('python')['executable'], state('python')),
150 ('', '', ''), # spacer
150 ('', '', ''), # spacer
151
151
152 # Systems stats
152 # Systems stats
153 (_('CPU'), val('cpu')['text'], state('cpu')),
153 (_('CPU'), val('cpu')['text'], state('cpu')),
154 (_('Load'), val('load')['text'], state('load')),
154 (_('Load'), val('load')['text'], state('load')),
155 (_('Memory'), val('memory')['text'], state('memory')),
155 (_('Memory'), val('memory')['text'], state('memory')),
156 (_('Uptime'), val('uptime')['text'], state('uptime')),
156 (_('Uptime'), val('uptime')['text'], state('uptime')),
157 ('', '', ''), # spacer
157 ('', '', ''), # spacer
158
158
159 # ulimit
159 # ulimit
160 (_('Ulimit'), val('ulimit')['text'], state('ulimit')),
160 (_('Ulimit'), val('ulimit')['text'], state('ulimit')),
161
161
162 # Repo storage
162 # Repo storage
163 (_('Storage location'), val('storage')['path'], state('storage')),
163 (_('Storage location'), val('storage')['path'], state('storage')),
164 (_('Storage info'), val('storage')['text'], state('storage')),
164 (_('Storage info'), val('storage')['text'], state('storage')),
165 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
165 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
166
166
167 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
167 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
168 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
168 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
169
169
170 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
170 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
171 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
171 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
172
172
173 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
173 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
174 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
174 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
175
175
176 (_('Search info'), val('search')['text'], state('search')),
176 (_('Search info'), val('search')['text'], state('search')),
177 (_('Search location'), val('search')['location'], state('search')),
177 (_('Search location'), val('search')['location'], state('search')),
178 ('', '', ''), # spacer
178 ('', '', ''), # spacer
179
179
180 # VCS specific
180 # VCS specific
181 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
181 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
182 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
182 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
183 (_('GIT'), val('git'), state('git')),
183 (_('GIT'), val('git'), state('git')),
184 (_('HG'), val('hg'), state('hg')),
184 (_('HG'), val('hg'), state('hg')),
185 (_('SVN'), val('svn'), state('svn')),
185 (_('SVN'), val('svn'), state('svn')),
186
186
187 ]
187 ]
188
188
189 c.vcsserver_data_items = [
189 c.vcsserver_data_items = [
190 (k, v) for k,v in (val('vcs_server_config') or {}).items()
190 (k, v) for k,v in (val('vcs_server_config') or {}).items()
191 ]
191 ]
192
192
193 if snapshot:
193 if snapshot:
194 if c.allowed_to_snapshot:
194 if c.allowed_to_snapshot:
195 c.data_items.pop(0) # remove server info
195 c.data_items.pop(0) # remove server info
196 self.request.override_renderer = 'admin/settings/settings_system_snapshot.mako'
196 self.request.override_renderer = 'admin/settings/settings_system_snapshot.mako'
197 else:
197 else:
198 h.flash('You are not allowed to do this', category='warning')
198 h.flash('You are not allowed to do this', category='warning')
199 return self._get_template_context(c)
199 return self._get_template_context(c)
200
200
201 @LoginRequired()
201 @LoginRequired()
202 @HasPermissionAllDecorator('hg.admin')
202 @HasPermissionAllDecorator('hg.admin')
203 def settings_system_info_check_update(self):
203 def settings_system_info_check_update(self):
204 _ = self.request.translate
204 _ = self.request.translate
205 c = self.load_default_context()
205 c = self.load_default_context()
206
206
207 update_url = UpdateModel().get_update_url()
207 update_url = UpdateModel().get_update_url()
208
208
209 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">{}</div>'.format(s)
209 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">{}</div>'.format(s)
210 try:
210 try:
211 data = UpdateModel().get_update_data(update_url)
211 data = UpdateModel().get_update_data(update_url)
212 except urllib2.URLError as e:
212 except urllib.error.URLError as e:
213 log.exception("Exception contacting upgrade server")
213 log.exception("Exception contacting upgrade server")
214 self.request.override_renderer = 'string'
214 self.request.override_renderer = 'string'
215 return _err('Failed to contact upgrade server: %r' % e)
215 return _err('Failed to contact upgrade server: %r' % e)
216 except ValueError as e:
216 except ValueError as e:
217 log.exception("Bad data sent from update server")
217 log.exception("Bad data sent from update server")
218 self.request.override_renderer = 'string'
218 self.request.override_renderer = 'string'
219 return _err('Bad data sent from update server')
219 return _err('Bad data sent from update server')
220
220
221 latest = data['versions'][0]
221 latest = data['versions'][0]
222
222
223 c.update_url = update_url
223 c.update_url = update_url
224 c.latest_data = latest
224 c.latest_data = latest
225 c.latest_ver = latest['version']
225 c.latest_ver = latest['version']
226 c.cur_ver = rhodecode.__version__
226 c.cur_ver = rhodecode.__version__
227 c.should_upgrade = False
227 c.should_upgrade = False
228
228
229 is_oudated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
229 is_oudated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
230 if is_oudated:
230 if is_oudated:
231 c.should_upgrade = True
231 c.should_upgrade = True
232 c.important_notices = latest['general']
232 c.important_notices = latest['general']
233 UpdateModel().store_version(latest['version'])
233 UpdateModel().store_version(latest['version'])
234 return self._get_template_context(c)
234 return self._get_template_context(c)
@@ -1,261 +1,261 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import os
20 import os
21 import pytest
21 import pytest
22
22
23 from rhodecode.lib.ext_json import json
23 from rhodecode.lib.ext_json import json
24 from rhodecode.model.auth_token import AuthTokenModel
24 from rhodecode.model.auth_token import AuthTokenModel
25 from rhodecode.model.db import Session, FileStore, Repository, User
25 from rhodecode.model.db import Session, FileStore, Repository, User
26 from rhodecode.tests import TestController
26 from rhodecode.tests import TestController
27 from rhodecode.apps.file_store import utils, config_keys
27 from rhodecode.apps.file_store import utils, config_keys
28
28
29
29
30 def route_path(name, params=None, **kwargs):
30 def route_path(name, params=None, **kwargs):
31 import urllib
31 import urllib.request, urllib.parse, urllib.error
32
32
33 base_url = {
33 base_url = {
34 'upload_file': '/_file_store/upload',
34 'upload_file': '/_file_store/upload',
35 'download_file': '/_file_store/download/{fid}',
35 'download_file': '/_file_store/download/{fid}',
36 'download_file_by_token': '/_file_store/token-download/{_auth_token}/{fid}'
36 'download_file_by_token': '/_file_store/token-download/{_auth_token}/{fid}'
37
37
38 }[name].format(**kwargs)
38 }[name].format(**kwargs)
39
39
40 if params:
40 if params:
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 return base_url
42 return base_url
43
43
44
44
45 class TestFileStoreViews(TestController):
45 class TestFileStoreViews(TestController):
46
46
47 @pytest.mark.parametrize("fid, content, exists", [
47 @pytest.mark.parametrize("fid, content, exists", [
48 ('abcde-0.jpg', "xxxxx", True),
48 ('abcde-0.jpg', "xxxxx", True),
49 ('abcde-0.exe', "1234567", True),
49 ('abcde-0.exe', "1234567", True),
50 ('abcde-0.jpg', "xxxxx", False),
50 ('abcde-0.jpg', "xxxxx", False),
51 ])
51 ])
52 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
52 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
53 user = self.log_user()
53 user = self.log_user()
54 user_id = user['user_id']
54 user_id = user['user_id']
55 repo_id = user_util.create_repo().repo_id
55 repo_id = user_util.create_repo().repo_id
56 store_path = self.app._pyramid_settings[config_keys.store_path]
56 store_path = self.app._pyramid_settings[config_keys.store_path]
57 store_uid = fid
57 store_uid = fid
58
58
59 if exists:
59 if exists:
60 status = 200
60 status = 200
61 store = utils.get_file_storage({config_keys.store_path: store_path})
61 store = utils.get_file_storage({config_keys.store_path: store_path})
62 filesystem_file = os.path.join(str(tmpdir), fid)
62 filesystem_file = os.path.join(str(tmpdir), fid)
63 with open(filesystem_file, 'wb') as f:
63 with open(filesystem_file, 'wb') as f:
64 f.write(content)
64 f.write(content)
65
65
66 with open(filesystem_file, 'rb') as f:
66 with open(filesystem_file, 'rb') as f:
67 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
67 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
68
68
69 entry = FileStore.create(
69 entry = FileStore.create(
70 file_uid=store_uid, filename=metadata["filename"],
70 file_uid=store_uid, filename=metadata["filename"],
71 file_hash=metadata["sha256"], file_size=metadata["size"],
71 file_hash=metadata["sha256"], file_size=metadata["size"],
72 file_display_name='file_display_name',
72 file_display_name='file_display_name',
73 file_description='repo artifact `{}`'.format(metadata["filename"]),
73 file_description='repo artifact `{}`'.format(metadata["filename"]),
74 check_acl=True, user_id=user_id,
74 check_acl=True, user_id=user_id,
75 scope_repo_id=repo_id
75 scope_repo_id=repo_id
76 )
76 )
77 Session().add(entry)
77 Session().add(entry)
78 Session().commit()
78 Session().commit()
79
79
80 else:
80 else:
81 status = 404
81 status = 404
82
82
83 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
83 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
84
84
85 if exists:
85 if exists:
86 assert response.text == content
86 assert response.text == content
87 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
87 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
88 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
88 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
89 assert os.path.exists(metadata_file)
89 assert os.path.exists(metadata_file)
90 with open(metadata_file, 'rb') as f:
90 with open(metadata_file, 'rb') as f:
91 json_data = json.loads(f.read())
91 json_data = json.loads(f.read())
92
92
93 assert json_data
93 assert json_data
94 assert 'size' in json_data
94 assert 'size' in json_data
95
95
96 def test_upload_files_without_content_to_store(self):
96 def test_upload_files_without_content_to_store(self):
97 self.log_user()
97 self.log_user()
98 response = self.app.post(
98 response = self.app.post(
99 route_path('upload_file'),
99 route_path('upload_file'),
100 params={'csrf_token': self.csrf_token},
100 params={'csrf_token': self.csrf_token},
101 status=200)
101 status=200)
102
102
103 assert response.json == {
103 assert response.json == {
104 u'error': u'store_file data field is missing',
104 u'error': u'store_file data field is missing',
105 u'access_path': None,
105 u'access_path': None,
106 u'store_fid': None}
106 u'store_fid': None}
107
107
108 def test_upload_files_bogus_content_to_store(self):
108 def test_upload_files_bogus_content_to_store(self):
109 self.log_user()
109 self.log_user()
110 response = self.app.post(
110 response = self.app.post(
111 route_path('upload_file'),
111 route_path('upload_file'),
112 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
112 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
113 status=200)
113 status=200)
114
114
115 assert response.json == {
115 assert response.json == {
116 u'error': u'filename cannot be read from the data field',
116 u'error': u'filename cannot be read from the data field',
117 u'access_path': None,
117 u'access_path': None,
118 u'store_fid': None}
118 u'store_fid': None}
119
119
120 def test_upload_content_to_store(self):
120 def test_upload_content_to_store(self):
121 self.log_user()
121 self.log_user()
122 response = self.app.post(
122 response = self.app.post(
123 route_path('upload_file'),
123 route_path('upload_file'),
124 upload_files=[('store_file', 'myfile.txt', 'SOME CONTENT')],
124 upload_files=[('store_file', 'myfile.txt', 'SOME CONTENT')],
125 params={'csrf_token': self.csrf_token},
125 params={'csrf_token': self.csrf_token},
126 status=200)
126 status=200)
127
127
128 assert response.json['store_fid']
128 assert response.json['store_fid']
129
129
130 @pytest.fixture()
130 @pytest.fixture()
131 def create_artifact_factory(self, tmpdir):
131 def create_artifact_factory(self, tmpdir):
132 def factory(user_id, content):
132 def factory(user_id, content):
133 store_path = self.app._pyramid_settings[config_keys.store_path]
133 store_path = self.app._pyramid_settings[config_keys.store_path]
134 store = utils.get_file_storage({config_keys.store_path: store_path})
134 store = utils.get_file_storage({config_keys.store_path: store_path})
135 fid = 'example.txt'
135 fid = 'example.txt'
136
136
137 filesystem_file = os.path.join(str(tmpdir), fid)
137 filesystem_file = os.path.join(str(tmpdir), fid)
138 with open(filesystem_file, 'wb') as f:
138 with open(filesystem_file, 'wb') as f:
139 f.write(content)
139 f.write(content)
140
140
141 with open(filesystem_file, 'rb') as f:
141 with open(filesystem_file, 'rb') as f:
142 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
142 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
143
143
144 entry = FileStore.create(
144 entry = FileStore.create(
145 file_uid=store_uid, filename=metadata["filename"],
145 file_uid=store_uid, filename=metadata["filename"],
146 file_hash=metadata["sha256"], file_size=metadata["size"],
146 file_hash=metadata["sha256"], file_size=metadata["size"],
147 file_display_name='file_display_name',
147 file_display_name='file_display_name',
148 file_description='repo artifact `{}`'.format(metadata["filename"]),
148 file_description='repo artifact `{}`'.format(metadata["filename"]),
149 check_acl=True, user_id=user_id,
149 check_acl=True, user_id=user_id,
150 )
150 )
151 Session().add(entry)
151 Session().add(entry)
152 Session().commit()
152 Session().commit()
153 return entry
153 return entry
154 return factory
154 return factory
155
155
156 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
156 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
157 user = self.log_user()
157 user = self.log_user()
158 user_id = user['user_id']
158 user_id = user['user_id']
159 content = 'HELLO MY NAME IS ARTIFACT !'
159 content = 'HELLO MY NAME IS ARTIFACT !'
160
160
161 artifact = create_artifact_factory(user_id, content)
161 artifact = create_artifact_factory(user_id, content)
162 file_uid = artifact.file_uid
162 file_uid = artifact.file_uid
163 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
163 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
164 assert response.text == content
164 assert response.text == content
165
165
166 # log-in to new user and test download again
166 # log-in to new user and test download again
167 user = user_util.create_user(password='qweqwe')
167 user = user_util.create_user(password='qweqwe')
168 self.log_user(user.username, 'qweqwe')
168 self.log_user(user.username, 'qweqwe')
169 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
169 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
170 assert response.text == content
170 assert response.text == content
171
171
172 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
172 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
173 user = self.log_user()
173 user = self.log_user()
174 user_id = user['user_id']
174 user_id = user['user_id']
175 content = 'HELLO MY NAME IS ARTIFACT !'
175 content = 'HELLO MY NAME IS ARTIFACT !'
176
176
177 artifact = create_artifact_factory(user_id, content)
177 artifact = create_artifact_factory(user_id, content)
178 # bind to repo
178 # bind to repo
179 repo = user_util.create_repo()
179 repo = user_util.create_repo()
180 repo_id = repo.repo_id
180 repo_id = repo.repo_id
181 artifact.scope_repo_id = repo_id
181 artifact.scope_repo_id = repo_id
182 Session().add(artifact)
182 Session().add(artifact)
183 Session().commit()
183 Session().commit()
184
184
185 file_uid = artifact.file_uid
185 file_uid = artifact.file_uid
186 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
186 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
187 assert response.text == content
187 assert response.text == content
188
188
189 # log-in to new user and test download again
189 # log-in to new user and test download again
190 user = user_util.create_user(password='qweqwe')
190 user = user_util.create_user(password='qweqwe')
191 self.log_user(user.username, 'qweqwe')
191 self.log_user(user.username, 'qweqwe')
192 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
192 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
193 assert response.text == content
193 assert response.text == content
194
194
195 # forbid user the rights to repo
195 # forbid user the rights to repo
196 repo = Repository.get(repo_id)
196 repo = Repository.get(repo_id)
197 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
197 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
198 self.app.get(route_path('download_file', fid=file_uid), status=404)
198 self.app.get(route_path('download_file', fid=file_uid), status=404)
199
199
200 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
200 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
201 user = self.log_user()
201 user = self.log_user()
202 user_id = user['user_id']
202 user_id = user['user_id']
203 content = 'HELLO MY NAME IS ARTIFACT !'
203 content = 'HELLO MY NAME IS ARTIFACT !'
204
204
205 artifact = create_artifact_factory(user_id, content)
205 artifact = create_artifact_factory(user_id, content)
206 # bind to user
206 # bind to user
207 user = user_util.create_user(password='qweqwe')
207 user = user_util.create_user(password='qweqwe')
208
208
209 artifact.scope_user_id = user.user_id
209 artifact.scope_user_id = user.user_id
210 Session().add(artifact)
210 Session().add(artifact)
211 Session().commit()
211 Session().commit()
212
212
213 # artifact creator doesn't have access since it's bind to another user
213 # artifact creator doesn't have access since it's bind to another user
214 file_uid = artifact.file_uid
214 file_uid = artifact.file_uid
215 self.app.get(route_path('download_file', fid=file_uid), status=404)
215 self.app.get(route_path('download_file', fid=file_uid), status=404)
216
216
217 # log-in to new user and test download again, should be ok since we're bind to this artifact
217 # log-in to new user and test download again, should be ok since we're bind to this artifact
218 self.log_user(user.username, 'qweqwe')
218 self.log_user(user.username, 'qweqwe')
219 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
219 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
220 assert response.text == content
220 assert response.text == content
221
221
222 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
222 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
223 user_id = User.get_first_super_admin().user_id
223 user_id = User.get_first_super_admin().user_id
224 content = 'HELLO MY NAME IS ARTIFACT !'
224 content = 'HELLO MY NAME IS ARTIFACT !'
225
225
226 artifact = create_artifact_factory(user_id, content)
226 artifact = create_artifact_factory(user_id, content)
227 # bind to repo
227 # bind to repo
228 repo = user_util.create_repo()
228 repo = user_util.create_repo()
229 repo_id = repo.repo_id
229 repo_id = repo.repo_id
230 artifact.scope_repo_id = repo_id
230 artifact.scope_repo_id = repo_id
231 Session().add(artifact)
231 Session().add(artifact)
232 Session().commit()
232 Session().commit()
233
233
234 file_uid = artifact.file_uid
234 file_uid = artifact.file_uid
235 self.app.get(route_path('download_file_by_token',
235 self.app.get(route_path('download_file_by_token',
236 _auth_token='bogus', fid=file_uid), status=302)
236 _auth_token='bogus', fid=file_uid), status=302)
237
237
238 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
238 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
239 user = User.get_first_super_admin()
239 user = User.get_first_super_admin()
240 AuthTokenModel().create(user, 'test artifact token',
240 AuthTokenModel().create(user, 'test artifact token',
241 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
241 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
242
242
243 user = User.get_first_super_admin()
243 user = User.get_first_super_admin()
244 artifact_token = user.artifact_token
244 artifact_token = user.artifact_token
245
245
246 user_id = User.get_first_super_admin().user_id
246 user_id = User.get_first_super_admin().user_id
247 content = 'HELLO MY NAME IS ARTIFACT !'
247 content = 'HELLO MY NAME IS ARTIFACT !'
248
248
249 artifact = create_artifact_factory(user_id, content)
249 artifact = create_artifact_factory(user_id, content)
250 # bind to repo
250 # bind to repo
251 repo = user_util.create_repo()
251 repo = user_util.create_repo()
252 repo_id = repo.repo_id
252 repo_id = repo.repo_id
253 artifact.scope_repo_id = repo_id
253 artifact.scope_repo_id = repo_id
254 Session().add(artifact)
254 Session().add(artifact)
255 Session().commit()
255 Session().commit()
256
256
257 file_uid = artifact.file_uid
257 file_uid = artifact.file_uid
258 response = self.app.get(
258 response = self.app.get(
259 route_path('download_file_by_token',
259 route_path('download_file_by_token',
260 _auth_token=artifact_token, fid=file_uid), status=200)
260 _auth_token=artifact_token, fid=file_uid), status=200)
261 assert response.text == content
261 assert response.text == content
@@ -1,391 +1,391 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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
23
24 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
25 from rhodecode.model.db import User, Gist
25 from rhodecode.model.db import User, Gist
26 from rhodecode.model.gist import GistModel
26 from rhodecode.model.gist import GistModel
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.tests import (
28 from rhodecode.tests import (
29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
30 TestController, assert_session_flash)
30 TestController, assert_session_flash)
31
31
32
32
33 def route_path(name, params=None, **kwargs):
33 def route_path(name, params=None, **kwargs):
34 import urllib
34 import urllib.request, urllib.parse, urllib.error
35 from rhodecode.apps._base import ADMIN_PREFIX
35 from rhodecode.apps._base import ADMIN_PREFIX
36
36
37 base_url = {
37 base_url = {
38 'gists_show': ADMIN_PREFIX + '/gists',
38 'gists_show': ADMIN_PREFIX + '/gists',
39 'gists_new': ADMIN_PREFIX + '/gists/new',
39 'gists_new': ADMIN_PREFIX + '/gists/new',
40 'gists_create': ADMIN_PREFIX + '/gists/create',
40 'gists_create': ADMIN_PREFIX + '/gists/create',
41 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
41 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
42 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
42 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
43 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
43 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
44 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision',
44 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision',
45 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
45 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
46 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}',
46 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}',
47 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}',
47 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}',
48 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}/{f_path}',
48 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}/{f_path}',
49
49
50 }[name].format(**kwargs)
50 }[name].format(**kwargs)
51
51
52 if params:
52 if params:
53 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
53 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
54 return base_url
54 return base_url
55
55
56
56
57 class GistUtility(object):
57 class GistUtility(object):
58
58
59 def __init__(self):
59 def __init__(self):
60 self._gist_ids = []
60 self._gist_ids = []
61
61
62 def __call__(
62 def __call__(
63 self, f_name, content='some gist', lifetime=-1,
63 self, f_name, content='some gist', lifetime=-1,
64 description='gist-desc', gist_type='public',
64 description='gist-desc', gist_type='public',
65 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
65 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
66 gist_mapping = {
66 gist_mapping = {
67 f_name: {'content': content}
67 f_name: {'content': content}
68 }
68 }
69 user = User.get_by_username(owner)
69 user = User.get_by_username(owner)
70 gist = GistModel().create(
70 gist = GistModel().create(
71 description, owner=user, gist_mapping=gist_mapping,
71 description, owner=user, gist_mapping=gist_mapping,
72 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
72 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
73 Session().commit()
73 Session().commit()
74 self._gist_ids.append(gist.gist_id)
74 self._gist_ids.append(gist.gist_id)
75 return gist
75 return gist
76
76
77 def cleanup(self):
77 def cleanup(self):
78 for gist_id in self._gist_ids:
78 for gist_id in self._gist_ids:
79 gist = Gist.get(gist_id)
79 gist = Gist.get(gist_id)
80 if gist:
80 if gist:
81 Session().delete(gist)
81 Session().delete(gist)
82
82
83 Session().commit()
83 Session().commit()
84
84
85
85
86 @pytest.fixture()
86 @pytest.fixture()
87 def create_gist(request):
87 def create_gist(request):
88 gist_utility = GistUtility()
88 gist_utility = GistUtility()
89 request.addfinalizer(gist_utility.cleanup)
89 request.addfinalizer(gist_utility.cleanup)
90 return gist_utility
90 return gist_utility
91
91
92
92
93 class TestGistsController(TestController):
93 class TestGistsController(TestController):
94
94
95 def test_index_empty(self, create_gist):
95 def test_index_empty(self, create_gist):
96 self.log_user()
96 self.log_user()
97 response = self.app.get(route_path('gists_show'))
97 response = self.app.get(route_path('gists_show'))
98 response.mustcontain('data: [],')
98 response.mustcontain('data: [],')
99
99
100 def test_index(self, create_gist):
100 def test_index(self, create_gist):
101 self.log_user()
101 self.log_user()
102 g1 = create_gist('gist1')
102 g1 = create_gist('gist1')
103 g2 = create_gist('gist2', lifetime=1400)
103 g2 = create_gist('gist2', lifetime=1400)
104 g3 = create_gist('gist3', description='gist3-desc')
104 g3 = create_gist('gist3', description='gist3-desc')
105 g4 = create_gist('gist4', gist_type='private').gist_access_id
105 g4 = create_gist('gist4', gist_type='private').gist_access_id
106 response = self.app.get(route_path('gists_show'))
106 response = self.app.get(route_path('gists_show'))
107
107
108 response.mustcontain(g1.gist_access_id)
108 response.mustcontain(g1.gist_access_id)
109 response.mustcontain(g2.gist_access_id)
109 response.mustcontain(g2.gist_access_id)
110 response.mustcontain(g3.gist_access_id)
110 response.mustcontain(g3.gist_access_id)
111 response.mustcontain('gist3-desc')
111 response.mustcontain('gist3-desc')
112 response.mustcontain(no=[g4])
112 response.mustcontain(no=[g4])
113
113
114 # Expiration information should be visible
114 # Expiration information should be visible
115 expires_tag = '%s' % h.age_component(
115 expires_tag = '%s' % h.age_component(
116 h.time_to_utcdatetime(g2.gist_expires))
116 h.time_to_utcdatetime(g2.gist_expires))
117 response.mustcontain(expires_tag.replace('"', '\\"'))
117 response.mustcontain(expires_tag.replace('"', '\\"'))
118
118
119 def test_index_private_gists(self, create_gist):
119 def test_index_private_gists(self, create_gist):
120 self.log_user()
120 self.log_user()
121 gist = create_gist('gist5', gist_type='private')
121 gist = create_gist('gist5', gist_type='private')
122 response = self.app.get(route_path('gists_show', params=dict(private=1)))
122 response = self.app.get(route_path('gists_show', params=dict(private=1)))
123
123
124 # and privates
124 # and privates
125 response.mustcontain(gist.gist_access_id)
125 response.mustcontain(gist.gist_access_id)
126
126
127 def test_index_show_all(self, create_gist):
127 def test_index_show_all(self, create_gist):
128 self.log_user()
128 self.log_user()
129 create_gist('gist1')
129 create_gist('gist1')
130 create_gist('gist2', lifetime=1400)
130 create_gist('gist2', lifetime=1400)
131 create_gist('gist3', description='gist3-desc')
131 create_gist('gist3', description='gist3-desc')
132 create_gist('gist4', gist_type='private')
132 create_gist('gist4', gist_type='private')
133
133
134 response = self.app.get(route_path('gists_show', params=dict(all=1)))
134 response = self.app.get(route_path('gists_show', params=dict(all=1)))
135
135
136 assert len(GistModel.get_all()) == 4
136 assert len(GistModel.get_all()) == 4
137 # and privates
137 # and privates
138 for gist in GistModel.get_all():
138 for gist in GistModel.get_all():
139 response.mustcontain(gist.gist_access_id)
139 response.mustcontain(gist.gist_access_id)
140
140
141 def test_index_show_all_hidden_from_regular(self, create_gist):
141 def test_index_show_all_hidden_from_regular(self, create_gist):
142 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
142 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
143 create_gist('gist2', gist_type='private')
143 create_gist('gist2', gist_type='private')
144 create_gist('gist3', gist_type='private')
144 create_gist('gist3', gist_type='private')
145 create_gist('gist4', gist_type='private')
145 create_gist('gist4', gist_type='private')
146
146
147 response = self.app.get(route_path('gists_show', params=dict(all=1)))
147 response = self.app.get(route_path('gists_show', params=dict(all=1)))
148
148
149 assert len(GistModel.get_all()) == 3
149 assert len(GistModel.get_all()) == 3
150 # since we don't have access to private in this view, we
150 # since we don't have access to private in this view, we
151 # should see nothing
151 # should see nothing
152 for gist in GistModel.get_all():
152 for gist in GistModel.get_all():
153 response.mustcontain(no=[gist.gist_access_id])
153 response.mustcontain(no=[gist.gist_access_id])
154
154
155 def test_create(self):
155 def test_create(self):
156 self.log_user()
156 self.log_user()
157 response = self.app.post(
157 response = self.app.post(
158 route_path('gists_create'),
158 route_path('gists_create'),
159 params={'lifetime': -1,
159 params={'lifetime': -1,
160 'content': 'gist test',
160 'content': 'gist test',
161 'filename': 'foo',
161 'filename': 'foo',
162 'gist_type': 'public',
162 'gist_type': 'public',
163 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
163 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
164 'csrf_token': self.csrf_token},
164 'csrf_token': self.csrf_token},
165 status=302)
165 status=302)
166 response = response.follow()
166 response = response.follow()
167 response.mustcontain('added file: foo')
167 response.mustcontain('added file: foo')
168 response.mustcontain('gist test')
168 response.mustcontain('gist test')
169
169
170 def test_create_with_path_with_dirs(self):
170 def test_create_with_path_with_dirs(self):
171 self.log_user()
171 self.log_user()
172 response = self.app.post(
172 response = self.app.post(
173 route_path('gists_create'),
173 route_path('gists_create'),
174 params={'lifetime': -1,
174 params={'lifetime': -1,
175 'content': 'gist test',
175 'content': 'gist test',
176 'filename': '/home/foo',
176 'filename': '/home/foo',
177 'gist_type': 'public',
177 'gist_type': 'public',
178 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
178 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
179 'csrf_token': self.csrf_token},
179 'csrf_token': self.csrf_token},
180 status=200)
180 status=200)
181 response.mustcontain('Filename /home/foo cannot be inside a directory')
181 response.mustcontain('Filename /home/foo cannot be inside a directory')
182
182
183 def test_access_expired_gist(self, create_gist):
183 def test_access_expired_gist(self, create_gist):
184 self.log_user()
184 self.log_user()
185 gist = create_gist('never-see-me')
185 gist = create_gist('never-see-me')
186 gist.gist_expires = 0 # 1970
186 gist.gist_expires = 0 # 1970
187 Session().add(gist)
187 Session().add(gist)
188 Session().commit()
188 Session().commit()
189
189
190 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
190 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
191 status=404)
191 status=404)
192
192
193 def test_create_private(self):
193 def test_create_private(self):
194 self.log_user()
194 self.log_user()
195 response = self.app.post(
195 response = self.app.post(
196 route_path('gists_create'),
196 route_path('gists_create'),
197 params={'lifetime': -1,
197 params={'lifetime': -1,
198 'content': 'private gist test',
198 'content': 'private gist test',
199 'filename': 'private-foo',
199 'filename': 'private-foo',
200 'gist_type': 'private',
200 'gist_type': 'private',
201 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
201 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
202 'csrf_token': self.csrf_token},
202 'csrf_token': self.csrf_token},
203 status=302)
203 status=302)
204 response = response.follow()
204 response = response.follow()
205 response.mustcontain('added file: private-foo<')
205 response.mustcontain('added file: private-foo<')
206 response.mustcontain('private gist test')
206 response.mustcontain('private gist test')
207 response.mustcontain('Private Gist')
207 response.mustcontain('Private Gist')
208 # Make sure private gists are not indexed by robots
208 # Make sure private gists are not indexed by robots
209 response.mustcontain(
209 response.mustcontain(
210 '<meta name="robots" content="noindex, nofollow">')
210 '<meta name="robots" content="noindex, nofollow">')
211
211
212 def test_create_private_acl_private(self):
212 def test_create_private_acl_private(self):
213 self.log_user()
213 self.log_user()
214 response = self.app.post(
214 response = self.app.post(
215 route_path('gists_create'),
215 route_path('gists_create'),
216 params={'lifetime': -1,
216 params={'lifetime': -1,
217 'content': 'private gist test',
217 'content': 'private gist test',
218 'filename': 'private-foo',
218 'filename': 'private-foo',
219 'gist_type': 'private',
219 'gist_type': 'private',
220 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
220 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
221 'csrf_token': self.csrf_token},
221 'csrf_token': self.csrf_token},
222 status=302)
222 status=302)
223 response = response.follow()
223 response = response.follow()
224 response.mustcontain('added file: private-foo<')
224 response.mustcontain('added file: private-foo<')
225 response.mustcontain('private gist test')
225 response.mustcontain('private gist test')
226 response.mustcontain('Private Gist')
226 response.mustcontain('Private Gist')
227 # Make sure private gists are not indexed by robots
227 # Make sure private gists are not indexed by robots
228 response.mustcontain(
228 response.mustcontain(
229 '<meta name="robots" content="noindex, nofollow">')
229 '<meta name="robots" content="noindex, nofollow">')
230
230
231 def test_create_with_description(self):
231 def test_create_with_description(self):
232 self.log_user()
232 self.log_user()
233 response = self.app.post(
233 response = self.app.post(
234 route_path('gists_create'),
234 route_path('gists_create'),
235 params={'lifetime': -1,
235 params={'lifetime': -1,
236 'content': 'gist test',
236 'content': 'gist test',
237 'filename': 'foo-desc',
237 'filename': 'foo-desc',
238 'description': 'gist-desc',
238 'description': 'gist-desc',
239 'gist_type': 'public',
239 'gist_type': 'public',
240 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
240 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
241 'csrf_token': self.csrf_token},
241 'csrf_token': self.csrf_token},
242 status=302)
242 status=302)
243 response = response.follow()
243 response = response.follow()
244 response.mustcontain('added file: foo-desc')
244 response.mustcontain('added file: foo-desc')
245 response.mustcontain('gist test')
245 response.mustcontain('gist test')
246 response.mustcontain('gist-desc')
246 response.mustcontain('gist-desc')
247
247
248 def test_create_public_with_anonymous_access(self):
248 def test_create_public_with_anonymous_access(self):
249 self.log_user()
249 self.log_user()
250 params = {
250 params = {
251 'lifetime': -1,
251 'lifetime': -1,
252 'content': 'gist test',
252 'content': 'gist test',
253 'filename': 'foo-desc',
253 'filename': 'foo-desc',
254 'description': 'gist-desc',
254 'description': 'gist-desc',
255 'gist_type': 'public',
255 'gist_type': 'public',
256 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
256 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
257 'csrf_token': self.csrf_token
257 'csrf_token': self.csrf_token
258 }
258 }
259 response = self.app.post(
259 response = self.app.post(
260 route_path('gists_create'), params=params, status=302)
260 route_path('gists_create'), params=params, status=302)
261 self.logout_user()
261 self.logout_user()
262 response = response.follow()
262 response = response.follow()
263 response.mustcontain('added file: foo-desc')
263 response.mustcontain('added file: foo-desc')
264 response.mustcontain('gist test')
264 response.mustcontain('gist test')
265 response.mustcontain('gist-desc')
265 response.mustcontain('gist-desc')
266
266
267 def test_new(self):
267 def test_new(self):
268 self.log_user()
268 self.log_user()
269 self.app.get(route_path('gists_new'))
269 self.app.get(route_path('gists_new'))
270
270
271 def test_delete(self, create_gist):
271 def test_delete(self, create_gist):
272 self.log_user()
272 self.log_user()
273 gist = create_gist('delete-me')
273 gist = create_gist('delete-me')
274 response = self.app.post(
274 response = self.app.post(
275 route_path('gist_delete', gist_id=gist.gist_id),
275 route_path('gist_delete', gist_id=gist.gist_id),
276 params={'csrf_token': self.csrf_token})
276 params={'csrf_token': self.csrf_token})
277 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
277 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
278
278
279 def test_delete_normal_user_his_gist(self, create_gist):
279 def test_delete_normal_user_his_gist(self, create_gist):
280 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
280 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
281 gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
281 gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
282
282
283 response = self.app.post(
283 response = self.app.post(
284 route_path('gist_delete', gist_id=gist.gist_id),
284 route_path('gist_delete', gist_id=gist.gist_id),
285 params={'csrf_token': self.csrf_token})
285 params={'csrf_token': self.csrf_token})
286 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
286 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
287
287
288 def test_delete_normal_user_not_his_own_gist(self, create_gist):
288 def test_delete_normal_user_not_his_own_gist(self, create_gist):
289 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
289 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
290 gist = create_gist('delete-me-2')
290 gist = create_gist('delete-me-2')
291
291
292 self.app.post(
292 self.app.post(
293 route_path('gist_delete', gist_id=gist.gist_id),
293 route_path('gist_delete', gist_id=gist.gist_id),
294 params={'csrf_token': self.csrf_token}, status=404)
294 params={'csrf_token': self.csrf_token}, status=404)
295
295
296 def test_show(self, create_gist):
296 def test_show(self, create_gist):
297 gist = create_gist('gist-show-me')
297 gist = create_gist('gist-show-me')
298 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
298 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
299
299
300 response.mustcontain('added file: gist-show-me<')
300 response.mustcontain('added file: gist-show-me<')
301
301
302 assert_response = response.assert_response()
302 assert_response = response.assert_response()
303 assert_response.element_equals_to(
303 assert_response.element_equals_to(
304 'div.rc-user span.user',
304 'div.rc-user span.user',
305 '<a href="/_profiles/test_admin">test_admin</a>')
305 '<a href="/_profiles/test_admin">test_admin</a>')
306
306
307 response.mustcontain('gist-desc')
307 response.mustcontain('gist-desc')
308
308
309 def test_show_without_hg(self, create_gist):
309 def test_show_without_hg(self, create_gist):
310 with mock.patch(
310 with mock.patch(
311 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
311 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
312 gist = create_gist('gist-show-me-again')
312 gist = create_gist('gist-show-me-again')
313 self.app.get(
313 self.app.get(
314 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
314 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
315
315
316 def test_show_acl_private(self, create_gist):
316 def test_show_acl_private(self, create_gist):
317 gist = create_gist('gist-show-me-only-when-im-logged-in',
317 gist = create_gist('gist-show-me-only-when-im-logged-in',
318 acl_level=Gist.ACL_LEVEL_PRIVATE)
318 acl_level=Gist.ACL_LEVEL_PRIVATE)
319 self.app.get(
319 self.app.get(
320 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
320 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
321
321
322 # now we log-in we should see thi gist
322 # now we log-in we should see thi gist
323 self.log_user()
323 self.log_user()
324 response = self.app.get(
324 response = self.app.get(
325 route_path('gist_show', gist_id=gist.gist_access_id))
325 route_path('gist_show', gist_id=gist.gist_access_id))
326 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
326 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
327
327
328 assert_response = response.assert_response()
328 assert_response = response.assert_response()
329 assert_response.element_equals_to(
329 assert_response.element_equals_to(
330 'div.rc-user span.user',
330 'div.rc-user span.user',
331 '<a href="/_profiles/test_admin">test_admin</a>')
331 '<a href="/_profiles/test_admin">test_admin</a>')
332 response.mustcontain('gist-desc')
332 response.mustcontain('gist-desc')
333
333
334 def test_show_as_raw(self, create_gist):
334 def test_show_as_raw(self, create_gist):
335 gist = create_gist('gist-show-me', content='GIST CONTENT')
335 gist = create_gist('gist-show-me', content='GIST CONTENT')
336 response = self.app.get(
336 response = self.app.get(
337 route_path('gist_show_formatted',
337 route_path('gist_show_formatted',
338 gist_id=gist.gist_access_id, revision='tip',
338 gist_id=gist.gist_access_id, revision='tip',
339 format='raw'))
339 format='raw'))
340 assert response.body == 'GIST CONTENT'
340 assert response.body == 'GIST CONTENT'
341
341
342 def test_show_as_raw_individual_file(self, create_gist):
342 def test_show_as_raw_individual_file(self, create_gist):
343 gist = create_gist('gist-show-me-raw', content='GIST BODY')
343 gist = create_gist('gist-show-me-raw', content='GIST BODY')
344 response = self.app.get(
344 response = self.app.get(
345 route_path('gist_show_formatted_path',
345 route_path('gist_show_formatted_path',
346 gist_id=gist.gist_access_id, format='raw',
346 gist_id=gist.gist_access_id, format='raw',
347 revision='tip', f_path='gist-show-me-raw'))
347 revision='tip', f_path='gist-show-me-raw'))
348 assert response.body == 'GIST BODY'
348 assert response.body == 'GIST BODY'
349
349
350 def test_edit_page(self, create_gist):
350 def test_edit_page(self, create_gist):
351 self.log_user()
351 self.log_user()
352 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
352 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
353 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
353 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
354 response.mustcontain('GIST EDIT BODY')
354 response.mustcontain('GIST EDIT BODY')
355
355
356 def test_edit_page_non_logged_user(self, create_gist):
356 def test_edit_page_non_logged_user(self, create_gist):
357 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
357 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
358 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
358 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
359 status=302)
359 status=302)
360
360
361 def test_edit_normal_user_his_gist(self, create_gist):
361 def test_edit_normal_user_his_gist(self, create_gist):
362 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
362 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
363 gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
363 gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
364 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
364 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
365 status=200))
365 status=200))
366
366
367 def test_edit_normal_user_not_his_own_gist(self, create_gist):
367 def test_edit_normal_user_not_his_own_gist(self, create_gist):
368 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
368 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
369 gist = create_gist('delete-me')
369 gist = create_gist('delete-me')
370 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
370 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
371 status=404)
371 status=404)
372
372
373 def test_user_first_name_is_escaped(self, user_util, create_gist):
373 def test_user_first_name_is_escaped(self, user_util, create_gist):
374 xss_atack_string = '"><script>alert(\'First Name\')</script>'
374 xss_atack_string = '"><script>alert(\'First Name\')</script>'
375 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
375 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
376 password = 'test'
376 password = 'test'
377 user = user_util.create_user(
377 user = user_util.create_user(
378 firstname=xss_atack_string, password=password)
378 firstname=xss_atack_string, password=password)
379 create_gist('gist', gist_type='public', owner=user.username)
379 create_gist('gist', gist_type='public', owner=user.username)
380 response = self.app.get(route_path('gists_show'))
380 response = self.app.get(route_path('gists_show'))
381 response.mustcontain(xss_escaped_string)
381 response.mustcontain(xss_escaped_string)
382
382
383 def test_user_last_name_is_escaped(self, user_util, create_gist):
383 def test_user_last_name_is_escaped(self, user_util, create_gist):
384 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
384 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
385 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
385 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
386 password = 'test'
386 password = 'test'
387 user = user_util.create_user(
387 user = user_util.create_user(
388 lastname=xss_atack_string, password=password)
388 lastname=xss_atack_string, password=password)
389 create_gist('gist', gist_type='public', owner=user.username)
389 create_gist('gist', gist_type='public', owner=user.username)
390 response = self.app.get(route_path('gists_show'))
390 response = self.app.get(route_path('gists_show'))
391 response.mustcontain(xss_escaped_string)
391 response.mustcontain(xss_escaped_string)
@@ -1,180 +1,180 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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
22
23 import pytest
23 import pytest
24
24
25 from . import assert_and_get_main_filter_content
25 from . import assert_and_get_main_filter_content
26 from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
26 from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
27 from rhodecode.tests.fixture import Fixture
27 from rhodecode.tests.fixture import Fixture
28
28
29 from rhodecode.lib.utils import map_groups
29 from rhodecode.lib.utils import map_groups
30 from rhodecode.model.repo import RepoModel
30 from rhodecode.model.repo import RepoModel
31 from rhodecode.model.repo_group import RepoGroupModel
31 from rhodecode.model.repo_group import RepoGroupModel
32 from rhodecode.model.db import Session, Repository, RepoGroup
32 from rhodecode.model.db import Session, Repository, RepoGroup
33
33
34 fixture = Fixture()
34 fixture = Fixture()
35
35
36
36
37 def route_path(name, params=None, **kwargs):
37 def route_path(name, params=None, **kwargs):
38 import urllib
38 import urllib.request, urllib.parse, urllib.error
39
39
40 base_url = {
40 base_url = {
41 'goto_switcher_data': '/_goto_data',
41 'goto_switcher_data': '/_goto_data',
42 }[name].format(**kwargs)
42 }[name].format(**kwargs)
43
43
44 if params:
44 if params:
45 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
45 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 return base_url
46 return base_url
47
47
48
48
49 class TestGotoSwitcherData(TestController):
49 class TestGotoSwitcherData(TestController):
50
50
51 required_repos_with_groups = [
51 required_repos_with_groups = [
52 'abc',
52 'abc',
53 'abc-fork',
53 'abc-fork',
54 'forks/abcd',
54 'forks/abcd',
55 'abcd',
55 'abcd',
56 'abcde',
56 'abcde',
57 'a/abc',
57 'a/abc',
58 'aa/abc',
58 'aa/abc',
59 'aaa/abc',
59 'aaa/abc',
60 'aaaa/abc',
60 'aaaa/abc',
61 'repos_abc/aaa/abc',
61 'repos_abc/aaa/abc',
62 'abc_repos/abc',
62 'abc_repos/abc',
63 'abc_repos/abcd',
63 'abc_repos/abcd',
64 'xxx/xyz',
64 'xxx/xyz',
65 'forked-abc/a/abc'
65 'forked-abc/a/abc'
66 ]
66 ]
67
67
68 @pytest.fixture(autouse=True, scope='class')
68 @pytest.fixture(autouse=True, scope='class')
69 def prepare(self, request, baseapp):
69 def prepare(self, request, baseapp):
70 for repo_and_group in self.required_repos_with_groups:
70 for repo_and_group in self.required_repos_with_groups:
71 # create structure of groups and return the last group
71 # create structure of groups and return the last group
72
72
73 repo_group = map_groups(repo_and_group)
73 repo_group = map_groups(repo_and_group)
74
74
75 RepoModel()._create_repo(
75 RepoModel()._create_repo(
76 repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN,
76 repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN,
77 repo_group=getattr(repo_group, 'group_id', None))
77 repo_group=getattr(repo_group, 'group_id', None))
78
78
79 Session().commit()
79 Session().commit()
80
80
81 request.addfinalizer(self.cleanup)
81 request.addfinalizer(self.cleanup)
82
82
83 def cleanup(self):
83 def cleanup(self):
84 # first delete all repos
84 # first delete all repos
85 for repo_and_groups in self.required_repos_with_groups:
85 for repo_and_groups in self.required_repos_with_groups:
86 repo = Repository.get_by_repo_name(repo_and_groups)
86 repo = Repository.get_by_repo_name(repo_and_groups)
87 if repo:
87 if repo:
88 RepoModel().delete(repo)
88 RepoModel().delete(repo)
89 Session().commit()
89 Session().commit()
90
90
91 # then delete all empty groups
91 # then delete all empty groups
92 for repo_and_groups in self.required_repos_with_groups:
92 for repo_and_groups in self.required_repos_with_groups:
93 if '/' in repo_and_groups:
93 if '/' in repo_and_groups:
94 r_group = repo_and_groups.rsplit('/', 1)[0]
94 r_group = repo_and_groups.rsplit('/', 1)[0]
95 repo_group = RepoGroup.get_by_group_name(r_group)
95 repo_group = RepoGroup.get_by_group_name(r_group)
96 if not repo_group:
96 if not repo_group:
97 continue
97 continue
98 parents = repo_group.parents
98 parents = repo_group.parents
99 RepoGroupModel().delete(repo_group, force_delete=True)
99 RepoGroupModel().delete(repo_group, force_delete=True)
100 Session().commit()
100 Session().commit()
101
101
102 for el in reversed(parents):
102 for el in reversed(parents):
103 RepoGroupModel().delete(el, force_delete=True)
103 RepoGroupModel().delete(el, force_delete=True)
104 Session().commit()
104 Session().commit()
105
105
106 def test_empty_query(self, xhr_header):
106 def test_empty_query(self, xhr_header):
107 self.log_user()
107 self.log_user()
108
108
109 response = self.app.get(
109 response = self.app.get(
110 route_path('goto_switcher_data'),
110 route_path('goto_switcher_data'),
111 extra_environ=xhr_header, status=200)
111 extra_environ=xhr_header, status=200)
112 result = json.loads(response.body)['suggestions']
112 result = json.loads(response.body)['suggestions']
113
113
114 assert result == []
114 assert result == []
115
115
116 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
116 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
117 self.log_user()
117 self.log_user()
118
118
119 response = self.app.get(
119 response = self.app.get(
120 route_path('goto_switcher_data'),
120 route_path('goto_switcher_data'),
121 params={'query': 'abc'},
121 params={'query': 'abc'},
122 extra_environ=xhr_header, status=200)
122 extra_environ=xhr_header, status=200)
123 result = json.loads(response.body)['suggestions']
123 result = json.loads(response.body)['suggestions']
124
124
125 repos, groups, users, commits = assert_and_get_main_filter_content(result)
125 repos, groups, users, commits = assert_and_get_main_filter_content(result)
126
126
127 assert len(repos) == 13
127 assert len(repos) == 13
128 assert len(groups) == 5
128 assert len(groups) == 5
129 assert len(users) == 0
129 assert len(users) == 0
130 assert len(commits) == 0
130 assert len(commits) == 0
131
131
132 def test_returns_list_of_users_filtered(self, xhr_header):
132 def test_returns_list_of_users_filtered(self, xhr_header):
133 self.log_user()
133 self.log_user()
134
134
135 response = self.app.get(
135 response = self.app.get(
136 route_path('goto_switcher_data'),
136 route_path('goto_switcher_data'),
137 params={'query': 'user:admin'},
137 params={'query': 'user:admin'},
138 extra_environ=xhr_header, status=200)
138 extra_environ=xhr_header, status=200)
139 result = json.loads(response.body)['suggestions']
139 result = json.loads(response.body)['suggestions']
140
140
141 repos, groups, users, commits = assert_and_get_main_filter_content(result)
141 repos, groups, users, commits = assert_and_get_main_filter_content(result)
142
142
143 assert len(repos) == 0
143 assert len(repos) == 0
144 assert len(groups) == 0
144 assert len(groups) == 0
145 assert len(users) == 1
145 assert len(users) == 1
146 assert len(commits) == 0
146 assert len(commits) == 0
147
147
148 def test_returns_list_of_commits_filtered(self, xhr_header):
148 def test_returns_list_of_commits_filtered(self, xhr_header):
149 self.log_user()
149 self.log_user()
150
150
151 response = self.app.get(
151 response = self.app.get(
152 route_path('goto_switcher_data'),
152 route_path('goto_switcher_data'),
153 params={'query': 'commit:e8'},
153 params={'query': 'commit:e8'},
154 extra_environ=xhr_header, status=200)
154 extra_environ=xhr_header, status=200)
155 result = json.loads(response.body)['suggestions']
155 result = json.loads(response.body)['suggestions']
156
156
157 repos, groups, users, commits = assert_and_get_main_filter_content(result)
157 repos, groups, users, commits = assert_and_get_main_filter_content(result)
158
158
159 assert len(repos) == 0
159 assert len(repos) == 0
160 assert len(groups) == 0
160 assert len(groups) == 0
161 assert len(users) == 0
161 assert len(users) == 0
162 assert len(commits) == 5
162 assert len(commits) == 5
163
163
164 def test_returns_list_of_properly_sorted_and_filtered(self, xhr_header):
164 def test_returns_list_of_properly_sorted_and_filtered(self, xhr_header):
165 self.log_user()
165 self.log_user()
166
166
167 response = self.app.get(
167 response = self.app.get(
168 route_path('goto_switcher_data'),
168 route_path('goto_switcher_data'),
169 params={'query': 'abc'},
169 params={'query': 'abc'},
170 extra_environ=xhr_header, status=200)
170 extra_environ=xhr_header, status=200)
171 result = json.loads(response.body)['suggestions']
171 result = json.loads(response.body)['suggestions']
172
172
173 repos, groups, users, commits = assert_and_get_main_filter_content(result)
173 repos, groups, users, commits = assert_and_get_main_filter_content(result)
174
174
175 test_repos = [x['value_display'] for x in repos[:4]]
175 test_repos = [x['value_display'] for x in repos[:4]]
176 assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos
176 assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos
177
177
178 test_groups = [x['value_display'] for x in groups[:4]]
178 test_groups = [x['value_display'] for x in groups[:4]]
179 assert ['abc_repos', 'repos_abc',
179 assert ['abc_repos', 'repos_abc',
180 'forked-abc', 'forked-abc/a'] == test_groups
180 'forked-abc', 'forked-abc/a'] == test_groups
@@ -1,95 +1,95 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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
22
23 from . import assert_and_get_repo_list_content
23 from . import assert_and_get_repo_list_content
24 from rhodecode.tests import TestController
24 from rhodecode.tests import TestController
25 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.model.db import Repository
26 from rhodecode.model.db import Repository
27
27
28 fixture = Fixture()
28 fixture = Fixture()
29
29
30
30
31 def route_path(name, params=None, **kwargs):
31 def route_path(name, params=None, **kwargs):
32 import urllib
32 import urllib.request, urllib.parse, urllib.error
33
33
34 base_url = {
34 base_url = {
35 'repo_list_data': '/_repos',
35 'repo_list_data': '/_repos',
36 }[name].format(**kwargs)
36 }[name].format(**kwargs)
37
37
38 if params:
38 if params:
39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
39 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 return base_url
40 return base_url
41
41
42
42
43 class TestRepoListData(TestController):
43 class TestRepoListData(TestController):
44
44
45 def test_returns_list_of_repos_and_groups(self, xhr_header):
45 def test_returns_list_of_repos_and_groups(self, xhr_header):
46 self.log_user()
46 self.log_user()
47
47
48 response = self.app.get(
48 response = self.app.get(
49 route_path('repo_list_data'),
49 route_path('repo_list_data'),
50 extra_environ=xhr_header, status=200)
50 extra_environ=xhr_header, status=200)
51 result = json.loads(response.body)['results']
51 result = json.loads(response.body)['results']
52
52
53 repos = assert_and_get_repo_list_content(result)
53 repos = assert_and_get_repo_list_content(result)
54
54
55 assert len(repos) == len(Repository.get_all())
55 assert len(repos) == len(Repository.get_all())
56
56
57 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
57 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
58 self.log_user()
58 self.log_user()
59
59
60 response = self.app.get(
60 response = self.app.get(
61 route_path('repo_list_data'),
61 route_path('repo_list_data'),
62 params={'query': 'vcs_test_git'},
62 params={'query': 'vcs_test_git'},
63 extra_environ=xhr_header, status=200)
63 extra_environ=xhr_header, status=200)
64 result = json.loads(response.body)['results']
64 result = json.loads(response.body)['results']
65
65
66 repos = assert_and_get_repo_list_content(result)
66 repos = assert_and_get_repo_list_content(result)
67
67
68 assert len(repos) == len(Repository.query().filter(
68 assert len(repos) == len(Repository.query().filter(
69 Repository.repo_name.ilike('%vcs_test_git%')).all())
69 Repository.repo_name.ilike('%vcs_test_git%')).all())
70
70
71 def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header):
71 def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header):
72 self.log_user()
72 self.log_user()
73
73
74 response = self.app.get(
74 response = self.app.get(
75 route_path('repo_list_data'),
75 route_path('repo_list_data'),
76 params={'query': 'vcs_test_git', 'repo_type': 'git'},
76 params={'query': 'vcs_test_git', 'repo_type': 'git'},
77 extra_environ=xhr_header, status=200)
77 extra_environ=xhr_header, status=200)
78 result = json.loads(response.body)['results']
78 result = json.loads(response.body)['results']
79
79
80 repos = assert_and_get_repo_list_content(result)
80 repos = assert_and_get_repo_list_content(result)
81
81
82 assert len(repos) == len(Repository.query().filter(
82 assert len(repos) == len(Repository.query().filter(
83 Repository.repo_name.ilike('%vcs_test_git%')).all())
83 Repository.repo_name.ilike('%vcs_test_git%')).all())
84
84
85 def test_returns_list_of_repos_non_ascii_query(self, xhr_header):
85 def test_returns_list_of_repos_non_ascii_query(self, xhr_header):
86 self.log_user()
86 self.log_user()
87 response = self.app.get(
87 response = self.app.get(
88 route_path('repo_list_data'),
88 route_path('repo_list_data'),
89 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'},
89 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'},
90 extra_environ=xhr_header, status=200)
90 extra_environ=xhr_header, status=200)
91 result = json.loads(response.body)['results']
91 result = json.loads(response.body)['results']
92
92
93 repos = assert_and_get_repo_list_content(result)
93 repos = assert_and_get_repo_list_content(result)
94
94
95 assert len(repos) == 0
95 assert len(repos) == 0
@@ -1,112 +1,112 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 pytest
22 import pytest
23
23
24 from rhodecode.tests import TestController
24 from rhodecode.tests import TestController
25 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
26
26
27
27
28 fixture = Fixture()
28 fixture = Fixture()
29
29
30
30
31 def route_path(name, params=None, **kwargs):
31 def route_path(name, params=None, **kwargs):
32 import urllib
32 import urllib.request, urllib.parse, urllib.error
33
33
34 base_url = {
34 base_url = {
35 'user_autocomplete_data': '/_users',
35 'user_autocomplete_data': '/_users',
36 'user_group_autocomplete_data': '/_user_groups'
36 'user_group_autocomplete_data': '/_user_groups'
37 }[name].format(**kwargs)
37 }[name].format(**kwargs)
38
38
39 if params:
39 if params:
40 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 return base_url
41 return base_url
42
42
43
43
44 class TestUserAutocompleteData(TestController):
44 class TestUserAutocompleteData(TestController):
45
45
46 def test_returns_list_of_users(self, user_util, xhr_header):
46 def test_returns_list_of_users(self, user_util, xhr_header):
47 self.log_user()
47 self.log_user()
48 user = user_util.create_user(active=True)
48 user = user_util.create_user(active=True)
49 user_name = user.username
49 user_name = user.username
50 response = self.app.get(
50 response = self.app.get(
51 route_path('user_autocomplete_data'),
51 route_path('user_autocomplete_data'),
52 extra_environ=xhr_header, status=200)
52 extra_environ=xhr_header, status=200)
53 result = json.loads(response.body)
53 result = json.loads(response.body)
54 values = [suggestion['value'] for suggestion in result['suggestions']]
54 values = [suggestion['value'] for suggestion in result['suggestions']]
55 assert user_name in values
55 assert user_name in values
56
56
57 def test_returns_inactive_users_when_active_flag_sent(
57 def test_returns_inactive_users_when_active_flag_sent(
58 self, user_util, xhr_header):
58 self, user_util, xhr_header):
59 self.log_user()
59 self.log_user()
60 user = user_util.create_user(active=False)
60 user = user_util.create_user(active=False)
61 user_name = user.username
61 user_name = user.username
62
62
63 response = self.app.get(
63 response = self.app.get(
64 route_path('user_autocomplete_data',
64 route_path('user_autocomplete_data',
65 params=dict(user_groups='true', active='0')),
65 params=dict(user_groups='true', active='0')),
66 extra_environ=xhr_header, status=200)
66 extra_environ=xhr_header, status=200)
67 result = json.loads(response.body)
67 result = json.loads(response.body)
68 values = [suggestion['value'] for suggestion in result['suggestions']]
68 values = [suggestion['value'] for suggestion in result['suggestions']]
69 assert user_name in values
69 assert user_name in values
70
70
71 response = self.app.get(
71 response = self.app.get(
72 route_path('user_autocomplete_data',
72 route_path('user_autocomplete_data',
73 params=dict(user_groups='true', active='1')),
73 params=dict(user_groups='true', active='1')),
74 extra_environ=xhr_header, status=200)
74 extra_environ=xhr_header, status=200)
75 result = json.loads(response.body)
75 result = json.loads(response.body)
76 values = [suggestion['value'] for suggestion in result['suggestions']]
76 values = [suggestion['value'] for suggestion in result['suggestions']]
77 assert user_name not in values
77 assert user_name not in values
78
78
79 def test_returns_groups_when_user_groups_flag_sent(
79 def test_returns_groups_when_user_groups_flag_sent(
80 self, user_util, xhr_header):
80 self, user_util, xhr_header):
81 self.log_user()
81 self.log_user()
82 group = user_util.create_user_group(user_groups_active=True)
82 group = user_util.create_user_group(user_groups_active=True)
83 group_name = group.users_group_name
83 group_name = group.users_group_name
84 response = self.app.get(
84 response = self.app.get(
85 route_path('user_autocomplete_data',
85 route_path('user_autocomplete_data',
86 params=dict(user_groups='true')),
86 params=dict(user_groups='true')),
87 extra_environ=xhr_header, status=200)
87 extra_environ=xhr_header, status=200)
88 result = json.loads(response.body)
88 result = json.loads(response.body)
89 values = [suggestion['value'] for suggestion in result['suggestions']]
89 values = [suggestion['value'] for suggestion in result['suggestions']]
90 assert group_name in values
90 assert group_name in values
91
91
92 @pytest.mark.parametrize('query, count', [
92 @pytest.mark.parametrize('query, count', [
93 ('hello1', 0),
93 ('hello1', 0),
94 ('dev', 2),
94 ('dev', 2),
95 ])
95 ])
96 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header,
96 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header,
97 query, count):
97 query, count):
98 self.log_user()
98 self.log_user()
99
99
100 user_util._test_name = 'dev-test'
100 user_util._test_name = 'dev-test'
101 user_util.create_user()
101 user_util.create_user()
102
102
103 user_util._test_name = 'dev-group-test'
103 user_util._test_name = 'dev-group-test'
104 user_util.create_user_group()
104 user_util.create_user_group()
105
105
106 response = self.app.get(
106 response = self.app.get(
107 route_path('user_autocomplete_data',
107 route_path('user_autocomplete_data',
108 params=dict(user_groups='true', query=query)),
108 params=dict(user_groups='true', query=query)),
109 extra_environ=xhr_header, status=200)
109 extra_environ=xhr_header, status=200)
110
110
111 result = json.loads(response.body)
111 result = json.loads(response.body)
112 assert len(result['suggestions']) == count
112 assert len(result['suggestions']) == count
@@ -1,117 +1,117 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 # -*- coding: utf-8 -*-
20 # -*- coding: utf-8 -*-
21
21
22 # Copyright (C) 2016-2020 RhodeCode GmbH
22 # Copyright (C) 2016-2020 RhodeCode GmbH
23 #
23 #
24 # This program is free software: you can redistribute it and/or modify
24 # This program is free software: you can redistribute it and/or modify
25 # it under the terms of the GNU Affero General Public License, version 3
25 # it under the terms of the GNU Affero General Public License, version 3
26 # (only), as published by the Free Software Foundation.
26 # (only), as published by the Free Software Foundation.
27 #
27 #
28 # This program is distributed in the hope that it will be useful,
28 # This program is distributed in the hope that it will be useful,
29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 # GNU General Public License for more details.
31 # GNU General Public License for more details.
32 #
32 #
33 # You should have received a copy of the GNU Affero General Public License
33 # You should have received a copy of the GNU Affero General Public License
34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 #
35 #
36 # This program is dual-licensed. If you wish to learn more about the
36 # This program is dual-licensed. If you wish to learn more about the
37 # RhodeCode Enterprise Edition, including its added features, Support services,
37 # RhodeCode Enterprise Edition, including its added features, Support services,
38 # and proprietary license terms, please see https://rhodecode.com/licenses/
38 # and proprietary license terms, please see https://rhodecode.com/licenses/
39
39
40 import json
40 import json
41
41
42 import pytest
42 import pytest
43
43
44 from rhodecode.tests import TestController
44 from rhodecode.tests import TestController
45 from rhodecode.tests.fixture import Fixture
45 from rhodecode.tests.fixture import Fixture
46
46
47
47
48 fixture = Fixture()
48 fixture = Fixture()
49
49
50
50
51 def route_path(name, params=None, **kwargs):
51 def route_path(name, params=None, **kwargs):
52 import urllib
52 import urllib.request, urllib.parse, urllib.error
53
53
54 base_url = {
54 base_url = {
55 'user_autocomplete_data': '/_users',
55 'user_autocomplete_data': '/_users',
56 'user_group_autocomplete_data': '/_user_groups'
56 'user_group_autocomplete_data': '/_user_groups'
57 }[name].format(**kwargs)
57 }[name].format(**kwargs)
58
58
59 if params:
59 if params:
60 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
61 return base_url
61 return base_url
62
62
63
63
64 class TestUserGroupAutocompleteData(TestController):
64 class TestUserGroupAutocompleteData(TestController):
65
65
66 def test_returns_list_of_user_groups(self, user_util, xhr_header):
66 def test_returns_list_of_user_groups(self, user_util, xhr_header):
67 self.log_user()
67 self.log_user()
68 user_group = user_util.create_user_group(active=True)
68 user_group = user_util.create_user_group(active=True)
69 user_group_name = user_group.users_group_name
69 user_group_name = user_group.users_group_name
70 response = self.app.get(
70 response = self.app.get(
71 route_path('user_group_autocomplete_data'),
71 route_path('user_group_autocomplete_data'),
72 extra_environ=xhr_header, status=200)
72 extra_environ=xhr_header, status=200)
73 result = json.loads(response.body)
73 result = json.loads(response.body)
74 values = [suggestion['value'] for suggestion in result['suggestions']]
74 values = [suggestion['value'] for suggestion in result['suggestions']]
75 assert user_group_name in values
75 assert user_group_name in values
76
76
77 def test_returns_inactive_user_groups_when_active_flag_sent(
77 def test_returns_inactive_user_groups_when_active_flag_sent(
78 self, user_util, xhr_header):
78 self, user_util, xhr_header):
79 self.log_user()
79 self.log_user()
80 user_group = user_util.create_user_group(active=False)
80 user_group = user_util.create_user_group(active=False)
81 user_group_name = user_group.users_group_name
81 user_group_name = user_group.users_group_name
82
82
83 response = self.app.get(
83 response = self.app.get(
84 route_path('user_group_autocomplete_data',
84 route_path('user_group_autocomplete_data',
85 params=dict(active='0')),
85 params=dict(active='0')),
86 extra_environ=xhr_header, status=200)
86 extra_environ=xhr_header, status=200)
87 result = json.loads(response.body)
87 result = json.loads(response.body)
88 values = [suggestion['value'] for suggestion in result['suggestions']]
88 values = [suggestion['value'] for suggestion in result['suggestions']]
89 assert user_group_name in values
89 assert user_group_name in values
90
90
91 response = self.app.get(
91 response = self.app.get(
92 route_path('user_group_autocomplete_data',
92 route_path('user_group_autocomplete_data',
93 params=dict(active='1')),
93 params=dict(active='1')),
94 extra_environ=xhr_header, status=200)
94 extra_environ=xhr_header, status=200)
95 result = json.loads(response.body)
95 result = json.loads(response.body)
96 values = [suggestion['value'] for suggestion in result['suggestions']]
96 values = [suggestion['value'] for suggestion in result['suggestions']]
97 assert user_group_name not in values
97 assert user_group_name not in values
98
98
99 @pytest.mark.parametrize('query, count', [
99 @pytest.mark.parametrize('query, count', [
100 ('hello1', 0),
100 ('hello1', 0),
101 ('dev', 1),
101 ('dev', 1),
102 ])
102 ])
103 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count):
103 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count):
104 self.log_user()
104 self.log_user()
105
105
106 user_util._test_name = 'dev-test'
106 user_util._test_name = 'dev-test'
107 user_util.create_user_group()
107 user_util.create_user_group()
108
108
109 response = self.app.get(
109 response = self.app.get(
110 route_path('user_group_autocomplete_data',
110 route_path('user_group_autocomplete_data',
111 params=dict(user_groups='true',
111 params=dict(user_groups='true',
112 query=query)),
112 query=query)),
113 extra_environ=xhr_header, status=200)
113 extra_environ=xhr_header, status=200)
114
114
115 result = json.loads(response.body)
115 result = json.loads(response.body)
116
116
117 assert len(result['suggestions']) == count
117 assert len(result['suggestions']) == count
@@ -1,106 +1,106 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 datetime
21 import datetime
22
22
23 import pytest
23 import pytest
24
24
25 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.apps._base import ADMIN_PREFIX
26 from rhodecode.tests import TestController
26 from rhodecode.tests import TestController
27 from rhodecode.model.db import UserFollowing, Repository
27 from rhodecode.model.db import UserFollowing, Repository
28
28
29
29
30 def route_path(name, params=None, **kwargs):
30 def route_path(name, params=None, **kwargs):
31 import urllib
31 import urllib.request, urllib.parse, urllib.error
32
32
33 base_url = {
33 base_url = {
34 'journal': ADMIN_PREFIX + '/journal',
34 'journal': ADMIN_PREFIX + '/journal',
35 'journal_rss': ADMIN_PREFIX + '/journal/rss',
35 'journal_rss': ADMIN_PREFIX + '/journal/rss',
36 'journal_atom': ADMIN_PREFIX + '/journal/atom',
36 'journal_atom': ADMIN_PREFIX + '/journal/atom',
37 'journal_public': ADMIN_PREFIX + '/public_journal',
37 'journal_public': ADMIN_PREFIX + '/public_journal',
38 'journal_public_atom': ADMIN_PREFIX + '/public_journal/atom',
38 'journal_public_atom': ADMIN_PREFIX + '/public_journal/atom',
39 'journal_public_atom_old': ADMIN_PREFIX + '/public_journal_atom',
39 'journal_public_atom_old': ADMIN_PREFIX + '/public_journal_atom',
40 'journal_public_rss': ADMIN_PREFIX + '/public_journal/rss',
40 'journal_public_rss': ADMIN_PREFIX + '/public_journal/rss',
41 'journal_public_rss_old': ADMIN_PREFIX + '/public_journal_rss',
41 'journal_public_rss_old': ADMIN_PREFIX + '/public_journal_rss',
42 'toggle_following': ADMIN_PREFIX + '/toggle_following',
42 'toggle_following': ADMIN_PREFIX + '/toggle_following',
43 }[name].format(**kwargs)
43 }[name].format(**kwargs)
44
44
45 if params:
45 if params:
46 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
47 return base_url
47 return base_url
48
48
49
49
50 class TestJournalViews(TestController):
50 class TestJournalViews(TestController):
51
51
52 def test_journal(self):
52 def test_journal(self):
53 self.log_user()
53 self.log_user()
54 response = self.app.get(route_path('journal'))
54 response = self.app.get(route_path('journal'))
55 # response.mustcontain(
55 # response.mustcontain(
56 # """<div class="journal_day">%s</div>""" % datetime.date.today())
56 # """<div class="journal_day">%s</div>""" % datetime.date.today())
57
57
58 @pytest.mark.parametrize("feed_type, content_type", [
58 @pytest.mark.parametrize("feed_type, content_type", [
59 ('rss', "application/rss+xml"),
59 ('rss', "application/rss+xml"),
60 ('atom', "application/atom+xml")
60 ('atom', "application/atom+xml")
61 ])
61 ])
62 def test_journal_feed(self, feed_type, content_type):
62 def test_journal_feed(self, feed_type, content_type):
63 self.log_user()
63 self.log_user()
64 response = self.app.get(
64 response = self.app.get(
65 route_path(
65 route_path(
66 'journal_{}'.format(feed_type)),
66 'journal_{}'.format(feed_type)),
67 status=200)
67 status=200)
68
68
69 assert response.content_type == content_type
69 assert response.content_type == content_type
70
70
71 def test_toggle_following_repository(self, backend):
71 def test_toggle_following_repository(self, backend):
72 user = self.log_user()
72 user = self.log_user()
73 repo = Repository.get_by_repo_name(backend.repo_name)
73 repo = Repository.get_by_repo_name(backend.repo_name)
74 repo_id = repo.repo_id
74 repo_id = repo.repo_id
75 self.app.post(
75 self.app.post(
76 route_path('toggle_following'), {'follows_repo_id': repo_id,
76 route_path('toggle_following'), {'follows_repo_id': repo_id,
77 'csrf_token': self.csrf_token})
77 'csrf_token': self.csrf_token})
78
78
79 followings = UserFollowing.query()\
79 followings = UserFollowing.query()\
80 .filter(UserFollowing.user_id == user['user_id'])\
80 .filter(UserFollowing.user_id == user['user_id'])\
81 .filter(UserFollowing.follows_repo_id == repo_id).all()
81 .filter(UserFollowing.follows_repo_id == repo_id).all()
82
82
83 assert len(followings) == 0
83 assert len(followings) == 0
84
84
85 self.app.post(
85 self.app.post(
86 route_path('toggle_following'), {'follows_repo_id': repo_id,
86 route_path('toggle_following'), {'follows_repo_id': repo_id,
87 'csrf_token': self.csrf_token})
87 'csrf_token': self.csrf_token})
88
88
89 followings = UserFollowing.query()\
89 followings = UserFollowing.query()\
90 .filter(UserFollowing.user_id == user['user_id'])\
90 .filter(UserFollowing.user_id == user['user_id'])\
91 .filter(UserFollowing.follows_repo_id == repo_id).all()
91 .filter(UserFollowing.follows_repo_id == repo_id).all()
92
92
93 assert len(followings) == 1
93 assert len(followings) == 1
94
94
95 @pytest.mark.parametrize("feed_type, content_type", [
95 @pytest.mark.parametrize("feed_type, content_type", [
96 ('rss', "application/rss+xml"),
96 ('rss', "application/rss+xml"),
97 ('atom', "application/atom+xml")
97 ('atom', "application/atom+xml")
98 ])
98 ])
99 def test_public_journal_feed(self, feed_type, content_type):
99 def test_public_journal_feed(self, feed_type, content_type):
100 self.log_user()
100 self.log_user()
101 response = self.app.get(
101 response = self.app.get(
102 route_path(
102 route_path(
103 'journal_public_{}'.format(feed_type)),
103 'journal_public_{}'.format(feed_type)),
104 status=200)
104 status=200)
105
105
106 assert response.content_type == content_type
106 assert response.content_type == content_type
@@ -1,580 +1,580 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 urlparse
21 import urlparse
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.tests import (
26 from rhodecode.tests import (
27 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
27 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
28 no_newline_id_generator)
28 no_newline_id_generator)
29 from rhodecode.tests.fixture import Fixture
29 from rhodecode.tests.fixture import Fixture
30 from rhodecode.lib.auth import check_password
30 from rhodecode.lib.auth import check_password
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.model.auth_token import AuthTokenModel
32 from rhodecode.model.auth_token import AuthTokenModel
33 from rhodecode.model.db import User, Notification, UserApiKeys
33 from rhodecode.model.db import User, Notification, UserApiKeys
34 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
35
35
36 fixture = Fixture()
36 fixture = Fixture()
37
37
38 whitelist_view = ['RepoCommitsView:repo_commit_raw']
38 whitelist_view = ['RepoCommitsView:repo_commit_raw']
39
39
40
40
41 def route_path(name, params=None, **kwargs):
41 def route_path(name, params=None, **kwargs):
42 import urllib
42 import urllib.request, urllib.parse, urllib.error
43 from rhodecode.apps._base import ADMIN_PREFIX
43 from rhodecode.apps._base import ADMIN_PREFIX
44
44
45 base_url = {
45 base_url = {
46 'login': ADMIN_PREFIX + '/login',
46 'login': ADMIN_PREFIX + '/login',
47 'logout': ADMIN_PREFIX + '/logout',
47 'logout': ADMIN_PREFIX + '/logout',
48 'register': ADMIN_PREFIX + '/register',
48 'register': ADMIN_PREFIX + '/register',
49 'reset_password':
49 'reset_password':
50 ADMIN_PREFIX + '/password_reset',
50 ADMIN_PREFIX + '/password_reset',
51 'reset_password_confirmation':
51 'reset_password_confirmation':
52 ADMIN_PREFIX + '/password_reset_confirmation',
52 ADMIN_PREFIX + '/password_reset_confirmation',
53
53
54 'admin_permissions_application':
54 'admin_permissions_application':
55 ADMIN_PREFIX + '/permissions/application',
55 ADMIN_PREFIX + '/permissions/application',
56 'admin_permissions_application_update':
56 'admin_permissions_application_update':
57 ADMIN_PREFIX + '/permissions/application/update',
57 ADMIN_PREFIX + '/permissions/application/update',
58
58
59 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
59 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
60
60
61 }[name].format(**kwargs)
61 }[name].format(**kwargs)
62
62
63 if params:
63 if params:
64 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
64 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
65 return base_url
65 return base_url
66
66
67
67
68 @pytest.mark.usefixtures('app')
68 @pytest.mark.usefixtures('app')
69 class TestLoginController(object):
69 class TestLoginController(object):
70 destroy_users = set()
70 destroy_users = set()
71
71
72 @classmethod
72 @classmethod
73 def teardown_class(cls):
73 def teardown_class(cls):
74 fixture.destroy_users(cls.destroy_users)
74 fixture.destroy_users(cls.destroy_users)
75
75
76 def teardown_method(self, method):
76 def teardown_method(self, method):
77 for n in Notification.query().all():
77 for n in Notification.query().all():
78 Session().delete(n)
78 Session().delete(n)
79
79
80 Session().commit()
80 Session().commit()
81 assert Notification.query().all() == []
81 assert Notification.query().all() == []
82
82
83 def test_index(self):
83 def test_index(self):
84 response = self.app.get(route_path('login'))
84 response = self.app.get(route_path('login'))
85 assert response.status == '200 OK'
85 assert response.status == '200 OK'
86 # Test response...
86 # Test response...
87
87
88 def test_login_admin_ok(self):
88 def test_login_admin_ok(self):
89 response = self.app.post(route_path('login'),
89 response = self.app.post(route_path('login'),
90 {'username': 'test_admin',
90 {'username': 'test_admin',
91 'password': 'test12'}, status=302)
91 'password': 'test12'}, status=302)
92 response = response.follow()
92 response = response.follow()
93 session = response.get_session_from_response()
93 session = response.get_session_from_response()
94 username = session['rhodecode_user'].get('username')
94 username = session['rhodecode_user'].get('username')
95 assert username == 'test_admin'
95 assert username == 'test_admin'
96 response.mustcontain('logout')
96 response.mustcontain('logout')
97
97
98 def test_login_regular_ok(self):
98 def test_login_regular_ok(self):
99 response = self.app.post(route_path('login'),
99 response = self.app.post(route_path('login'),
100 {'username': 'test_regular',
100 {'username': 'test_regular',
101 'password': 'test12'}, status=302)
101 'password': 'test12'}, status=302)
102
102
103 response = response.follow()
103 response = response.follow()
104 session = response.get_session_from_response()
104 session = response.get_session_from_response()
105 username = session['rhodecode_user'].get('username')
105 username = session['rhodecode_user'].get('username')
106 assert username == 'test_regular'
106 assert username == 'test_regular'
107 response.mustcontain('logout')
107 response.mustcontain('logout')
108
108
109 def test_login_regular_forbidden_when_super_admin_restriction(self):
109 def test_login_regular_forbidden_when_super_admin_restriction(self):
110 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
110 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
111 with fixture.auth_restriction(self.app._pyramid_registry,
111 with fixture.auth_restriction(self.app._pyramid_registry,
112 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
112 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
113 response = self.app.post(route_path('login'),
113 response = self.app.post(route_path('login'),
114 {'username': 'test_regular',
114 {'username': 'test_regular',
115 'password': 'test12'})
115 'password': 'test12'})
116
116
117 response.mustcontain('invalid user name')
117 response.mustcontain('invalid user name')
118 response.mustcontain('invalid password')
118 response.mustcontain('invalid password')
119
119
120 def test_login_regular_forbidden_when_scope_restriction(self):
120 def test_login_regular_forbidden_when_scope_restriction(self):
121 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
121 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
122 with fixture.scope_restriction(self.app._pyramid_registry,
122 with fixture.scope_restriction(self.app._pyramid_registry,
123 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
123 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
124 response = self.app.post(route_path('login'),
124 response = self.app.post(route_path('login'),
125 {'username': 'test_regular',
125 {'username': 'test_regular',
126 'password': 'test12'})
126 'password': 'test12'})
127
127
128 response.mustcontain('invalid user name')
128 response.mustcontain('invalid user name')
129 response.mustcontain('invalid password')
129 response.mustcontain('invalid password')
130
130
131 def test_login_ok_came_from(self):
131 def test_login_ok_came_from(self):
132 test_came_from = '/_admin/users?branch=stable'
132 test_came_from = '/_admin/users?branch=stable'
133 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
133 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
134 response = self.app.post(
134 response = self.app.post(
135 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
135 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
136
136
137 assert 'branch=stable' in response.location
137 assert 'branch=stable' in response.location
138 response = response.follow()
138 response = response.follow()
139
139
140 assert response.status == '200 OK'
140 assert response.status == '200 OK'
141 response.mustcontain('Users administration')
141 response.mustcontain('Users administration')
142
142
143 def test_redirect_to_login_with_get_args(self):
143 def test_redirect_to_login_with_get_args(self):
144 with fixture.anon_access(False):
144 with fixture.anon_access(False):
145 kwargs = {'branch': 'stable'}
145 kwargs = {'branch': 'stable'}
146 response = self.app.get(
146 response = self.app.get(
147 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
147 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
148 status=302)
148 status=302)
149
149
150 response_query = urlparse.parse_qsl(response.location)
150 response_query = urlparse.parse_qsl(response.location)
151 assert 'branch=stable' in response_query[0][1]
151 assert 'branch=stable' in response_query[0][1]
152
152
153 def test_login_form_with_get_args(self):
153 def test_login_form_with_get_args(self):
154 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
154 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
155 response = self.app.get(_url)
155 response = self.app.get(_url)
156 assert 'branch%3Dstable' in response.form.action
156 assert 'branch%3Dstable' in response.form.action
157
157
158 @pytest.mark.parametrize("url_came_from", [
158 @pytest.mark.parametrize("url_came_from", [
159 'data:text/html,<script>window.alert("xss")</script>',
159 'data:text/html,<script>window.alert("xss")</script>',
160 'mailto:test@rhodecode.org',
160 'mailto:test@rhodecode.org',
161 'file:///etc/passwd',
161 'file:///etc/passwd',
162 'ftp://some.ftp.server',
162 'ftp://some.ftp.server',
163 'http://other.domain',
163 'http://other.domain',
164 '/\r\nX-Forwarded-Host: http://example.org',
164 '/\r\nX-Forwarded-Host: http://example.org',
165 ], ids=no_newline_id_generator)
165 ], ids=no_newline_id_generator)
166 def test_login_bad_came_froms(self, url_came_from):
166 def test_login_bad_came_froms(self, url_came_from):
167 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
167 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
168 response = self.app.post(
168 response = self.app.post(
169 _url,
169 _url,
170 {'username': 'test_admin', 'password': 'test12'})
170 {'username': 'test_admin', 'password': 'test12'})
171 assert response.status == '302 Found'
171 assert response.status == '302 Found'
172 response = response.follow()
172 response = response.follow()
173 assert response.status == '200 OK'
173 assert response.status == '200 OK'
174 assert response.request.path == '/'
174 assert response.request.path == '/'
175
175
176 def test_login_short_password(self):
176 def test_login_short_password(self):
177 response = self.app.post(route_path('login'),
177 response = self.app.post(route_path('login'),
178 {'username': 'test_admin',
178 {'username': 'test_admin',
179 'password': 'as'})
179 'password': 'as'})
180 assert response.status == '200 OK'
180 assert response.status == '200 OK'
181
181
182 response.mustcontain('Enter 3 characters or more')
182 response.mustcontain('Enter 3 characters or more')
183
183
184 def test_login_wrong_non_ascii_password(self, user_regular):
184 def test_login_wrong_non_ascii_password(self, user_regular):
185 response = self.app.post(
185 response = self.app.post(
186 route_path('login'),
186 route_path('login'),
187 {'username': user_regular.username,
187 {'username': user_regular.username,
188 'password': u'invalid-non-asci\xe4'.encode('utf8')})
188 'password': u'invalid-non-asci\xe4'.encode('utf8')})
189
189
190 response.mustcontain('invalid user name')
190 response.mustcontain('invalid user name')
191 response.mustcontain('invalid password')
191 response.mustcontain('invalid password')
192
192
193 def test_login_with_non_ascii_password(self, user_util):
193 def test_login_with_non_ascii_password(self, user_util):
194 password = u'valid-non-ascii\xe4'
194 password = u'valid-non-ascii\xe4'
195 user = user_util.create_user(password=password)
195 user = user_util.create_user(password=password)
196 response = self.app.post(
196 response = self.app.post(
197 route_path('login'),
197 route_path('login'),
198 {'username': user.username,
198 {'username': user.username,
199 'password': password.encode('utf-8')})
199 'password': password.encode('utf-8')})
200 assert response.status_code == 302
200 assert response.status_code == 302
201
201
202 def test_login_wrong_username_password(self):
202 def test_login_wrong_username_password(self):
203 response = self.app.post(route_path('login'),
203 response = self.app.post(route_path('login'),
204 {'username': 'error',
204 {'username': 'error',
205 'password': 'test12'})
205 'password': 'test12'})
206
206
207 response.mustcontain('invalid user name')
207 response.mustcontain('invalid user name')
208 response.mustcontain('invalid password')
208 response.mustcontain('invalid password')
209
209
210 def test_login_admin_ok_password_migration(self, real_crypto_backend):
210 def test_login_admin_ok_password_migration(self, real_crypto_backend):
211 from rhodecode.lib import auth
211 from rhodecode.lib import auth
212
212
213 # create new user, with sha256 password
213 # create new user, with sha256 password
214 temp_user = 'test_admin_sha256'
214 temp_user = 'test_admin_sha256'
215 user = fixture.create_user(temp_user)
215 user = fixture.create_user(temp_user)
216 user.password = auth._RhodeCodeCryptoSha256().hash_create(
216 user.password = auth._RhodeCodeCryptoSha256().hash_create(
217 b'test123')
217 b'test123')
218 Session().add(user)
218 Session().add(user)
219 Session().commit()
219 Session().commit()
220 self.destroy_users.add(temp_user)
220 self.destroy_users.add(temp_user)
221 response = self.app.post(route_path('login'),
221 response = self.app.post(route_path('login'),
222 {'username': temp_user,
222 {'username': temp_user,
223 'password': 'test123'}, status=302)
223 'password': 'test123'}, status=302)
224
224
225 response = response.follow()
225 response = response.follow()
226 session = response.get_session_from_response()
226 session = response.get_session_from_response()
227 username = session['rhodecode_user'].get('username')
227 username = session['rhodecode_user'].get('username')
228 assert username == temp_user
228 assert username == temp_user
229 response.mustcontain('logout')
229 response.mustcontain('logout')
230
230
231 # new password should be bcrypted, after log-in and transfer
231 # new password should be bcrypted, after log-in and transfer
232 user = User.get_by_username(temp_user)
232 user = User.get_by_username(temp_user)
233 assert user.password.startswith('$')
233 assert user.password.startswith('$')
234
234
235 # REGISTRATIONS
235 # REGISTRATIONS
236 def test_register(self):
236 def test_register(self):
237 response = self.app.get(route_path('register'))
237 response = self.app.get(route_path('register'))
238 response.mustcontain('Create an Account')
238 response.mustcontain('Create an Account')
239
239
240 def test_register_err_same_username(self):
240 def test_register_err_same_username(self):
241 uname = 'test_admin'
241 uname = 'test_admin'
242 response = self.app.post(
242 response = self.app.post(
243 route_path('register'),
243 route_path('register'),
244 {
244 {
245 'username': uname,
245 'username': uname,
246 'password': 'test12',
246 'password': 'test12',
247 'password_confirmation': 'test12',
247 'password_confirmation': 'test12',
248 'email': 'goodmail@domain.com',
248 'email': 'goodmail@domain.com',
249 'firstname': 'test',
249 'firstname': 'test',
250 'lastname': 'test'
250 'lastname': 'test'
251 }
251 }
252 )
252 )
253
253
254 assertr = response.assert_response()
254 assertr = response.assert_response()
255 msg = 'Username "%(username)s" already exists'
255 msg = 'Username "%(username)s" already exists'
256 msg = msg % {'username': uname}
256 msg = msg % {'username': uname}
257 assertr.element_contains('#username+.error-message', msg)
257 assertr.element_contains('#username+.error-message', msg)
258
258
259 def test_register_err_same_email(self):
259 def test_register_err_same_email(self):
260 response = self.app.post(
260 response = self.app.post(
261 route_path('register'),
261 route_path('register'),
262 {
262 {
263 'username': 'test_admin_0',
263 'username': 'test_admin_0',
264 'password': 'test12',
264 'password': 'test12',
265 'password_confirmation': 'test12',
265 'password_confirmation': 'test12',
266 'email': 'test_admin@mail.com',
266 'email': 'test_admin@mail.com',
267 'firstname': 'test',
267 'firstname': 'test',
268 'lastname': 'test'
268 'lastname': 'test'
269 }
269 }
270 )
270 )
271
271
272 assertr = response.assert_response()
272 assertr = response.assert_response()
273 msg = u'This e-mail address is already taken'
273 msg = u'This e-mail address is already taken'
274 assertr.element_contains('#email+.error-message', msg)
274 assertr.element_contains('#email+.error-message', msg)
275
275
276 def test_register_err_same_email_case_sensitive(self):
276 def test_register_err_same_email_case_sensitive(self):
277 response = self.app.post(
277 response = self.app.post(
278 route_path('register'),
278 route_path('register'),
279 {
279 {
280 'username': 'test_admin_1',
280 'username': 'test_admin_1',
281 'password': 'test12',
281 'password': 'test12',
282 'password_confirmation': 'test12',
282 'password_confirmation': 'test12',
283 'email': 'TesT_Admin@mail.COM',
283 'email': 'TesT_Admin@mail.COM',
284 'firstname': 'test',
284 'firstname': 'test',
285 'lastname': 'test'
285 'lastname': 'test'
286 }
286 }
287 )
287 )
288 assertr = response.assert_response()
288 assertr = response.assert_response()
289 msg = u'This e-mail address is already taken'
289 msg = u'This e-mail address is already taken'
290 assertr.element_contains('#email+.error-message', msg)
290 assertr.element_contains('#email+.error-message', msg)
291
291
292 def test_register_err_wrong_data(self):
292 def test_register_err_wrong_data(self):
293 response = self.app.post(
293 response = self.app.post(
294 route_path('register'),
294 route_path('register'),
295 {
295 {
296 'username': 'xs',
296 'username': 'xs',
297 'password': 'test',
297 'password': 'test',
298 'password_confirmation': 'test',
298 'password_confirmation': 'test',
299 'email': 'goodmailm',
299 'email': 'goodmailm',
300 'firstname': 'test',
300 'firstname': 'test',
301 'lastname': 'test'
301 'lastname': 'test'
302 }
302 }
303 )
303 )
304 assert response.status == '200 OK'
304 assert response.status == '200 OK'
305 response.mustcontain('An email address must contain a single @')
305 response.mustcontain('An email address must contain a single @')
306 response.mustcontain('Enter a value 6 characters long or more')
306 response.mustcontain('Enter a value 6 characters long or more')
307
307
308 def test_register_err_username(self):
308 def test_register_err_username(self):
309 response = self.app.post(
309 response = self.app.post(
310 route_path('register'),
310 route_path('register'),
311 {
311 {
312 'username': 'error user',
312 'username': 'error user',
313 'password': 'test12',
313 'password': 'test12',
314 'password_confirmation': 'test12',
314 'password_confirmation': 'test12',
315 'email': 'goodmailm',
315 'email': 'goodmailm',
316 'firstname': 'test',
316 'firstname': 'test',
317 'lastname': 'test'
317 'lastname': 'test'
318 }
318 }
319 )
319 )
320
320
321 response.mustcontain('An email address must contain a single @')
321 response.mustcontain('An email address must contain a single @')
322 response.mustcontain(
322 response.mustcontain(
323 'Username may only contain '
323 'Username may only contain '
324 'alphanumeric characters underscores, '
324 'alphanumeric characters underscores, '
325 'periods or dashes and must begin with '
325 'periods or dashes and must begin with '
326 'alphanumeric character')
326 'alphanumeric character')
327
327
328 def test_register_err_case_sensitive(self):
328 def test_register_err_case_sensitive(self):
329 usr = 'Test_Admin'
329 usr = 'Test_Admin'
330 response = self.app.post(
330 response = self.app.post(
331 route_path('register'),
331 route_path('register'),
332 {
332 {
333 'username': usr,
333 'username': usr,
334 'password': 'test12',
334 'password': 'test12',
335 'password_confirmation': 'test12',
335 'password_confirmation': 'test12',
336 'email': 'goodmailm',
336 'email': 'goodmailm',
337 'firstname': 'test',
337 'firstname': 'test',
338 'lastname': 'test'
338 'lastname': 'test'
339 }
339 }
340 )
340 )
341
341
342 assertr = response.assert_response()
342 assertr = response.assert_response()
343 msg = u'Username "%(username)s" already exists'
343 msg = u'Username "%(username)s" already exists'
344 msg = msg % {'username': usr}
344 msg = msg % {'username': usr}
345 assertr.element_contains('#username+.error-message', msg)
345 assertr.element_contains('#username+.error-message', msg)
346
346
347 def test_register_special_chars(self):
347 def test_register_special_chars(self):
348 response = self.app.post(
348 response = self.app.post(
349 route_path('register'),
349 route_path('register'),
350 {
350 {
351 'username': 'xxxaxn',
351 'username': 'xxxaxn',
352 'password': 'ąćźżąśśśś',
352 'password': 'ąćźżąśśśś',
353 'password_confirmation': 'ąćźżąśśśś',
353 'password_confirmation': 'ąćźżąśśśś',
354 'email': 'goodmailm@test.plx',
354 'email': 'goodmailm@test.plx',
355 'firstname': 'test',
355 'firstname': 'test',
356 'lastname': 'test'
356 'lastname': 'test'
357 }
357 }
358 )
358 )
359
359
360 msg = u'Invalid characters (non-ascii) in password'
360 msg = u'Invalid characters (non-ascii) in password'
361 response.mustcontain(msg)
361 response.mustcontain(msg)
362
362
363 def test_register_password_mismatch(self):
363 def test_register_password_mismatch(self):
364 response = self.app.post(
364 response = self.app.post(
365 route_path('register'),
365 route_path('register'),
366 {
366 {
367 'username': 'xs',
367 'username': 'xs',
368 'password': '123qwe',
368 'password': '123qwe',
369 'password_confirmation': 'qwe123',
369 'password_confirmation': 'qwe123',
370 'email': 'goodmailm@test.plxa',
370 'email': 'goodmailm@test.plxa',
371 'firstname': 'test',
371 'firstname': 'test',
372 'lastname': 'test'
372 'lastname': 'test'
373 }
373 }
374 )
374 )
375 msg = u'Passwords do not match'
375 msg = u'Passwords do not match'
376 response.mustcontain(msg)
376 response.mustcontain(msg)
377
377
378 def test_register_ok(self):
378 def test_register_ok(self):
379 username = 'test_regular4'
379 username = 'test_regular4'
380 password = 'qweqwe'
380 password = 'qweqwe'
381 email = 'marcin@test.com'
381 email = 'marcin@test.com'
382 name = 'testname'
382 name = 'testname'
383 lastname = 'testlastname'
383 lastname = 'testlastname'
384
384
385 # this initializes a session
385 # this initializes a session
386 response = self.app.get(route_path('register'))
386 response = self.app.get(route_path('register'))
387 response.mustcontain('Create an Account')
387 response.mustcontain('Create an Account')
388
388
389
389
390 response = self.app.post(
390 response = self.app.post(
391 route_path('register'),
391 route_path('register'),
392 {
392 {
393 'username': username,
393 'username': username,
394 'password': password,
394 'password': password,
395 'password_confirmation': password,
395 'password_confirmation': password,
396 'email': email,
396 'email': email,
397 'firstname': name,
397 'firstname': name,
398 'lastname': lastname,
398 'lastname': lastname,
399 'admin': True
399 'admin': True
400 },
400 },
401 status=302
401 status=302
402 ) # This should be overridden
402 ) # This should be overridden
403
403
404 assert_session_flash(
404 assert_session_flash(
405 response, 'You have successfully registered with RhodeCode. You can log-in now.')
405 response, 'You have successfully registered with RhodeCode. You can log-in now.')
406
406
407 ret = Session().query(User).filter(
407 ret = Session().query(User).filter(
408 User.username == 'test_regular4').one()
408 User.username == 'test_regular4').one()
409 assert ret.username == username
409 assert ret.username == username
410 assert check_password(password, ret.password)
410 assert check_password(password, ret.password)
411 assert ret.email == email
411 assert ret.email == email
412 assert ret.name == name
412 assert ret.name == name
413 assert ret.lastname == lastname
413 assert ret.lastname == lastname
414 assert ret.auth_tokens is not None
414 assert ret.auth_tokens is not None
415 assert not ret.admin
415 assert not ret.admin
416
416
417 def test_forgot_password_wrong_mail(self):
417 def test_forgot_password_wrong_mail(self):
418 bad_email = 'marcin@wrongmail.org'
418 bad_email = 'marcin@wrongmail.org'
419 # this initializes a session
419 # this initializes a session
420 self.app.get(route_path('reset_password'))
420 self.app.get(route_path('reset_password'))
421
421
422 response = self.app.post(
422 response = self.app.post(
423 route_path('reset_password'), {'email': bad_email, }
423 route_path('reset_password'), {'email': bad_email, }
424 )
424 )
425 assert_session_flash(response,
425 assert_session_flash(response,
426 'If such email exists, a password reset link was sent to it.')
426 'If such email exists, a password reset link was sent to it.')
427
427
428 def test_forgot_password(self, user_util):
428 def test_forgot_password(self, user_util):
429 # this initializes a session
429 # this initializes a session
430 self.app.get(route_path('reset_password'))
430 self.app.get(route_path('reset_password'))
431
431
432 user = user_util.create_user()
432 user = user_util.create_user()
433 user_id = user.user_id
433 user_id = user.user_id
434 email = user.email
434 email = user.email
435
435
436 response = self.app.post(route_path('reset_password'), {'email': email, })
436 response = self.app.post(route_path('reset_password'), {'email': email, })
437
437
438 assert_session_flash(response,
438 assert_session_flash(response,
439 'If such email exists, a password reset link was sent to it.')
439 'If such email exists, a password reset link was sent to it.')
440
440
441 # BAD KEY
441 # BAD KEY
442 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
442 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
443 response = self.app.get(confirm_url, status=302)
443 response = self.app.get(confirm_url, status=302)
444 assert response.location.endswith(route_path('reset_password'))
444 assert response.location.endswith(route_path('reset_password'))
445 assert_session_flash(response, 'Given reset token is invalid')
445 assert_session_flash(response, 'Given reset token is invalid')
446
446
447 response.follow() # cleanup flash
447 response.follow() # cleanup flash
448
448
449 # GOOD KEY
449 # GOOD KEY
450 key = UserApiKeys.query()\
450 key = UserApiKeys.query()\
451 .filter(UserApiKeys.user_id == user_id)\
451 .filter(UserApiKeys.user_id == user_id)\
452 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
452 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
453 .first()
453 .first()
454
454
455 assert key
455 assert key
456
456
457 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
457 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
458 response = self.app.get(confirm_url)
458 response = self.app.get(confirm_url)
459 assert response.status == '302 Found'
459 assert response.status == '302 Found'
460 assert response.location.endswith(route_path('login'))
460 assert response.location.endswith(route_path('login'))
461
461
462 assert_session_flash(
462 assert_session_flash(
463 response,
463 response,
464 'Your password reset was successful, '
464 'Your password reset was successful, '
465 'a new password has been sent to your email')
465 'a new password has been sent to your email')
466
466
467 response.follow()
467 response.follow()
468
468
469 def _get_api_whitelist(self, values=None):
469 def _get_api_whitelist(self, values=None):
470 config = {'api_access_controllers_whitelist': values or []}
470 config = {'api_access_controllers_whitelist': values or []}
471 return config
471 return config
472
472
473 @pytest.mark.parametrize("test_name, auth_token", [
473 @pytest.mark.parametrize("test_name, auth_token", [
474 ('none', None),
474 ('none', None),
475 ('empty_string', ''),
475 ('empty_string', ''),
476 ('fake_number', '123456'),
476 ('fake_number', '123456'),
477 ('proper_auth_token', None)
477 ('proper_auth_token', None)
478 ])
478 ])
479 def test_access_not_whitelisted_page_via_auth_token(
479 def test_access_not_whitelisted_page_via_auth_token(
480 self, test_name, auth_token, user_admin):
480 self, test_name, auth_token, user_admin):
481
481
482 whitelist = self._get_api_whitelist([])
482 whitelist = self._get_api_whitelist([])
483 with mock.patch.dict('rhodecode.CONFIG', whitelist):
483 with mock.patch.dict('rhodecode.CONFIG', whitelist):
484 assert [] == whitelist['api_access_controllers_whitelist']
484 assert [] == whitelist['api_access_controllers_whitelist']
485 if test_name == 'proper_auth_token':
485 if test_name == 'proper_auth_token':
486 # use builtin if api_key is None
486 # use builtin if api_key is None
487 auth_token = user_admin.api_key
487 auth_token = user_admin.api_key
488
488
489 with fixture.anon_access(False):
489 with fixture.anon_access(False):
490 self.app.get(
490 self.app.get(
491 route_path('repo_commit_raw',
491 route_path('repo_commit_raw',
492 repo_name=HG_REPO, commit_id='tip',
492 repo_name=HG_REPO, commit_id='tip',
493 params=dict(api_key=auth_token)),
493 params=dict(api_key=auth_token)),
494 status=302)
494 status=302)
495
495
496 @pytest.mark.parametrize("test_name, auth_token, code", [
496 @pytest.mark.parametrize("test_name, auth_token, code", [
497 ('none', None, 302),
497 ('none', None, 302),
498 ('empty_string', '', 302),
498 ('empty_string', '', 302),
499 ('fake_number', '123456', 302),
499 ('fake_number', '123456', 302),
500 ('proper_auth_token', None, 200)
500 ('proper_auth_token', None, 200)
501 ])
501 ])
502 def test_access_whitelisted_page_via_auth_token(
502 def test_access_whitelisted_page_via_auth_token(
503 self, test_name, auth_token, code, user_admin):
503 self, test_name, auth_token, code, user_admin):
504
504
505 whitelist = self._get_api_whitelist(whitelist_view)
505 whitelist = self._get_api_whitelist(whitelist_view)
506
506
507 with mock.patch.dict('rhodecode.CONFIG', whitelist):
507 with mock.patch.dict('rhodecode.CONFIG', whitelist):
508 assert whitelist_view == whitelist['api_access_controllers_whitelist']
508 assert whitelist_view == whitelist['api_access_controllers_whitelist']
509
509
510 if test_name == 'proper_auth_token':
510 if test_name == 'proper_auth_token':
511 auth_token = user_admin.api_key
511 auth_token = user_admin.api_key
512 assert auth_token
512 assert auth_token
513
513
514 with fixture.anon_access(False):
514 with fixture.anon_access(False):
515 self.app.get(
515 self.app.get(
516 route_path('repo_commit_raw',
516 route_path('repo_commit_raw',
517 repo_name=HG_REPO, commit_id='tip',
517 repo_name=HG_REPO, commit_id='tip',
518 params=dict(api_key=auth_token)),
518 params=dict(api_key=auth_token)),
519 status=code)
519 status=code)
520
520
521 @pytest.mark.parametrize("test_name, auth_token, code", [
521 @pytest.mark.parametrize("test_name, auth_token, code", [
522 ('proper_auth_token', None, 200),
522 ('proper_auth_token', None, 200),
523 ('wrong_auth_token', '123456', 302),
523 ('wrong_auth_token', '123456', 302),
524 ])
524 ])
525 def test_access_whitelisted_page_via_auth_token_bound_to_token(
525 def test_access_whitelisted_page_via_auth_token_bound_to_token(
526 self, test_name, auth_token, code, user_admin):
526 self, test_name, auth_token, code, user_admin):
527
527
528 expected_token = auth_token
528 expected_token = auth_token
529 if test_name == 'proper_auth_token':
529 if test_name == 'proper_auth_token':
530 auth_token = user_admin.api_key
530 auth_token = user_admin.api_key
531 expected_token = auth_token
531 expected_token = auth_token
532 assert auth_token
532 assert auth_token
533
533
534 whitelist = self._get_api_whitelist([
534 whitelist = self._get_api_whitelist([
535 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
535 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
536
536
537 with mock.patch.dict('rhodecode.CONFIG', whitelist):
537 with mock.patch.dict('rhodecode.CONFIG', whitelist):
538
538
539 with fixture.anon_access(False):
539 with fixture.anon_access(False):
540 self.app.get(
540 self.app.get(
541 route_path('repo_commit_raw',
541 route_path('repo_commit_raw',
542 repo_name=HG_REPO, commit_id='tip',
542 repo_name=HG_REPO, commit_id='tip',
543 params=dict(api_key=auth_token)),
543 params=dict(api_key=auth_token)),
544 status=code)
544 status=code)
545
545
546 def test_access_page_via_extra_auth_token(self):
546 def test_access_page_via_extra_auth_token(self):
547 whitelist = self._get_api_whitelist(whitelist_view)
547 whitelist = self._get_api_whitelist(whitelist_view)
548 with mock.patch.dict('rhodecode.CONFIG', whitelist):
548 with mock.patch.dict('rhodecode.CONFIG', whitelist):
549 assert whitelist_view == \
549 assert whitelist_view == \
550 whitelist['api_access_controllers_whitelist']
550 whitelist['api_access_controllers_whitelist']
551
551
552 new_auth_token = AuthTokenModel().create(
552 new_auth_token = AuthTokenModel().create(
553 TEST_USER_ADMIN_LOGIN, 'test')
553 TEST_USER_ADMIN_LOGIN, 'test')
554 Session().commit()
554 Session().commit()
555 with fixture.anon_access(False):
555 with fixture.anon_access(False):
556 self.app.get(
556 self.app.get(
557 route_path('repo_commit_raw',
557 route_path('repo_commit_raw',
558 repo_name=HG_REPO, commit_id='tip',
558 repo_name=HG_REPO, commit_id='tip',
559 params=dict(api_key=new_auth_token.api_key)),
559 params=dict(api_key=new_auth_token.api_key)),
560 status=200)
560 status=200)
561
561
562 def test_access_page_via_expired_auth_token(self):
562 def test_access_page_via_expired_auth_token(self):
563 whitelist = self._get_api_whitelist(whitelist_view)
563 whitelist = self._get_api_whitelist(whitelist_view)
564 with mock.patch.dict('rhodecode.CONFIG', whitelist):
564 with mock.patch.dict('rhodecode.CONFIG', whitelist):
565 assert whitelist_view == \
565 assert whitelist_view == \
566 whitelist['api_access_controllers_whitelist']
566 whitelist['api_access_controllers_whitelist']
567
567
568 new_auth_token = AuthTokenModel().create(
568 new_auth_token = AuthTokenModel().create(
569 TEST_USER_ADMIN_LOGIN, 'test')
569 TEST_USER_ADMIN_LOGIN, 'test')
570 Session().commit()
570 Session().commit()
571 # patch the api key and make it expired
571 # patch the api key and make it expired
572 new_auth_token.expires = 0
572 new_auth_token.expires = 0
573 Session().add(new_auth_token)
573 Session().add(new_auth_token)
574 Session().commit()
574 Session().commit()
575 with fixture.anon_access(False):
575 with fixture.anon_access(False):
576 self.app.get(
576 self.app.get(
577 route_path('repo_commit_raw',
577 route_path('repo_commit_raw',
578 repo_name=HG_REPO, commit_id='tip',
578 repo_name=HG_REPO, commit_id='tip',
579 params=dict(api_key=new_auth_token.api_key)),
579 params=dict(api_key=new_auth_token.api_key)),
580 status=302)
580 status=302)
@@ -1,118 +1,118 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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.lib import helpers as h
23 from rhodecode.lib import helpers as h
24 from rhodecode.tests import (
24 from rhodecode.tests import (
25 TestController, clear_cache_regions,
25 TestController, clear_cache_regions,
26 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
26 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
27 from rhodecode.tests.fixture import Fixture
27 from rhodecode.tests.fixture import Fixture
28 from rhodecode.tests.utils import AssertResponse
28 from rhodecode.tests.utils import AssertResponse
29
29
30 fixture = Fixture()
30 fixture = Fixture()
31
31
32
32
33 def route_path(name, params=None, **kwargs):
33 def route_path(name, params=None, **kwargs):
34 import urllib
34 import urllib.request, urllib.parse, urllib.error
35 from rhodecode.apps._base import ADMIN_PREFIX
35 from rhodecode.apps._base import ADMIN_PREFIX
36
36
37 base_url = {
37 base_url = {
38 'login': ADMIN_PREFIX + '/login',
38 'login': ADMIN_PREFIX + '/login',
39 'logout': ADMIN_PREFIX + '/logout',
39 'logout': ADMIN_PREFIX + '/logout',
40 'register': ADMIN_PREFIX + '/register',
40 'register': ADMIN_PREFIX + '/register',
41 'reset_password':
41 'reset_password':
42 ADMIN_PREFIX + '/password_reset',
42 ADMIN_PREFIX + '/password_reset',
43 'reset_password_confirmation':
43 'reset_password_confirmation':
44 ADMIN_PREFIX + '/password_reset_confirmation',
44 ADMIN_PREFIX + '/password_reset_confirmation',
45
45
46 'admin_permissions_application':
46 'admin_permissions_application':
47 ADMIN_PREFIX + '/permissions/application',
47 ADMIN_PREFIX + '/permissions/application',
48 'admin_permissions_application_update':
48 'admin_permissions_application_update':
49 ADMIN_PREFIX + '/permissions/application/update',
49 ADMIN_PREFIX + '/permissions/application/update',
50 }[name].format(**kwargs)
50 }[name].format(**kwargs)
51
51
52 if params:
52 if params:
53 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
53 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
54 return base_url
54 return base_url
55
55
56
56
57 class TestPasswordReset(TestController):
57 class TestPasswordReset(TestController):
58
58
59 @pytest.mark.parametrize(
59 @pytest.mark.parametrize(
60 'pwd_reset_setting, show_link, show_reset', [
60 'pwd_reset_setting, show_link, show_reset', [
61 ('hg.password_reset.enabled', True, True),
61 ('hg.password_reset.enabled', True, True),
62 ('hg.password_reset.hidden', False, True),
62 ('hg.password_reset.hidden', False, True),
63 ('hg.password_reset.disabled', False, False),
63 ('hg.password_reset.disabled', False, False),
64 ])
64 ])
65 def test_password_reset_settings(
65 def test_password_reset_settings(
66 self, pwd_reset_setting, show_link, show_reset):
66 self, pwd_reset_setting, show_link, show_reset):
67 clear_cache_regions()
67 clear_cache_regions()
68 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
68 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
69 params = {
69 params = {
70 'csrf_token': self.csrf_token,
70 'csrf_token': self.csrf_token,
71 'anonymous': 'True',
71 'anonymous': 'True',
72 'default_register': 'hg.register.auto_activate',
72 'default_register': 'hg.register.auto_activate',
73 'default_register_message': '',
73 'default_register_message': '',
74 'default_password_reset': pwd_reset_setting,
74 'default_password_reset': pwd_reset_setting,
75 'default_extern_activate': 'hg.extern_activate.auto',
75 'default_extern_activate': 'hg.extern_activate.auto',
76 }
76 }
77 resp = self.app.post(
77 resp = self.app.post(
78 route_path('admin_permissions_application_update'), params=params)
78 route_path('admin_permissions_application_update'), params=params)
79 self.logout_user()
79 self.logout_user()
80
80
81 login_page = self.app.get(route_path('login'))
81 login_page = self.app.get(route_path('login'))
82 asr_login = AssertResponse(login_page)
82 asr_login = AssertResponse(login_page)
83
83
84 if show_link:
84 if show_link:
85 asr_login.one_element_exists('a.pwd_reset')
85 asr_login.one_element_exists('a.pwd_reset')
86 else:
86 else:
87 asr_login.no_element_exists('a.pwd_reset')
87 asr_login.no_element_exists('a.pwd_reset')
88
88
89 response = self.app.get(route_path('reset_password'))
89 response = self.app.get(route_path('reset_password'))
90
90
91 assert_response = response.assert_response()
91 assert_response = response.assert_response()
92 if show_reset:
92 if show_reset:
93 response.mustcontain('Send password reset email')
93 response.mustcontain('Send password reset email')
94 assert_response.one_element_exists('#email')
94 assert_response.one_element_exists('#email')
95 assert_response.one_element_exists('#send')
95 assert_response.one_element_exists('#send')
96 else:
96 else:
97 response.mustcontain('Password reset is disabled.')
97 response.mustcontain('Password reset is disabled.')
98 assert_response.no_element_exists('#email')
98 assert_response.no_element_exists('#email')
99 assert_response.no_element_exists('#send')
99 assert_response.no_element_exists('#send')
100
100
101 def test_password_form_disabled(self):
101 def test_password_form_disabled(self):
102 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
102 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
103 params = {
103 params = {
104 'csrf_token': self.csrf_token,
104 'csrf_token': self.csrf_token,
105 'anonymous': 'True',
105 'anonymous': 'True',
106 'default_register': 'hg.register.auto_activate',
106 'default_register': 'hg.register.auto_activate',
107 'default_register_message': '',
107 'default_register_message': '',
108 'default_password_reset': 'hg.password_reset.disabled',
108 'default_password_reset': 'hg.password_reset.disabled',
109 'default_extern_activate': 'hg.extern_activate.auto',
109 'default_extern_activate': 'hg.extern_activate.auto',
110 }
110 }
111 self.app.post(route_path('admin_permissions_application_update'), params=params)
111 self.app.post(route_path('admin_permissions_application_update'), params=params)
112 self.logout_user()
112 self.logout_user()
113
113
114 response = self.app.post(
114 response = self.app.post(
115 route_path('reset_password'), {'email': 'lisa@rhodecode.com',}
115 route_path('reset_password'), {'email': 'lisa@rhodecode.com',}
116 )
116 )
117 response = response.follow()
117 response = response.follow()
118 response.mustcontain('Password reset is disabled.')
118 response.mustcontain('Password reset is disabled.')
@@ -1,208 +1,208 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 # -*- coding: utf-8 -*-
20 # -*- coding: utf-8 -*-
21
21
22 # Copyright (C) 2016-2020 RhodeCode GmbH
22 # Copyright (C) 2016-2020 RhodeCode GmbH
23 #
23 #
24 # This program is free software: you can redistribute it and/or modify
24 # This program is free software: you can redistribute it and/or modify
25 # it under the terms of the GNU Affero General Public License, version 3
25 # it under the terms of the GNU Affero General Public License, version 3
26 # (only), as published by the Free Software Foundation.
26 # (only), as published by the Free Software Foundation.
27 #
27 #
28 # This program is distributed in the hope that it will be useful,
28 # This program is distributed in the hope that it will be useful,
29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 # GNU General Public License for more details.
31 # GNU General Public License for more details.
32 #
32 #
33 # You should have received a copy of the GNU Affero General Public License
33 # You should have received a copy of the GNU Affero General Public License
34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 #
35 #
36 # This program is dual-licensed. If you wish to learn more about the
36 # This program is dual-licensed. If you wish to learn more about the
37 # RhodeCode Enterprise Edition, including its added features, Support services,
37 # RhodeCode Enterprise Edition, including its added features, Support services,
38 # and proprietary license terms, please see https://rhodecode.com/licenses/
38 # and proprietary license terms, please see https://rhodecode.com/licenses/
39
39
40 import pytest
40 import pytest
41
41
42 from rhodecode.model.db import User
42 from rhodecode.model.db import User
43 from rhodecode.tests import TestController, assert_session_flash
43 from rhodecode.tests import TestController, assert_session_flash
44 from rhodecode.lib import helpers as h
44 from rhodecode.lib import helpers as h
45
45
46
46
47 def route_path(name, params=None, **kwargs):
47 def route_path(name, params=None, **kwargs):
48 import urllib
48 import urllib.request, urllib.parse, urllib.error
49 from rhodecode.apps._base import ADMIN_PREFIX
49 from rhodecode.apps._base import ADMIN_PREFIX
50
50
51 base_url = {
51 base_url = {
52 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
52 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
53 'my_account_update': ADMIN_PREFIX + '/my_account/update',
53 'my_account_update': ADMIN_PREFIX + '/my_account/update',
54 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
54 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
55 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
55 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
56 }[name].format(**kwargs)
56 }[name].format(**kwargs)
57
57
58 if params:
58 if params:
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
59 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
60 return base_url
60 return base_url
61
61
62
62
63 class TestMyAccountEdit(TestController):
63 class TestMyAccountEdit(TestController):
64
64
65 def test_my_account_edit(self):
65 def test_my_account_edit(self):
66 self.log_user()
66 self.log_user()
67 response = self.app.get(route_path('my_account_edit'))
67 response = self.app.get(route_path('my_account_edit'))
68
68
69 response.mustcontain('value="test_admin')
69 response.mustcontain('value="test_admin')
70
70
71 @pytest.mark.backends("git", "hg")
71 @pytest.mark.backends("git", "hg")
72 def test_my_account_my_pullrequests(self, pr_util):
72 def test_my_account_my_pullrequests(self, pr_util):
73 self.log_user()
73 self.log_user()
74 response = self.app.get(route_path('my_account_pullrequests'))
74 response = self.app.get(route_path('my_account_pullrequests'))
75 response.mustcontain('There are currently no open pull '
75 response.mustcontain('There are currently no open pull '
76 'requests requiring your participation.')
76 'requests requiring your participation.')
77
77
78 @pytest.mark.backends("git", "hg")
78 @pytest.mark.backends("git", "hg")
79 @pytest.mark.parametrize('params, expected_title', [
79 @pytest.mark.parametrize('params, expected_title', [
80 ({'closed': 1}, 'Closed'),
80 ({'closed': 1}, 'Closed'),
81 ({'awaiting_my_review': 1}, 'Awaiting my review'),
81 ({'awaiting_my_review': 1}, 'Awaiting my review'),
82 ])
82 ])
83 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header, params, expected_title):
83 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header, params, expected_title):
84 self.log_user()
84 self.log_user()
85 response = self.app.get(route_path('my_account_pullrequests_data'),
85 response = self.app.get(route_path('my_account_pullrequests_data'),
86 extra_environ=xhr_header)
86 extra_environ=xhr_header)
87 assert response.json == {
87 assert response.json == {
88 u'data': [], u'draw': None,
88 u'data': [], u'draw': None,
89 u'recordsFiltered': 0, u'recordsTotal': 0}
89 u'recordsFiltered': 0, u'recordsTotal': 0}
90
90
91 pr = pr_util.create_pull_request(title='TestMyAccountPR')
91 pr = pr_util.create_pull_request(title='TestMyAccountPR')
92 expected = {
92 expected = {
93 'author_raw': 'RhodeCode Admin',
93 'author_raw': 'RhodeCode Admin',
94 'name_raw': pr.pull_request_id
94 'name_raw': pr.pull_request_id
95 }
95 }
96 response = self.app.get(route_path('my_account_pullrequests_data'),
96 response = self.app.get(route_path('my_account_pullrequests_data'),
97 extra_environ=xhr_header)
97 extra_environ=xhr_header)
98 assert response.json['recordsTotal'] == 1
98 assert response.json['recordsTotal'] == 1
99 assert response.json['data'][0]['author_raw'] == expected['author_raw']
99 assert response.json['data'][0]['author_raw'] == expected['author_raw']
100
100
101 assert response.json['data'][0]['author_raw'] == expected['author_raw']
101 assert response.json['data'][0]['author_raw'] == expected['author_raw']
102 assert response.json['data'][0]['name_raw'] == expected['name_raw']
102 assert response.json['data'][0]['name_raw'] == expected['name_raw']
103
103
104 @pytest.mark.parametrize(
104 @pytest.mark.parametrize(
105 "name, attrs", [
105 "name, attrs", [
106 ('firstname', {'firstname': 'new_username'}),
106 ('firstname', {'firstname': 'new_username'}),
107 ('lastname', {'lastname': 'new_username'}),
107 ('lastname', {'lastname': 'new_username'}),
108 ('admin', {'admin': True}),
108 ('admin', {'admin': True}),
109 ('admin', {'admin': False}),
109 ('admin', {'admin': False}),
110 ('extern_type', {'extern_type': 'ldap'}),
110 ('extern_type', {'extern_type': 'ldap'}),
111 ('extern_type', {'extern_type': None}),
111 ('extern_type', {'extern_type': None}),
112 # ('extern_name', {'extern_name': 'test'}),
112 # ('extern_name', {'extern_name': 'test'}),
113 # ('extern_name', {'extern_name': None}),
113 # ('extern_name', {'extern_name': None}),
114 ('active', {'active': False}),
114 ('active', {'active': False}),
115 ('active', {'active': True}),
115 ('active', {'active': True}),
116 ('email', {'email': u'some@email.com'}),
116 ('email', {'email': u'some@email.com'}),
117 ])
117 ])
118 def test_my_account_update(self, name, attrs, user_util):
118 def test_my_account_update(self, name, attrs, user_util):
119 usr = user_util.create_user(password='qweqwe')
119 usr = user_util.create_user(password='qweqwe')
120 params = usr.get_api_data() # current user data
120 params = usr.get_api_data() # current user data
121 user_id = usr.user_id
121 user_id = usr.user_id
122 self.log_user(
122 self.log_user(
123 username=usr.username, password='qweqwe')
123 username=usr.username, password='qweqwe')
124
124
125 params.update({'password_confirmation': ''})
125 params.update({'password_confirmation': ''})
126 params.update({'new_password': ''})
126 params.update({'new_password': ''})
127 params.update({'extern_type': u'rhodecode'})
127 params.update({'extern_type': u'rhodecode'})
128 params.update({'extern_name': u'rhodecode'})
128 params.update({'extern_name': u'rhodecode'})
129 params.update({'csrf_token': self.csrf_token})
129 params.update({'csrf_token': self.csrf_token})
130
130
131 params.update(attrs)
131 params.update(attrs)
132 # my account page cannot set language param yet, only for admins
132 # my account page cannot set language param yet, only for admins
133 del params['language']
133 del params['language']
134 if name == 'email':
134 if name == 'email':
135 uem = user_util.create_additional_user_email(usr, attrs['email'])
135 uem = user_util.create_additional_user_email(usr, attrs['email'])
136 email_before = User.get(user_id).email
136 email_before = User.get(user_id).email
137
137
138 response = self.app.post(route_path('my_account_update'), params)
138 response = self.app.post(route_path('my_account_update'), params)
139
139
140 assert_session_flash(
140 assert_session_flash(
141 response, 'Your account was updated successfully')
141 response, 'Your account was updated successfully')
142
142
143 del params['csrf_token']
143 del params['csrf_token']
144
144
145 updated_user = User.get(user_id)
145 updated_user = User.get(user_id)
146 updated_params = updated_user.get_api_data()
146 updated_params = updated_user.get_api_data()
147 updated_params.update({'password_confirmation': ''})
147 updated_params.update({'password_confirmation': ''})
148 updated_params.update({'new_password': ''})
148 updated_params.update({'new_password': ''})
149
149
150 params['last_login'] = updated_params['last_login']
150 params['last_login'] = updated_params['last_login']
151 params['last_activity'] = updated_params['last_activity']
151 params['last_activity'] = updated_params['last_activity']
152 # my account page cannot set language param yet, only for admins
152 # my account page cannot set language param yet, only for admins
153 # but we get this info from API anyway
153 # but we get this info from API anyway
154 params['language'] = updated_params['language']
154 params['language'] = updated_params['language']
155
155
156 if name == 'email':
156 if name == 'email':
157 params['emails'] = [attrs['email'], email_before]
157 params['emails'] = [attrs['email'], email_before]
158 if name == 'extern_type':
158 if name == 'extern_type':
159 # cannot update this via form, expected value is original one
159 # cannot update this via form, expected value is original one
160 params['extern_type'] = "rhodecode"
160 params['extern_type'] = "rhodecode"
161 if name == 'extern_name':
161 if name == 'extern_name':
162 # cannot update this via form, expected value is original one
162 # cannot update this via form, expected value is original one
163 params['extern_name'] = str(user_id)
163 params['extern_name'] = str(user_id)
164 if name == 'active':
164 if name == 'active':
165 # my account cannot deactivate account
165 # my account cannot deactivate account
166 params['active'] = True
166 params['active'] = True
167 if name == 'admin':
167 if name == 'admin':
168 # my account cannot make you an admin !
168 # my account cannot make you an admin !
169 params['admin'] = False
169 params['admin'] = False
170
170
171 assert params == updated_params
171 assert params == updated_params
172
172
173 def test_my_account_update_err_email_not_exists_in_emails(self):
173 def test_my_account_update_err_email_not_exists_in_emails(self):
174 self.log_user()
174 self.log_user()
175
175
176 new_email = 'test_regular@mail.com' # not in emails
176 new_email = 'test_regular@mail.com' # not in emails
177 params = {
177 params = {
178 'username': 'test_admin',
178 'username': 'test_admin',
179 'new_password': 'test12',
179 'new_password': 'test12',
180 'password_confirmation': 'test122',
180 'password_confirmation': 'test122',
181 'firstname': 'NewName',
181 'firstname': 'NewName',
182 'lastname': 'NewLastname',
182 'lastname': 'NewLastname',
183 'email': new_email,
183 'email': new_email,
184 'csrf_token': self.csrf_token,
184 'csrf_token': self.csrf_token,
185 }
185 }
186
186
187 response = self.app.post(route_path('my_account_update'),
187 response = self.app.post(route_path('my_account_update'),
188 params=params)
188 params=params)
189
189
190 response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com')
190 response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com')
191
191
192 def test_my_account_update_bad_email_address(self):
192 def test_my_account_update_bad_email_address(self):
193 self.log_user('test_regular2', 'test12')
193 self.log_user('test_regular2', 'test12')
194
194
195 new_email = 'newmail.pl'
195 new_email = 'newmail.pl'
196 params = {
196 params = {
197 'username': 'test_admin',
197 'username': 'test_admin',
198 'new_password': 'test12',
198 'new_password': 'test12',
199 'password_confirmation': 'test122',
199 'password_confirmation': 'test122',
200 'firstname': 'NewName',
200 'firstname': 'NewName',
201 'lastname': 'NewLastname',
201 'lastname': 'NewLastname',
202 'email': new_email,
202 'email': new_email,
203 'csrf_token': self.csrf_token,
203 'csrf_token': self.csrf_token,
204 }
204 }
205 response = self.app.post(route_path('my_account_update'),
205 response = self.app.post(route_path('my_account_update'),
206 params=params)
206 params=params)
207
207
208 response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com')
208 response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com')
@@ -1,206 +1,206 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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.apps._base import ADMIN_PREFIX
23 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.tests import (
24 from rhodecode.tests import (
25 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
25 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
26 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
26 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
27 from rhodecode.tests.fixture import Fixture
27 from rhodecode.tests.fixture import Fixture
28
28
29 from rhodecode.model.db import Notification, User
29 from rhodecode.model.db import Notification, User
30 from rhodecode.model.user import UserModel
30 from rhodecode.model.user import UserModel
31 from rhodecode.model.notification import NotificationModel
31 from rhodecode.model.notification import NotificationModel
32 from rhodecode.model.meta import Session
32 from rhodecode.model.meta import Session
33
33
34 fixture = Fixture()
34 fixture = Fixture()
35
35
36
36
37 def route_path(name, params=None, **kwargs):
37 def route_path(name, params=None, **kwargs):
38 import urllib
38 import urllib.request, urllib.parse, urllib.error
39 from rhodecode.apps._base import ADMIN_PREFIX
39 from rhodecode.apps._base import ADMIN_PREFIX
40
40
41 base_url = {
41 base_url = {
42 'notifications_show_all': ADMIN_PREFIX + '/notifications',
42 'notifications_show_all': ADMIN_PREFIX + '/notifications',
43 'notifications_mark_all_read': ADMIN_PREFIX + '/notifications_mark_all_read',
43 'notifications_mark_all_read': ADMIN_PREFIX + '/notifications_mark_all_read',
44 'notifications_show': ADMIN_PREFIX + '/notifications/{notification_id}',
44 'notifications_show': ADMIN_PREFIX + '/notifications/{notification_id}',
45 'notifications_update': ADMIN_PREFIX + '/notifications/{notification_id}/update',
45 'notifications_update': ADMIN_PREFIX + '/notifications/{notification_id}/update',
46 'notifications_delete': ADMIN_PREFIX + '/notifications/{notification_id}/delete',
46 'notifications_delete': ADMIN_PREFIX + '/notifications/{notification_id}/delete',
47
47
48 }[name].format(**kwargs)
48 }[name].format(**kwargs)
49
49
50 if params:
50 if params:
51 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
51 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
52 return base_url
52 return base_url
53
53
54
54
55 class TestNotificationsController(TestController):
55 class TestNotificationsController(TestController):
56
56
57 def teardown_method(self, method):
57 def teardown_method(self, method):
58 for n in Notification.query().all():
58 for n in Notification.query().all():
59 inst = Notification.get(n.notification_id)
59 inst = Notification.get(n.notification_id)
60 Session().delete(inst)
60 Session().delete(inst)
61 Session().commit()
61 Session().commit()
62
62
63 def test_mark_all_read(self, user_util):
63 def test_mark_all_read(self, user_util):
64 user = user_util.create_user(password='qweqwe')
64 user = user_util.create_user(password='qweqwe')
65 self.log_user(user.username, 'qweqwe')
65 self.log_user(user.username, 'qweqwe')
66
66
67 self.app.post(
67 self.app.post(
68 route_path('notifications_mark_all_read'), status=302,
68 route_path('notifications_mark_all_read'), status=302,
69 params={'csrf_token': self.csrf_token}
69 params={'csrf_token': self.csrf_token}
70 )
70 )
71
71
72 def test_show_all(self, user_util):
72 def test_show_all(self, user_util):
73 user = user_util.create_user(password='qweqwe')
73 user = user_util.create_user(password='qweqwe')
74 user_id = user.user_id
74 user_id = user.user_id
75 self.log_user(user.username, 'qweqwe')
75 self.log_user(user.username, 'qweqwe')
76
76
77 response = self.app.get(
77 response = self.app.get(
78 route_path('notifications_show_all', params={'type': 'all'}))
78 route_path('notifications_show_all', params={'type': 'all'}))
79 response.mustcontain(
79 response.mustcontain(
80 '<div class="table">No notifications here yet</div>')
80 '<div class="table">No notifications here yet</div>')
81
81
82 notification = NotificationModel().create(
82 notification = NotificationModel().create(
83 created_by=user_id, notification_subject=u'test_notification_1',
83 created_by=user_id, notification_subject=u'test_notification_1',
84 notification_body=u'notification_1', recipients=[user_id])
84 notification_body=u'notification_1', recipients=[user_id])
85 Session().commit()
85 Session().commit()
86 notification_id = notification.notification_id
86 notification_id = notification.notification_id
87
87
88 response = self.app.get(route_path('notifications_show_all',
88 response = self.app.get(route_path('notifications_show_all',
89 params={'type': 'all'}))
89 params={'type': 'all'}))
90 response.mustcontain('id="notification_%s"' % notification_id)
90 response.mustcontain('id="notification_%s"' % notification_id)
91
91
92 def test_show_unread(self, user_util):
92 def test_show_unread(self, user_util):
93 user = user_util.create_user(password='qweqwe')
93 user = user_util.create_user(password='qweqwe')
94 user_id = user.user_id
94 user_id = user.user_id
95 self.log_user(user.username, 'qweqwe')
95 self.log_user(user.username, 'qweqwe')
96
96
97 response = self.app.get(route_path('notifications_show_all'))
97 response = self.app.get(route_path('notifications_show_all'))
98 response.mustcontain(
98 response.mustcontain(
99 '<div class="table">No notifications here yet</div>')
99 '<div class="table">No notifications here yet</div>')
100
100
101 notification = NotificationModel().create(
101 notification = NotificationModel().create(
102 created_by=user_id, notification_subject=u'test_notification_1',
102 created_by=user_id, notification_subject=u'test_notification_1',
103 notification_body=u'notification_1', recipients=[user_id])
103 notification_body=u'notification_1', recipients=[user_id])
104
104
105 # mark the USER notification as unread
105 # mark the USER notification as unread
106 user_notification = NotificationModel().get_user_notification(
106 user_notification = NotificationModel().get_user_notification(
107 user_id, notification)
107 user_id, notification)
108 user_notification.read = False
108 user_notification.read = False
109
109
110 Session().commit()
110 Session().commit()
111 notification_id = notification.notification_id
111 notification_id = notification.notification_id
112
112
113 response = self.app.get(route_path('notifications_show_all'))
113 response = self.app.get(route_path('notifications_show_all'))
114 response.mustcontain('id="notification_%s"' % notification_id)
114 response.mustcontain('id="notification_%s"' % notification_id)
115 response.mustcontain('<div class="desc unread')
115 response.mustcontain('<div class="desc unread')
116
116
117 @pytest.mark.parametrize('user,password', [
117 @pytest.mark.parametrize('user,password', [
118 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
118 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
119 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
119 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
120 ])
120 ])
121 def test_delete(self, user, password, user_util):
121 def test_delete(self, user, password, user_util):
122 self.log_user(user, password)
122 self.log_user(user, password)
123 cur_user = self._get_logged_user()
123 cur_user = self._get_logged_user()
124
124
125 u1 = user_util.create_user()
125 u1 = user_util.create_user()
126 u2 = user_util.create_user()
126 u2 = user_util.create_user()
127
127
128 # make notifications
128 # make notifications
129 notification = NotificationModel().create(
129 notification = NotificationModel().create(
130 created_by=cur_user, notification_subject=u'test',
130 created_by=cur_user, notification_subject=u'test',
131 notification_body=u'hi there', recipients=[cur_user, u1, u2])
131 notification_body=u'hi there', recipients=[cur_user, u1, u2])
132 Session().commit()
132 Session().commit()
133 u1 = User.get(u1.user_id)
133 u1 = User.get(u1.user_id)
134 u2 = User.get(u2.user_id)
134 u2 = User.get(u2.user_id)
135
135
136 # check DB
136 # check DB
137 get_notif = lambda un: [x.notification for x in un]
137 get_notif = lambda un: [x.notification for x in un]
138 assert get_notif(cur_user.notifications) == [notification]
138 assert get_notif(cur_user.notifications) == [notification]
139 assert get_notif(u1.notifications) == [notification]
139 assert get_notif(u1.notifications) == [notification]
140 assert get_notif(u2.notifications) == [notification]
140 assert get_notif(u2.notifications) == [notification]
141 cur_usr_id = cur_user.user_id
141 cur_usr_id = cur_user.user_id
142
142
143 response = self.app.post(
143 response = self.app.post(
144 route_path('notifications_delete',
144 route_path('notifications_delete',
145 notification_id=notification.notification_id),
145 notification_id=notification.notification_id),
146 params={'csrf_token': self.csrf_token})
146 params={'csrf_token': self.csrf_token})
147 assert response.json == 'ok'
147 assert response.json == 'ok'
148
148
149 cur_user = User.get(cur_usr_id)
149 cur_user = User.get(cur_usr_id)
150 assert cur_user.notifications == []
150 assert cur_user.notifications == []
151
151
152 @pytest.mark.parametrize('user,password', [
152 @pytest.mark.parametrize('user,password', [
153 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
153 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
154 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
154 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
155 ])
155 ])
156 def test_show(self, user, password, user_util):
156 def test_show(self, user, password, user_util):
157 self.log_user(user, password)
157 self.log_user(user, password)
158 cur_user = self._get_logged_user()
158 cur_user = self._get_logged_user()
159 u1 = user_util.create_user()
159 u1 = user_util.create_user()
160 u2 = user_util.create_user()
160 u2 = user_util.create_user()
161
161
162 subject = u'test'
162 subject = u'test'
163 notif_body = u'hi there'
163 notif_body = u'hi there'
164 notification = NotificationModel().create(
164 notification = NotificationModel().create(
165 created_by=cur_user, notification_subject=subject,
165 created_by=cur_user, notification_subject=subject,
166 notification_body=notif_body, recipients=[cur_user, u1, u2])
166 notification_body=notif_body, recipients=[cur_user, u1, u2])
167 Session().commit()
167 Session().commit()
168
168
169 response = self.app.get(
169 response = self.app.get(
170 route_path('notifications_show',
170 route_path('notifications_show',
171 notification_id=notification.notification_id))
171 notification_id=notification.notification_id))
172
172
173 response.mustcontain(subject)
173 response.mustcontain(subject)
174 response.mustcontain(notif_body)
174 response.mustcontain(notif_body)
175
175
176 @pytest.mark.parametrize('user,password', [
176 @pytest.mark.parametrize('user,password', [
177 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
177 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
178 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
178 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
179 ])
179 ])
180 def test_update(self, user, password, user_util):
180 def test_update(self, user, password, user_util):
181 self.log_user(user, password)
181 self.log_user(user, password)
182 cur_user = self._get_logged_user()
182 cur_user = self._get_logged_user()
183 u1 = user_util.create_user()
183 u1 = user_util.create_user()
184 u2 = user_util.create_user()
184 u2 = user_util.create_user()
185
185
186 # make notifications
186 # make notifications
187 recipients = [cur_user, u1, u2]
187 recipients = [cur_user, u1, u2]
188 notification = NotificationModel().create(
188 notification = NotificationModel().create(
189 created_by=cur_user, notification_subject=u'test',
189 created_by=cur_user, notification_subject=u'test',
190 notification_body=u'hi there', recipients=recipients)
190 notification_body=u'hi there', recipients=recipients)
191 Session().commit()
191 Session().commit()
192
192
193 for u_obj in recipients:
193 for u_obj in recipients:
194 # if it's current user, he has his message already read
194 # if it's current user, he has his message already read
195 read = u_obj.username == user
195 read = u_obj.username == user
196 assert len(u_obj.notifications) == 1
196 assert len(u_obj.notifications) == 1
197 assert u_obj.notifications[0].read == read
197 assert u_obj.notifications[0].read == read
198
198
199 response = self.app.post(
199 response = self.app.post(
200 route_path('notifications_update',
200 route_path('notifications_update',
201 notification_id=notification.notification_id),
201 notification_id=notification.notification_id),
202 params={'csrf_token': self.csrf_token})
202 params={'csrf_token': self.csrf_token})
203 assert response.json == 'ok'
203 assert response.json == 'ok'
204
204
205 cur_user = self._get_logged_user()
205 cur_user = self._get_logged_user()
206 assert True is cur_user.notifications[0].read
206 assert True is cur_user.notifications[0].read
@@ -1,163 +1,163 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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.model.db import User, UserSshKeys
23 from rhodecode.model.db import User, UserSshKeys
24
24
25 from rhodecode.tests import TestController, assert_session_flash
25 from rhodecode.tests import TestController, assert_session_flash
26 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.fixture import Fixture
27
27
28 fixture = Fixture()
28 fixture = Fixture()
29
29
30
30
31 def route_path(name, params=None, **kwargs):
31 def route_path(name, params=None, **kwargs):
32 import urllib
32 import urllib.request, urllib.parse, urllib.error
33 from rhodecode.apps._base import ADMIN_PREFIX
33 from rhodecode.apps._base import ADMIN_PREFIX
34
34
35 base_url = {
35 base_url = {
36 'my_account_ssh_keys':
36 'my_account_ssh_keys':
37 ADMIN_PREFIX + '/my_account/ssh_keys',
37 ADMIN_PREFIX + '/my_account/ssh_keys',
38 'my_account_ssh_keys_generate':
38 'my_account_ssh_keys_generate':
39 ADMIN_PREFIX + '/my_account/ssh_keys/generate',
39 ADMIN_PREFIX + '/my_account/ssh_keys/generate',
40 'my_account_ssh_keys_add':
40 'my_account_ssh_keys_add':
41 ADMIN_PREFIX + '/my_account/ssh_keys/new',
41 ADMIN_PREFIX + '/my_account/ssh_keys/new',
42 'my_account_ssh_keys_delete':
42 'my_account_ssh_keys_delete':
43 ADMIN_PREFIX + '/my_account/ssh_keys/delete',
43 ADMIN_PREFIX + '/my_account/ssh_keys/delete',
44 }[name].format(**kwargs)
44 }[name].format(**kwargs)
45
45
46 if params:
46 if params:
47 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 return base_url
48 return base_url
49
49
50
50
51 class TestMyAccountSshKeysView(TestController):
51 class TestMyAccountSshKeysView(TestController):
52 INVALID_KEY = """\
52 INVALID_KEY = """\
53 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
53 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
54 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
54 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
55 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
55 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
56 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
56 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
57 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
57 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
58 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
58 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
59 your_email@example.com
59 your_email@example.com
60 """
60 """
61 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
61 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
62 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
62 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
63 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
63 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
64 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
64 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
65 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
65 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
66 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
66 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
67 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
67 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
68 'your_email@example.com'
68 'your_email@example.com'
69 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
69 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
70
70
71 def test_add_ssh_key_error(self, user_util):
71 def test_add_ssh_key_error(self, user_util):
72 user = user_util.create_user(password='qweqwe')
72 user = user_util.create_user(password='qweqwe')
73 self.log_user(user.username, 'qweqwe')
73 self.log_user(user.username, 'qweqwe')
74
74
75 key_data = self.INVALID_KEY
75 key_data = self.INVALID_KEY
76
76
77 desc = 'MY SSH KEY'
77 desc = 'MY SSH KEY'
78 response = self.app.post(
78 response = self.app.post(
79 route_path('my_account_ssh_keys_add'),
79 route_path('my_account_ssh_keys_add'),
80 {'description': desc, 'key_data': key_data,
80 {'description': desc, 'key_data': key_data,
81 'csrf_token': self.csrf_token})
81 'csrf_token': self.csrf_token})
82 assert_session_flash(response, 'An error occurred during ssh '
82 assert_session_flash(response, 'An error occurred during ssh '
83 'key saving: Unable to decode the key')
83 'key saving: Unable to decode the key')
84
84
85 def test_ssh_key_duplicate(self, user_util):
85 def test_ssh_key_duplicate(self, user_util):
86 user = user_util.create_user(password='qweqwe')
86 user = user_util.create_user(password='qweqwe')
87 self.log_user(user.username, 'qweqwe')
87 self.log_user(user.username, 'qweqwe')
88 key_data = self.VALID_KEY
88 key_data = self.VALID_KEY
89
89
90 desc = 'MY SSH KEY'
90 desc = 'MY SSH KEY'
91 response = self.app.post(
91 response = self.app.post(
92 route_path('my_account_ssh_keys_add'),
92 route_path('my_account_ssh_keys_add'),
93 {'description': desc, 'key_data': key_data,
93 {'description': desc, 'key_data': key_data,
94 'csrf_token': self.csrf_token})
94 'csrf_token': self.csrf_token})
95 assert_session_flash(response, 'Ssh Key successfully created')
95 assert_session_flash(response, 'Ssh Key successfully created')
96 response.follow() # flush session flash
96 response.follow() # flush session flash
97
97
98 # add the same key AGAIN
98 # add the same key AGAIN
99 desc = 'MY SSH KEY'
99 desc = 'MY SSH KEY'
100 response = self.app.post(
100 response = self.app.post(
101 route_path('my_account_ssh_keys_add'),
101 route_path('my_account_ssh_keys_add'),
102 {'description': desc, 'key_data': key_data,
102 {'description': desc, 'key_data': key_data,
103 'csrf_token': self.csrf_token})
103 'csrf_token': self.csrf_token})
104
104
105 err = 'Such key with fingerprint `{}` already exists, ' \
105 err = 'Such key with fingerprint `{}` already exists, ' \
106 'please use a different one'.format(self.FINGERPRINT)
106 'please use a different one'.format(self.FINGERPRINT)
107 assert_session_flash(response, 'An error occurred during ssh key '
107 assert_session_flash(response, 'An error occurred during ssh key '
108 'saving: {}'.format(err))
108 'saving: {}'.format(err))
109
109
110 def test_add_ssh_key(self, user_util):
110 def test_add_ssh_key(self, user_util):
111 user = user_util.create_user(password='qweqwe')
111 user = user_util.create_user(password='qweqwe')
112 self.log_user(user.username, 'qweqwe')
112 self.log_user(user.username, 'qweqwe')
113
113
114 key_data = self.VALID_KEY
114 key_data = self.VALID_KEY
115
115
116 desc = 'MY SSH KEY'
116 desc = 'MY SSH KEY'
117 response = self.app.post(
117 response = self.app.post(
118 route_path('my_account_ssh_keys_add'),
118 route_path('my_account_ssh_keys_add'),
119 {'description': desc, 'key_data': key_data,
119 {'description': desc, 'key_data': key_data,
120 'csrf_token': self.csrf_token})
120 'csrf_token': self.csrf_token})
121 assert_session_flash(response, 'Ssh Key successfully created')
121 assert_session_flash(response, 'Ssh Key successfully created')
122
122
123 response = response.follow()
123 response = response.follow()
124 response.mustcontain(desc)
124 response.mustcontain(desc)
125
125
126 def test_delete_ssh_key(self, user_util):
126 def test_delete_ssh_key(self, user_util):
127 user = user_util.create_user(password='qweqwe')
127 user = user_util.create_user(password='qweqwe')
128 user_id = user.user_id
128 user_id = user.user_id
129 self.log_user(user.username, 'qweqwe')
129 self.log_user(user.username, 'qweqwe')
130
130
131 key_data = self.VALID_KEY
131 key_data = self.VALID_KEY
132
132
133 desc = 'MY SSH KEY'
133 desc = 'MY SSH KEY'
134 response = self.app.post(
134 response = self.app.post(
135 route_path('my_account_ssh_keys_add'),
135 route_path('my_account_ssh_keys_add'),
136 {'description': desc, 'key_data': key_data,
136 {'description': desc, 'key_data': key_data,
137 'csrf_token': self.csrf_token})
137 'csrf_token': self.csrf_token})
138 assert_session_flash(response, 'Ssh Key successfully created')
138 assert_session_flash(response, 'Ssh Key successfully created')
139 response = response.follow() # flush the Session flash
139 response = response.follow() # flush the Session flash
140
140
141 # now delete our key
141 # now delete our key
142 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
142 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
143 assert 1 == len(keys)
143 assert 1 == len(keys)
144
144
145 response = self.app.post(
145 response = self.app.post(
146 route_path('my_account_ssh_keys_delete'),
146 route_path('my_account_ssh_keys_delete'),
147 {'del_ssh_key': keys[0].ssh_key_id,
147 {'del_ssh_key': keys[0].ssh_key_id,
148 'csrf_token': self.csrf_token})
148 'csrf_token': self.csrf_token})
149
149
150 assert_session_flash(response, 'Ssh key successfully deleted')
150 assert_session_flash(response, 'Ssh key successfully deleted')
151 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
151 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
152 assert 0 == len(keys)
152 assert 0 == len(keys)
153
153
154 def test_generate_keypair(self, user_util):
154 def test_generate_keypair(self, user_util):
155 user = user_util.create_user(password='qweqwe')
155 user = user_util.create_user(password='qweqwe')
156 self.log_user(user.username, 'qweqwe')
156 self.log_user(user.username, 'qweqwe')
157
157
158 response = self.app.get(
158 response = self.app.get(
159 route_path('my_account_ssh_keys_generate'))
159 route_path('my_account_ssh_keys_generate'))
160
160
161 response.mustcontain('Private key')
161 response.mustcontain('Private key')
162 response.mustcontain('Public key')
162 response.mustcontain('Public key')
163 response.mustcontain('-----BEGIN PRIVATE KEY-----')
163 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,89 +1,89 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import assert_session_flash
23 from rhodecode.tests import assert_session_flash
24
24
25
25
26 def route_path(name, params=None, **kwargs):
26 def route_path(name, params=None, **kwargs):
27 import urllib
27 import urllib.request, urllib.parse, urllib.error
28
28
29 base_url = {
29 base_url = {
30 'edit_repo_group_advanced':
30 'edit_repo_group_advanced':
31 '/{repo_group_name}/_settings/advanced',
31 '/{repo_group_name}/_settings/advanced',
32 'edit_repo_group_advanced_delete':
32 'edit_repo_group_advanced_delete':
33 '/{repo_group_name}/_settings/advanced/delete',
33 '/{repo_group_name}/_settings/advanced/delete',
34 }[name].format(**kwargs)
34 }[name].format(**kwargs)
35
35
36 if params:
36 if params:
37 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 return base_url
38 return base_url
39
39
40
40
41 @pytest.mark.usefixtures("app")
41 @pytest.mark.usefixtures("app")
42 class TestRepoGroupsAdvancedView(object):
42 class TestRepoGroupsAdvancedView(object):
43
43
44 @pytest.mark.parametrize('repo_group_name', [
44 @pytest.mark.parametrize('repo_group_name', [
45 'gro',
45 'gro',
46 '12345',
46 '12345',
47 ])
47 ])
48 def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
48 def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
49 user_util._test_name = repo_group_name
49 user_util._test_name = repo_group_name
50 gr = user_util.create_repo_group()
50 gr = user_util.create_repo_group()
51 self.app.get(
51 self.app.get(
52 route_path('edit_repo_group_advanced',
52 route_path('edit_repo_group_advanced',
53 repo_group_name=gr.group_name))
53 repo_group_name=gr.group_name))
54
54
55 def test_show_advanced_settings_delete(self, autologin_user, user_util,
55 def test_show_advanced_settings_delete(self, autologin_user, user_util,
56 csrf_token):
56 csrf_token):
57 gr = user_util.create_repo_group(auto_cleanup=False)
57 gr = user_util.create_repo_group(auto_cleanup=False)
58 repo_group_name = gr.group_name
58 repo_group_name = gr.group_name
59
59
60 params = dict(
60 params = dict(
61 csrf_token=csrf_token
61 csrf_token=csrf_token
62 )
62 )
63 response = self.app.post(
63 response = self.app.post(
64 route_path('edit_repo_group_advanced_delete',
64 route_path('edit_repo_group_advanced_delete',
65 repo_group_name=repo_group_name), params=params)
65 repo_group_name=repo_group_name), params=params)
66 assert_session_flash(
66 assert_session_flash(
67 response, 'Removed repository group `{}`'.format(repo_group_name))
67 response, 'Removed repository group `{}`'.format(repo_group_name))
68
68
69 def test_delete_not_possible_with_objects_inside(self, autologin_user,
69 def test_delete_not_possible_with_objects_inside(self, autologin_user,
70 repo_groups, csrf_token):
70 repo_groups, csrf_token):
71 zombie_group, parent_group, child_group = repo_groups
71 zombie_group, parent_group, child_group = repo_groups
72
72
73 response = self.app.get(
73 response = self.app.get(
74 route_path('edit_repo_group_advanced',
74 route_path('edit_repo_group_advanced',
75 repo_group_name=parent_group.group_name))
75 repo_group_name=parent_group.group_name))
76
76
77 response.mustcontain(
77 response.mustcontain(
78 'This repository group includes 1 children repository group')
78 'This repository group includes 1 children repository group')
79
79
80 params = dict(
80 params = dict(
81 csrf_token=csrf_token
81 csrf_token=csrf_token
82 )
82 )
83 response = self.app.post(
83 response = self.app.post(
84 route_path('edit_repo_group_advanced_delete',
84 route_path('edit_repo_group_advanced_delete',
85 repo_group_name=parent_group.group_name), params=params)
85 repo_group_name=parent_group.group_name), params=params)
86
86
87 assert_session_flash(
87 assert_session_flash(
88 response, 'This repository group contains 1 subgroup '
88 response, 'This repository group contains 1 subgroup '
89 'and cannot be deleted')
89 'and cannot be deleted')
@@ -1,86 +1,86 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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.utils import permission_update_data_generator
23 from rhodecode.tests.utils import permission_update_data_generator
24
24
25
25
26 def route_path(name, params=None, **kwargs):
26 def route_path(name, params=None, **kwargs):
27 import urllib
27 import urllib.request, urllib.parse, urllib.error
28
28
29 base_url = {
29 base_url = {
30 'edit_repo_group_perms':
30 'edit_repo_group_perms':
31 '/{repo_group_name:}/_settings/permissions',
31 '/{repo_group_name:}/_settings/permissions',
32 'edit_repo_group_perms_update':
32 'edit_repo_group_perms_update':
33 '/{repo_group_name}/_settings/permissions/update',
33 '/{repo_group_name}/_settings/permissions/update',
34 }[name].format(**kwargs)
34 }[name].format(**kwargs)
35
35
36 if params:
36 if params:
37 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 return base_url
38 return base_url
39
39
40
40
41 @pytest.mark.usefixtures("app")
41 @pytest.mark.usefixtures("app")
42 class TestRepoGroupPermissionsView(object):
42 class TestRepoGroupPermissionsView(object):
43
43
44 def test_edit_perms_view(self, user_util, autologin_user):
44 def test_edit_perms_view(self, user_util, autologin_user):
45 repo_group = user_util.create_repo_group()
45 repo_group = user_util.create_repo_group()
46
46
47 self.app.get(
47 self.app.get(
48 route_path('edit_repo_group_perms',
48 route_path('edit_repo_group_perms',
49 repo_group_name=repo_group.group_name), status=200)
49 repo_group_name=repo_group.group_name), status=200)
50
50
51 def test_update_permissions(self, csrf_token, user_util):
51 def test_update_permissions(self, csrf_token, user_util):
52 repo_group = user_util.create_repo_group()
52 repo_group = user_util.create_repo_group()
53 repo_group_name = repo_group.group_name
53 repo_group_name = repo_group.group_name
54 user = user_util.create_user()
54 user = user_util.create_user()
55 user_id = user.user_id
55 user_id = user.user_id
56 username = user.username
56 username = user.username
57
57
58 # grant new
58 # grant new
59 form_data = permission_update_data_generator(
59 form_data = permission_update_data_generator(
60 csrf_token,
60 csrf_token,
61 default='group.write',
61 default='group.write',
62 grant=[(user_id, 'group.write', username, 'user')])
62 grant=[(user_id, 'group.write', username, 'user')])
63
63
64 # recursive flag required for repo groups
64 # recursive flag required for repo groups
65 form_data.extend([('recursive', u'none')])
65 form_data.extend([('recursive', u'none')])
66
66
67 response = self.app.post(
67 response = self.app.post(
68 route_path('edit_repo_group_perms_update',
68 route_path('edit_repo_group_perms_update',
69 repo_group_name=repo_group_name), form_data).follow()
69 repo_group_name=repo_group_name), form_data).follow()
70
70
71 assert 'Repository Group permissions updated' in response
71 assert 'Repository Group permissions updated' in response
72
72
73 # revoke given
73 # revoke given
74 form_data = permission_update_data_generator(
74 form_data = permission_update_data_generator(
75 csrf_token,
75 csrf_token,
76 default='group.read',
76 default='group.read',
77 revoke=[(user_id, 'user')])
77 revoke=[(user_id, 'user')])
78
78
79 # recursive flag required for repo groups
79 # recursive flag required for repo groups
80 form_data.extend([('recursive', u'none')])
80 form_data.extend([('recursive', u'none')])
81
81
82 response = self.app.post(
82 response = self.app.post(
83 route_path('edit_repo_group_perms_update',
83 route_path('edit_repo_group_perms_update',
84 repo_group_name=repo_group_name), form_data).follow()
84 repo_group_name=repo_group_name), form_data).follow()
85
85
86 assert 'Repository Group permissions updated' in response
86 assert 'Repository Group permissions updated' in response
@@ -1,90 +1,90 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import assert_session_flash
23 from rhodecode.tests import assert_session_flash
24
24
25
25
26 def route_path(name, params=None, **kwargs):
26 def route_path(name, params=None, **kwargs):
27 import urllib
27 import urllib.request, urllib.parse, urllib.error
28
28
29 base_url = {
29 base_url = {
30 'edit_repo_group': '/{repo_group_name}/_edit',
30 'edit_repo_group': '/{repo_group_name}/_edit',
31 # Update is POST to the above url
31 # Update is POST to the above url
32 }[name].format(**kwargs)
32 }[name].format(**kwargs)
33
33
34 if params:
34 if params:
35 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
36 return base_url
36 return base_url
37
37
38
38
39 @pytest.mark.usefixtures("app")
39 @pytest.mark.usefixtures("app")
40 class TestRepoGroupsSettingsView(object):
40 class TestRepoGroupsSettingsView(object):
41
41
42 @pytest.mark.parametrize('repo_group_name', [
42 @pytest.mark.parametrize('repo_group_name', [
43 'gro',
43 'gro',
44 u'12345',
44 u'12345',
45 ])
45 ])
46 def test_edit(self, user_util, autologin_user, repo_group_name):
46 def test_edit(self, user_util, autologin_user, repo_group_name):
47 user_util._test_name = repo_group_name
47 user_util._test_name = repo_group_name
48 repo_group = user_util.create_repo_group()
48 repo_group = user_util.create_repo_group()
49
49
50 self.app.get(
50 self.app.get(
51 route_path('edit_repo_group', repo_group_name=repo_group.group_name),
51 route_path('edit_repo_group', repo_group_name=repo_group.group_name),
52 status=200)
52 status=200)
53
53
54 def test_update(self, csrf_token, autologin_user, user_util, rc_fixture):
54 def test_update(self, csrf_token, autologin_user, user_util, rc_fixture):
55 repo_group = user_util.create_repo_group()
55 repo_group = user_util.create_repo_group()
56 repo_group_name = repo_group.group_name
56 repo_group_name = repo_group.group_name
57
57
58 description = 'description for newly created repo group'
58 description = 'description for newly created repo group'
59 form_data = rc_fixture._get_group_create_params(
59 form_data = rc_fixture._get_group_create_params(
60 group_name=repo_group.group_name,
60 group_name=repo_group.group_name,
61 group_description=description,
61 group_description=description,
62 csrf_token=csrf_token,
62 csrf_token=csrf_token,
63 repo_group_name=repo_group.group_name,
63 repo_group_name=repo_group.group_name,
64 repo_group_owner=repo_group.user.username)
64 repo_group_owner=repo_group.user.username)
65
65
66 response = self.app.post(
66 response = self.app.post(
67 route_path('edit_repo_group',
67 route_path('edit_repo_group',
68 repo_group_name=repo_group.group_name),
68 repo_group_name=repo_group.group_name),
69 form_data,
69 form_data,
70 status=302)
70 status=302)
71
71
72 assert_session_flash(
72 assert_session_flash(
73 response, 'Repository Group `{}` updated successfully'.format(
73 response, 'Repository Group `{}` updated successfully'.format(
74 repo_group_name))
74 repo_group_name))
75
75
76 def test_update_fails_when_parent_pointing_to_self(
76 def test_update_fails_when_parent_pointing_to_self(
77 self, csrf_token, user_util, autologin_user, rc_fixture):
77 self, csrf_token, user_util, autologin_user, rc_fixture):
78 group = user_util.create_repo_group()
78 group = user_util.create_repo_group()
79 response = self.app.post(
79 response = self.app.post(
80 route_path('edit_repo_group', repo_group_name=group.group_name),
80 route_path('edit_repo_group', repo_group_name=group.group_name),
81 rc_fixture._get_group_create_params(
81 rc_fixture._get_group_create_params(
82 repo_group_name=group.group_name,
82 repo_group_name=group.group_name,
83 repo_group_owner=group.user.username,
83 repo_group_owner=group.user.username,
84 repo_group=group.group_id,
84 repo_group=group.group_id,
85 csrf_token=csrf_token),
85 csrf_token=csrf_token),
86 status=200
86 status=200
87 )
87 )
88 response.mustcontain(
88 response.mustcontain(
89 '<span class="error-message">"{}" is not one of -1'.format(
89 '<span class="error-message">"{}" is not one of -1'.format(
90 group.group_id))
90 group.group_id))
@@ -1,84 +1,84 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 rhodecode.model.db import Repository
22 from rhodecode.model.db import Repository
23
23
24
24
25 def route_path(name, params=None, **kwargs):
25 def route_path(name, params=None, **kwargs):
26 import urllib
26 import urllib.request, urllib.parse, urllib.error
27
27
28 base_url = {
28 base_url = {
29 'pullrequest_show_all': '/{repo_name}/pull-request',
29 'pullrequest_show_all': '/{repo_name}/pull-request',
30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
31 }[name].format(**kwargs)
31 }[name].format(**kwargs)
32
32
33 if params:
33 if params:
34 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
35 return base_url
35 return base_url
36
36
37
37
38 @pytest.mark.backends("git", "hg")
38 @pytest.mark.backends("git", "hg")
39 @pytest.mark.usefixtures('autologin_user', 'app')
39 @pytest.mark.usefixtures('autologin_user', 'app')
40 class TestPullRequestList(object):
40 class TestPullRequestList(object):
41
41
42 @pytest.mark.parametrize('params, expected_title', [
42 @pytest.mark.parametrize('params, expected_title', [
43 ({'source': 0, 'closed': 1}, 'Closed'),
43 ({'source': 0, 'closed': 1}, 'Closed'),
44 ({'source': 0, 'my': 1}, 'Created by me'),
44 ({'source': 0, 'my': 1}, 'Created by me'),
45 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
45 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
46 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
46 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
47 ({'source': 1}, 'From this repo'),
47 ({'source': 1}, 'From this repo'),
48 ])
48 ])
49 def test_showing_list_page(self, backend, pr_util, params, expected_title):
49 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 pull_request = pr_util.create_pull_request()
50 pull_request = pr_util.create_pull_request()
51
51
52 response = self.app.get(
52 response = self.app.get(
53 route_path('pullrequest_show_all',
53 route_path('pullrequest_show_all',
54 repo_name=pull_request.target_repo.repo_name,
54 repo_name=pull_request.target_repo.repo_name,
55 params=params))
55 params=params))
56
56
57 assert_response = response.assert_response()
57 assert_response = response.assert_response()
58
58
59 element = assert_response.get_element('.title .active')
59 element = assert_response.get_element('.title .active')
60 element_text = element.text_content()
60 element_text = element.text_content()
61 assert expected_title == element_text
61 assert expected_title == element_text
62
62
63 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
63 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
64 pull_request = pr_util.create_pull_request()
64 pull_request = pr_util.create_pull_request()
65 response = self.app.get(
65 response = self.app.get(
66 route_path('pullrequest_show_all_data',
66 route_path('pullrequest_show_all_data',
67 repo_name=pull_request.target_repo.repo_name),
67 repo_name=pull_request.target_repo.repo_name),
68 extra_environ=xhr_header)
68 extra_environ=xhr_header)
69
69
70 assert response.json['recordsTotal'] == 1
70 assert response.json['recordsTotal'] == 1
71 assert response.json['data'][0]['description'] == 'Description'
71 assert response.json['data'][0]['description'] == 'Description'
72
72
73 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
73 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
74 xss_description = "<script>alert('Hi!')</script>"
74 xss_description = "<script>alert('Hi!')</script>"
75 pull_request = pr_util.create_pull_request(description=xss_description)
75 pull_request = pr_util.create_pull_request(description=xss_description)
76
76
77 response = self.app.get(
77 response = self.app.get(
78 route_path('pullrequest_show_all_data',
78 route_path('pullrequest_show_all_data',
79 repo_name=pull_request.target_repo.repo_name),
79 repo_name=pull_request.target_repo.repo_name),
80 extra_environ=xhr_header)
80 extra_environ=xhr_header)
81
81
82 assert response.json['recordsTotal'] == 1
82 assert response.json['recordsTotal'] == 1
83 assert response.json['data'][0]['description'] == \
83 assert response.json['data'][0]['description'] == \
84 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
84 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -1,52 +1,52 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 rhodecode.model.db import Repository
22 from rhodecode.model.db import Repository
23
23
24
24
25 def route_path(name, params=None, **kwargs):
25 def route_path(name, params=None, **kwargs):
26 import urllib
26 import urllib.request, urllib.parse, urllib.error
27
27
28 base_url = {
28 base_url = {
29 'bookmarks_home': '/{repo_name}/bookmarks',
29 'bookmarks_home': '/{repo_name}/bookmarks',
30 }[name].format(**kwargs)
30 }[name].format(**kwargs)
31
31
32 if params:
32 if params:
33 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
33 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
34 return base_url
34 return base_url
35
35
36
36
37 @pytest.mark.usefixtures('autologin_user', 'app')
37 @pytest.mark.usefixtures('autologin_user', 'app')
38 class TestBookmarks(object):
38 class TestBookmarks(object):
39
39
40 def test_index(self, backend):
40 def test_index(self, backend):
41 if backend.alias == 'hg':
41 if backend.alias == 'hg':
42 response = self.app.get(
42 response = self.app.get(
43 route_path('bookmarks_home', repo_name=backend.repo_name))
43 route_path('bookmarks_home', repo_name=backend.repo_name))
44
44
45 repo = Repository.get_by_repo_name(backend.repo_name)
45 repo = Repository.get_by_repo_name(backend.repo_name)
46 for commit_id, obj_name in repo.scm_instance().bookmarks.items():
46 for commit_id, obj_name in repo.scm_instance().bookmarks.items():
47 assert commit_id in response
47 assert commit_id in response
48 assert obj_name in response
48 assert obj_name in response
49 else:
49 else:
50 self.app.get(
50 self.app.get(
51 route_path('bookmarks_home', repo_name=backend.repo_name),
51 route_path('bookmarks_home', repo_name=backend.repo_name),
52 status=404)
52 status=404)
@@ -1,48 +1,48 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 rhodecode.model.db import Repository
22 from rhodecode.model.db import Repository
23
23
24
24
25 def route_path(name, params=None, **kwargs):
25 def route_path(name, params=None, **kwargs):
26 import urllib
26 import urllib.request, urllib.parse, urllib.error
27
27
28 base_url = {
28 base_url = {
29 'branches_home': '/{repo_name}/branches',
29 'branches_home': '/{repo_name}/branches',
30 }[name].format(**kwargs)
30 }[name].format(**kwargs)
31
31
32 if params:
32 if params:
33 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
33 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
34 return base_url
34 return base_url
35
35
36
36
37 @pytest.mark.usefixtures('autologin_user', 'app')
37 @pytest.mark.usefixtures('autologin_user', 'app')
38 class TestBranchesController(object):
38 class TestBranchesController(object):
39
39
40 def test_index(self, backend):
40 def test_index(self, backend):
41 response = self.app.get(
41 response = self.app.get(
42 route_path('branches_home', repo_name=backend.repo_name))
42 route_path('branches_home', repo_name=backend.repo_name))
43
43
44 repo = Repository.get_by_repo_name(backend.repo_name)
44 repo = Repository.get_by_repo_name(backend.repo_name)
45
45
46 for commit_id, obj_name in repo.scm_instance().branches.items():
46 for commit_id, obj_name in repo.scm_instance().branches.items():
47 assert commit_id in response
47 assert commit_id in response
48 assert obj_name in response
48 assert obj_name in response
@@ -1,220 +1,220 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 re
21 import re
22
22
23 import pytest
23 import pytest
24
24
25 from rhodecode.apps.repository.views.repo_changelog import DEFAULT_CHANGELOG_SIZE
25 from rhodecode.apps.repository.views.repo_changelog import DEFAULT_CHANGELOG_SIZE
26 from rhodecode.tests import TestController
26 from rhodecode.tests import TestController
27
27
28 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
28 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
29
29
30
30
31 def route_path(name, params=None, **kwargs):
31 def route_path(name, params=None, **kwargs):
32 import urllib
32 import urllib.request, urllib.parse, urllib.error
33
33
34 base_url = {
34 base_url = {
35 'repo_changelog': '/{repo_name}/changelog',
35 'repo_changelog': '/{repo_name}/changelog',
36 'repo_commits': '/{repo_name}/commits',
36 'repo_commits': '/{repo_name}/commits',
37 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
37 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
38 'repo_commits_elements': '/{repo_name}/commits_elements',
38 'repo_commits_elements': '/{repo_name}/commits_elements',
39 }[name].format(**kwargs)
39 }[name].format(**kwargs)
40
40
41 if params:
41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 return base_url
43 return base_url
44
44
45
45
46 def assert_commits_on_page(response, indexes):
46 def assert_commits_on_page(response, indexes):
47 found_indexes = [int(idx) for idx in MATCH_HASH.findall(response.body)]
47 found_indexes = [int(idx) for idx in MATCH_HASH.findall(response.body)]
48 assert found_indexes == indexes
48 assert found_indexes == indexes
49
49
50
50
51 class TestChangelogController(TestController):
51 class TestChangelogController(TestController):
52
52
53 def test_commits_page(self, backend):
53 def test_commits_page(self, backend):
54 self.log_user()
54 self.log_user()
55 response = self.app.get(
55 response = self.app.get(
56 route_path('repo_commits', repo_name=backend.repo_name))
56 route_path('repo_commits', repo_name=backend.repo_name))
57
57
58 first_idx = -1
58 first_idx = -1
59 last_idx = -DEFAULT_CHANGELOG_SIZE
59 last_idx = -DEFAULT_CHANGELOG_SIZE
60 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
60 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
61
61
62 def test_changelog(self, backend):
62 def test_changelog(self, backend):
63 self.log_user()
63 self.log_user()
64 response = self.app.get(
64 response = self.app.get(
65 route_path('repo_changelog', repo_name=backend.repo_name))
65 route_path('repo_changelog', repo_name=backend.repo_name))
66
66
67 first_idx = -1
67 first_idx = -1
68 last_idx = -DEFAULT_CHANGELOG_SIZE
68 last_idx = -DEFAULT_CHANGELOG_SIZE
69 self.assert_commit_range_on_page(
69 self.assert_commit_range_on_page(
70 response, first_idx, last_idx, backend)
70 response, first_idx, last_idx, backend)
71
71
72 @pytest.mark.backends("hg", "git")
72 @pytest.mark.backends("hg", "git")
73 def test_changelog_filtered_by_branch(self, backend):
73 def test_changelog_filtered_by_branch(self, backend):
74 self.log_user()
74 self.log_user()
75 self.app.get(
75 self.app.get(
76 route_path('repo_changelog', repo_name=backend.repo_name,
76 route_path('repo_changelog', repo_name=backend.repo_name,
77 params=dict(branch=backend.default_branch_name)),
77 params=dict(branch=backend.default_branch_name)),
78 status=200)
78 status=200)
79
79
80 @pytest.mark.backends("hg", "git")
80 @pytest.mark.backends("hg", "git")
81 def test_commits_filtered_by_branch(self, backend):
81 def test_commits_filtered_by_branch(self, backend):
82 self.log_user()
82 self.log_user()
83 self.app.get(
83 self.app.get(
84 route_path('repo_commits', repo_name=backend.repo_name,
84 route_path('repo_commits', repo_name=backend.repo_name,
85 params=dict(branch=backend.default_branch_name)),
85 params=dict(branch=backend.default_branch_name)),
86 status=200)
86 status=200)
87
87
88 @pytest.mark.backends("svn")
88 @pytest.mark.backends("svn")
89 def test_changelog_filtered_by_branch_svn(self, autologin_user, backend):
89 def test_changelog_filtered_by_branch_svn(self, autologin_user, backend):
90 repo = backend['svn-simple-layout']
90 repo = backend['svn-simple-layout']
91 response = self.app.get(
91 response = self.app.get(
92 route_path('repo_changelog', repo_name=repo.repo_name,
92 route_path('repo_changelog', repo_name=repo.repo_name,
93 params=dict(branch='trunk')),
93 params=dict(branch='trunk')),
94 status=200)
94 status=200)
95
95
96 assert_commits_on_page(response, indexes=[15, 12, 7, 3, 2, 1])
96 assert_commits_on_page(response, indexes=[15, 12, 7, 3, 2, 1])
97
97
98 def test_commits_filtered_by_wrong_branch(self, backend):
98 def test_commits_filtered_by_wrong_branch(self, backend):
99 self.log_user()
99 self.log_user()
100 branch = 'wrong-branch-name'
100 branch = 'wrong-branch-name'
101 response = self.app.get(
101 response = self.app.get(
102 route_path('repo_commits', repo_name=backend.repo_name,
102 route_path('repo_commits', repo_name=backend.repo_name,
103 params=dict(branch=branch)),
103 params=dict(branch=branch)),
104 status=302)
104 status=302)
105 expected_url = '/{repo}/commits/{branch}'.format(
105 expected_url = '/{repo}/commits/{branch}'.format(
106 repo=backend.repo_name, branch=branch)
106 repo=backend.repo_name, branch=branch)
107 assert expected_url in response.location
107 assert expected_url in response.location
108 response = response.follow()
108 response = response.follow()
109 expected_warning = 'Branch {} is not found.'.format(branch)
109 expected_warning = 'Branch {} is not found.'.format(branch)
110 assert expected_warning in response.body
110 assert expected_warning in response.body
111
111
112 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
112 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
113 def test_changelog_filtered_by_branch_with_merges(
113 def test_changelog_filtered_by_branch_with_merges(
114 self, autologin_user, backend):
114 self, autologin_user, backend):
115
115
116 # Note: The changelog of branch "b" does not contain the commit "a1"
116 # Note: The changelog of branch "b" does not contain the commit "a1"
117 # although this is a parent of commit "b1". And branch "b" has commits
117 # although this is a parent of commit "b1". And branch "b" has commits
118 # which have a smaller index than commit "a1".
118 # which have a smaller index than commit "a1".
119 commits = [
119 commits = [
120 {'message': 'a'},
120 {'message': 'a'},
121 {'message': 'b', 'branch': 'b'},
121 {'message': 'b', 'branch': 'b'},
122 {'message': 'a1', 'parents': ['a']},
122 {'message': 'a1', 'parents': ['a']},
123 {'message': 'b1', 'branch': 'b', 'parents': ['b', 'a1']},
123 {'message': 'b1', 'branch': 'b', 'parents': ['b', 'a1']},
124 ]
124 ]
125 backend.create_repo(commits)
125 backend.create_repo(commits)
126
126
127 self.app.get(
127 self.app.get(
128 route_path('repo_changelog', repo_name=backend.repo_name,
128 route_path('repo_changelog', repo_name=backend.repo_name,
129 params=dict(branch='b')),
129 params=dict(branch='b')),
130 status=200)
130 status=200)
131
131
132 @pytest.mark.backends("hg")
132 @pytest.mark.backends("hg")
133 def test_commits_closed_branches(self, autologin_user, backend):
133 def test_commits_closed_branches(self, autologin_user, backend):
134 repo = backend['closed_branch']
134 repo = backend['closed_branch']
135 response = self.app.get(
135 response = self.app.get(
136 route_path('repo_commits', repo_name=repo.repo_name,
136 route_path('repo_commits', repo_name=repo.repo_name,
137 params=dict(branch='experimental')),
137 params=dict(branch='experimental')),
138 status=200)
138 status=200)
139
139
140 assert_commits_on_page(response, indexes=[3, 1])
140 assert_commits_on_page(response, indexes=[3, 1])
141
141
142 def test_changelog_pagination(self, backend):
142 def test_changelog_pagination(self, backend):
143 self.log_user()
143 self.log_user()
144 # pagination, walk up to page 6
144 # pagination, walk up to page 6
145 changelog_url = route_path(
145 changelog_url = route_path(
146 'repo_commits', repo_name=backend.repo_name)
146 'repo_commits', repo_name=backend.repo_name)
147
147
148 for page in range(1, 7):
148 for page in range(1, 7):
149 response = self.app.get(changelog_url, {'page': page})
149 response = self.app.get(changelog_url, {'page': page})
150
150
151 first_idx = -DEFAULT_CHANGELOG_SIZE * (page - 1) - 1
151 first_idx = -DEFAULT_CHANGELOG_SIZE * (page - 1) - 1
152 last_idx = -DEFAULT_CHANGELOG_SIZE * page
152 last_idx = -DEFAULT_CHANGELOG_SIZE * page
153 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
153 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
154
154
155 def assert_commit_range_on_page(
155 def assert_commit_range_on_page(
156 self, response, first_idx, last_idx, backend):
156 self, response, first_idx, last_idx, backend):
157 input_template = (
157 input_template = (
158 """<input class="commit-range" """
158 """<input class="commit-range" """
159 """data-commit-id="%(raw_id)s" data-commit-idx="%(idx)s" """
159 """data-commit-id="%(raw_id)s" data-commit-idx="%(idx)s" """
160 """data-short-id="%(short_id)s" id="%(raw_id)s" """
160 """data-short-id="%(short_id)s" id="%(raw_id)s" """
161 """name="%(raw_id)s" type="checkbox" value="1" />"""
161 """name="%(raw_id)s" type="checkbox" value="1" />"""
162 )
162 )
163
163
164 commit_span_template = """<span class="commit_hash">r%s:%s</span>"""
164 commit_span_template = """<span class="commit_hash">r%s:%s</span>"""
165 repo = backend.repo
165 repo = backend.repo
166
166
167 first_commit_on_page = repo.get_commit(commit_idx=first_idx)
167 first_commit_on_page = repo.get_commit(commit_idx=first_idx)
168 response.mustcontain(
168 response.mustcontain(
169 input_template % {'raw_id': first_commit_on_page.raw_id,
169 input_template % {'raw_id': first_commit_on_page.raw_id,
170 'idx': first_commit_on_page.idx,
170 'idx': first_commit_on_page.idx,
171 'short_id': first_commit_on_page.short_id})
171 'short_id': first_commit_on_page.short_id})
172
172
173 response.mustcontain(commit_span_template % (
173 response.mustcontain(commit_span_template % (
174 first_commit_on_page.idx, first_commit_on_page.short_id)
174 first_commit_on_page.idx, first_commit_on_page.short_id)
175 )
175 )
176
176
177 last_commit_on_page = repo.get_commit(commit_idx=last_idx)
177 last_commit_on_page = repo.get_commit(commit_idx=last_idx)
178 response.mustcontain(
178 response.mustcontain(
179 input_template % {'raw_id': last_commit_on_page.raw_id,
179 input_template % {'raw_id': last_commit_on_page.raw_id,
180 'idx': last_commit_on_page.idx,
180 'idx': last_commit_on_page.idx,
181 'short_id': last_commit_on_page.short_id})
181 'short_id': last_commit_on_page.short_id})
182 response.mustcontain(commit_span_template % (
182 response.mustcontain(commit_span_template % (
183 last_commit_on_page.idx, last_commit_on_page.short_id)
183 last_commit_on_page.idx, last_commit_on_page.short_id)
184 )
184 )
185
185
186 first_commit_of_next_page = repo.get_commit(commit_idx=last_idx - 1)
186 first_commit_of_next_page = repo.get_commit(commit_idx=last_idx - 1)
187 first_span_of_next_page = commit_span_template % (
187 first_span_of_next_page = commit_span_template % (
188 first_commit_of_next_page.idx, first_commit_of_next_page.short_id)
188 first_commit_of_next_page.idx, first_commit_of_next_page.short_id)
189 assert first_span_of_next_page not in response
189 assert first_span_of_next_page not in response
190
190
191 @pytest.mark.parametrize('test_path', [
191 @pytest.mark.parametrize('test_path', [
192 'vcs/exceptions.py',
192 'vcs/exceptions.py',
193 '/vcs/exceptions.py',
193 '/vcs/exceptions.py',
194 '//vcs/exceptions.py'
194 '//vcs/exceptions.py'
195 ])
195 ])
196 def test_commits_with_filenode(self, backend, test_path):
196 def test_commits_with_filenode(self, backend, test_path):
197 self.log_user()
197 self.log_user()
198 response = self.app.get(
198 response = self.app.get(
199 route_path('repo_commits_file', repo_name=backend.repo_name,
199 route_path('repo_commits_file', repo_name=backend.repo_name,
200 commit_id='tip', f_path=test_path),
200 commit_id='tip', f_path=test_path),
201 )
201 )
202
202
203 # history commits messages
203 # history commits messages
204 response.mustcontain('Added exceptions module, this time for real')
204 response.mustcontain('Added exceptions module, this time for real')
205 response.mustcontain('Added not implemented hg backend test case')
205 response.mustcontain('Added not implemented hg backend test case')
206 response.mustcontain('Added BaseChangeset class')
206 response.mustcontain('Added BaseChangeset class')
207
207
208 def test_commits_with_filenode_that_is_dirnode(self, backend):
208 def test_commits_with_filenode_that_is_dirnode(self, backend):
209 self.log_user()
209 self.log_user()
210 self.app.get(
210 self.app.get(
211 route_path('repo_commits_file', repo_name=backend.repo_name,
211 route_path('repo_commits_file', repo_name=backend.repo_name,
212 commit_id='tip', f_path='/tests'),
212 commit_id='tip', f_path='/tests'),
213 status=302)
213 status=302)
214
214
215 def test_commits_with_filenode_not_existing(self, backend):
215 def test_commits_with_filenode_not_existing(self, backend):
216 self.log_user()
216 self.log_user()
217 self.app.get(
217 self.app.get(
218 route_path('repo_commits_file', repo_name=backend.repo_name,
218 route_path('repo_commits_file', repo_name=backend.repo_name,
219 commit_id='tip', f_path='wrong_path'),
219 commit_id='tip', f_path='wrong_path'),
220 status=302)
220 status=302)
@@ -1,494 +1,494 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import TestController
23 from rhodecode.tests import TestController
24
24
25 from rhodecode.model.db import ChangesetComment, Notification
25 from rhodecode.model.db import ChangesetComment, Notification
26 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28
28
29
29
30 def route_path(name, params=None, **kwargs):
30 def route_path(name, params=None, **kwargs):
31 import urllib
31 import urllib.request, urllib.parse, urllib.error
32
32
33 base_url = {
33 base_url = {
34 'repo_commit': '/{repo_name}/changeset/{commit_id}',
34 'repo_commit': '/{repo_name}/changeset/{commit_id}',
35 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
35 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
36 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
36 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
37 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
37 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
38 'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
38 'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
39 }[name].format(**kwargs)
39 }[name].format(**kwargs)
40
40
41 if params:
41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 return base_url
43 return base_url
44
44
45
45
46 @pytest.mark.backends("git", "hg", "svn")
46 @pytest.mark.backends("git", "hg", "svn")
47 class TestRepoCommitCommentsView(TestController):
47 class TestRepoCommitCommentsView(TestController):
48
48
49 @pytest.fixture(autouse=True)
49 @pytest.fixture(autouse=True)
50 def prepare(self, request, baseapp):
50 def prepare(self, request, baseapp):
51 for x in ChangesetComment.query().all():
51 for x in ChangesetComment.query().all():
52 Session().delete(x)
52 Session().delete(x)
53 Session().commit()
53 Session().commit()
54
54
55 for x in Notification.query().all():
55 for x in Notification.query().all():
56 Session().delete(x)
56 Session().delete(x)
57 Session().commit()
57 Session().commit()
58
58
59 request.addfinalizer(self.cleanup)
59 request.addfinalizer(self.cleanup)
60
60
61 def cleanup(self):
61 def cleanup(self):
62 for x in ChangesetComment.query().all():
62 for x in ChangesetComment.query().all():
63 Session().delete(x)
63 Session().delete(x)
64 Session().commit()
64 Session().commit()
65
65
66 for x in Notification.query().all():
66 for x in Notification.query().all():
67 Session().delete(x)
67 Session().delete(x)
68 Session().commit()
68 Session().commit()
69
69
70 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
70 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
71 def test_create(self, comment_type, backend):
71 def test_create(self, comment_type, backend):
72 self.log_user()
72 self.log_user()
73 commit = backend.repo.get_commit('300')
73 commit = backend.repo.get_commit('300')
74 commit_id = commit.raw_id
74 commit_id = commit.raw_id
75 text = u'CommentOnCommit'
75 text = u'CommentOnCommit'
76
76
77 params = {'text': text, 'csrf_token': self.csrf_token,
77 params = {'text': text, 'csrf_token': self.csrf_token,
78 'comment_type': comment_type}
78 'comment_type': comment_type}
79 self.app.post(
79 self.app.post(
80 route_path('repo_commit_comment_create',
80 route_path('repo_commit_comment_create',
81 repo_name=backend.repo_name, commit_id=commit_id),
81 repo_name=backend.repo_name, commit_id=commit_id),
82 params=params)
82 params=params)
83
83
84 response = self.app.get(
84 response = self.app.get(
85 route_path('repo_commit',
85 route_path('repo_commit',
86 repo_name=backend.repo_name, commit_id=commit_id))
86 repo_name=backend.repo_name, commit_id=commit_id))
87
87
88 # test DB
88 # test DB
89 assert ChangesetComment.query().count() == 1
89 assert ChangesetComment.query().count() == 1
90 assert_comment_links(response, ChangesetComment.query().count(), 0)
90 assert_comment_links(response, ChangesetComment.query().count(), 0)
91
91
92 assert Notification.query().count() == 1
92 assert Notification.query().count() == 1
93 assert ChangesetComment.query().count() == 1
93 assert ChangesetComment.query().count() == 1
94
94
95 notification = Notification.query().all()[0]
95 notification = Notification.query().all()[0]
96
96
97 comment_id = ChangesetComment.query().first().comment_id
97 comment_id = ChangesetComment.query().first().comment_id
98 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
98 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
99
99
100 author = notification.created_by_user.username_and_name
100 author = notification.created_by_user.username_and_name
101 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
101 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
102 author, comment_type, h.show_id(commit), backend.repo_name)
102 author, comment_type, h.show_id(commit), backend.repo_name)
103 assert sbj == notification.subject
103 assert sbj == notification.subject
104
104
105 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
105 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
106 backend.repo_name, commit_id, comment_id))
106 backend.repo_name, commit_id, comment_id))
107 assert lnk in notification.body
107 assert lnk in notification.body
108
108
109 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
109 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
110 def test_create_inline(self, comment_type, backend):
110 def test_create_inline(self, comment_type, backend):
111 self.log_user()
111 self.log_user()
112 commit = backend.repo.get_commit('300')
112 commit = backend.repo.get_commit('300')
113 commit_id = commit.raw_id
113 commit_id = commit.raw_id
114 text = u'CommentOnCommit'
114 text = u'CommentOnCommit'
115 f_path = 'vcs/web/simplevcs/views/repository.py'
115 f_path = 'vcs/web/simplevcs/views/repository.py'
116 line = 'n1'
116 line = 'n1'
117
117
118 params = {'text': text, 'f_path': f_path, 'line': line,
118 params = {'text': text, 'f_path': f_path, 'line': line,
119 'comment_type': comment_type,
119 'comment_type': comment_type,
120 'csrf_token': self.csrf_token}
120 'csrf_token': self.csrf_token}
121
121
122 self.app.post(
122 self.app.post(
123 route_path('repo_commit_comment_create',
123 route_path('repo_commit_comment_create',
124 repo_name=backend.repo_name, commit_id=commit_id),
124 repo_name=backend.repo_name, commit_id=commit_id),
125 params=params)
125 params=params)
126
126
127 response = self.app.get(
127 response = self.app.get(
128 route_path('repo_commit',
128 route_path('repo_commit',
129 repo_name=backend.repo_name, commit_id=commit_id))
129 repo_name=backend.repo_name, commit_id=commit_id))
130
130
131 # test DB
131 # test DB
132 assert ChangesetComment.query().count() == 1
132 assert ChangesetComment.query().count() == 1
133 assert_comment_links(response, 0, ChangesetComment.query().count())
133 assert_comment_links(response, 0, ChangesetComment.query().count())
134
134
135 if backend.alias == 'svn':
135 if backend.alias == 'svn':
136 response.mustcontain(
136 response.mustcontain(
137 '''data-f-path="vcs/commands/summary.py" '''
137 '''data-f-path="vcs/commands/summary.py" '''
138 '''data-anchor-id="c-300-ad05457a43f8"'''
138 '''data-anchor-id="c-300-ad05457a43f8"'''
139 )
139 )
140 if backend.alias == 'git':
140 if backend.alias == 'git':
141 response.mustcontain(
141 response.mustcontain(
142 '''data-f-path="vcs/backends/hg.py" '''
142 '''data-f-path="vcs/backends/hg.py" '''
143 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
143 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
144 )
144 )
145
145
146 if backend.alias == 'hg':
146 if backend.alias == 'hg':
147 response.mustcontain(
147 response.mustcontain(
148 '''data-f-path="vcs/backends/hg.py" '''
148 '''data-f-path="vcs/backends/hg.py" '''
149 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
149 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
150 )
150 )
151
151
152 assert Notification.query().count() == 1
152 assert Notification.query().count() == 1
153 assert ChangesetComment.query().count() == 1
153 assert ChangesetComment.query().count() == 1
154
154
155 notification = Notification.query().all()[0]
155 notification = Notification.query().all()[0]
156 comment = ChangesetComment.query().first()
156 comment = ChangesetComment.query().first()
157 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
157 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
158
158
159 assert comment.revision == commit_id
159 assert comment.revision == commit_id
160
160
161 author = notification.created_by_user.username_and_name
161 author = notification.created_by_user.username_and_name
162 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
162 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
163 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
163 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
164
164
165 assert sbj == notification.subject
165 assert sbj == notification.subject
166
166
167 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
167 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
168 backend.repo_name, commit_id, comment.comment_id))
168 backend.repo_name, commit_id, comment.comment_id))
169 assert lnk in notification.body
169 assert lnk in notification.body
170 assert 'on line n1' in notification.body
170 assert 'on line n1' in notification.body
171
171
172 def test_create_with_mention(self, backend):
172 def test_create_with_mention(self, backend):
173 self.log_user()
173 self.log_user()
174
174
175 commit_id = backend.repo.get_commit('300').raw_id
175 commit_id = backend.repo.get_commit('300').raw_id
176 text = u'@test_regular check CommentOnCommit'
176 text = u'@test_regular check CommentOnCommit'
177
177
178 params = {'text': text, 'csrf_token': self.csrf_token}
178 params = {'text': text, 'csrf_token': self.csrf_token}
179 self.app.post(
179 self.app.post(
180 route_path('repo_commit_comment_create',
180 route_path('repo_commit_comment_create',
181 repo_name=backend.repo_name, commit_id=commit_id),
181 repo_name=backend.repo_name, commit_id=commit_id),
182 params=params)
182 params=params)
183
183
184 response = self.app.get(
184 response = self.app.get(
185 route_path('repo_commit',
185 route_path('repo_commit',
186 repo_name=backend.repo_name, commit_id=commit_id))
186 repo_name=backend.repo_name, commit_id=commit_id))
187 # test DB
187 # test DB
188 assert ChangesetComment.query().count() == 1
188 assert ChangesetComment.query().count() == 1
189 assert_comment_links(response, ChangesetComment.query().count(), 0)
189 assert_comment_links(response, ChangesetComment.query().count(), 0)
190
190
191 notification = Notification.query().one()
191 notification = Notification.query().one()
192
192
193 assert len(notification.recipients) == 2
193 assert len(notification.recipients) == 2
194 users = [x.username for x in notification.recipients]
194 users = [x.username for x in notification.recipients]
195
195
196 # test_regular gets notification by @mention
196 # test_regular gets notification by @mention
197 assert sorted(users) == [u'test_admin', u'test_regular']
197 assert sorted(users) == [u'test_admin', u'test_regular']
198
198
199 def test_create_with_status_change(self, backend):
199 def test_create_with_status_change(self, backend):
200 self.log_user()
200 self.log_user()
201 commit = backend.repo.get_commit('300')
201 commit = backend.repo.get_commit('300')
202 commit_id = commit.raw_id
202 commit_id = commit.raw_id
203 text = u'CommentOnCommit'
203 text = u'CommentOnCommit'
204 f_path = 'vcs/web/simplevcs/views/repository.py'
204 f_path = 'vcs/web/simplevcs/views/repository.py'
205 line = 'n1'
205 line = 'n1'
206
206
207 params = {'text': text, 'changeset_status': 'approved',
207 params = {'text': text, 'changeset_status': 'approved',
208 'csrf_token': self.csrf_token}
208 'csrf_token': self.csrf_token}
209
209
210 self.app.post(
210 self.app.post(
211 route_path(
211 route_path(
212 'repo_commit_comment_create',
212 'repo_commit_comment_create',
213 repo_name=backend.repo_name, commit_id=commit_id),
213 repo_name=backend.repo_name, commit_id=commit_id),
214 params=params)
214 params=params)
215
215
216 response = self.app.get(
216 response = self.app.get(
217 route_path('repo_commit',
217 route_path('repo_commit',
218 repo_name=backend.repo_name, commit_id=commit_id))
218 repo_name=backend.repo_name, commit_id=commit_id))
219
219
220 # test DB
220 # test DB
221 assert ChangesetComment.query().count() == 1
221 assert ChangesetComment.query().count() == 1
222 assert_comment_links(response, ChangesetComment.query().count(), 0)
222 assert_comment_links(response, ChangesetComment.query().count(), 0)
223
223
224 assert Notification.query().count() == 1
224 assert Notification.query().count() == 1
225 assert ChangesetComment.query().count() == 1
225 assert ChangesetComment.query().count() == 1
226
226
227 notification = Notification.query().all()[0]
227 notification = Notification.query().all()[0]
228
228
229 comment_id = ChangesetComment.query().first().comment_id
229 comment_id = ChangesetComment.query().first().comment_id
230 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
230 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
231
231
232 author = notification.created_by_user.username_and_name
232 author = notification.created_by_user.username_and_name
233 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
233 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
234 author, h.show_id(commit), backend.repo_name)
234 author, h.show_id(commit), backend.repo_name)
235 assert sbj == notification.subject
235 assert sbj == notification.subject
236
236
237 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
237 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
238 backend.repo_name, commit_id, comment_id))
238 backend.repo_name, commit_id, comment_id))
239 assert lnk in notification.body
239 assert lnk in notification.body
240
240
241 def test_delete(self, backend):
241 def test_delete(self, backend):
242 self.log_user()
242 self.log_user()
243 commit_id = backend.repo.get_commit('300').raw_id
243 commit_id = backend.repo.get_commit('300').raw_id
244 text = u'CommentOnCommit'
244 text = u'CommentOnCommit'
245
245
246 params = {'text': text, 'csrf_token': self.csrf_token}
246 params = {'text': text, 'csrf_token': self.csrf_token}
247 self.app.post(
247 self.app.post(
248 route_path(
248 route_path(
249 'repo_commit_comment_create',
249 'repo_commit_comment_create',
250 repo_name=backend.repo_name, commit_id=commit_id),
250 repo_name=backend.repo_name, commit_id=commit_id),
251 params=params)
251 params=params)
252
252
253 comments = ChangesetComment.query().all()
253 comments = ChangesetComment.query().all()
254 assert len(comments) == 1
254 assert len(comments) == 1
255 comment_id = comments[0].comment_id
255 comment_id = comments[0].comment_id
256
256
257 self.app.post(
257 self.app.post(
258 route_path('repo_commit_comment_delete',
258 route_path('repo_commit_comment_delete',
259 repo_name=backend.repo_name,
259 repo_name=backend.repo_name,
260 commit_id=commit_id,
260 commit_id=commit_id,
261 comment_id=comment_id),
261 comment_id=comment_id),
262 params={'csrf_token': self.csrf_token})
262 params={'csrf_token': self.csrf_token})
263
263
264 comments = ChangesetComment.query().all()
264 comments = ChangesetComment.query().all()
265 assert len(comments) == 0
265 assert len(comments) == 0
266
266
267 response = self.app.get(
267 response = self.app.get(
268 route_path('repo_commit',
268 route_path('repo_commit',
269 repo_name=backend.repo_name, commit_id=commit_id))
269 repo_name=backend.repo_name, commit_id=commit_id))
270 assert_comment_links(response, 0, 0)
270 assert_comment_links(response, 0, 0)
271
271
272 def test_edit(self, backend):
272 def test_edit(self, backend):
273 self.log_user()
273 self.log_user()
274 commit_id = backend.repo.get_commit('300').raw_id
274 commit_id = backend.repo.get_commit('300').raw_id
275 text = u'CommentOnCommit'
275 text = u'CommentOnCommit'
276
276
277 params = {'text': text, 'csrf_token': self.csrf_token}
277 params = {'text': text, 'csrf_token': self.csrf_token}
278 self.app.post(
278 self.app.post(
279 route_path(
279 route_path(
280 'repo_commit_comment_create',
280 'repo_commit_comment_create',
281 repo_name=backend.repo_name, commit_id=commit_id),
281 repo_name=backend.repo_name, commit_id=commit_id),
282 params=params)
282 params=params)
283
283
284 comments = ChangesetComment.query().all()
284 comments = ChangesetComment.query().all()
285 assert len(comments) == 1
285 assert len(comments) == 1
286 comment_id = comments[0].comment_id
286 comment_id = comments[0].comment_id
287 test_text = 'test_text'
287 test_text = 'test_text'
288 self.app.post(
288 self.app.post(
289 route_path(
289 route_path(
290 'repo_commit_comment_edit',
290 'repo_commit_comment_edit',
291 repo_name=backend.repo_name,
291 repo_name=backend.repo_name,
292 commit_id=commit_id,
292 commit_id=commit_id,
293 comment_id=comment_id,
293 comment_id=comment_id,
294 ),
294 ),
295 params={
295 params={
296 'csrf_token': self.csrf_token,
296 'csrf_token': self.csrf_token,
297 'text': test_text,
297 'text': test_text,
298 'version': '0',
298 'version': '0',
299 })
299 })
300
300
301 text_form_db = ChangesetComment.query().filter(
301 text_form_db = ChangesetComment.query().filter(
302 ChangesetComment.comment_id == comment_id).first().text
302 ChangesetComment.comment_id == comment_id).first().text
303 assert test_text == text_form_db
303 assert test_text == text_form_db
304
304
305 def test_edit_without_change(self, backend):
305 def test_edit_without_change(self, backend):
306 self.log_user()
306 self.log_user()
307 commit_id = backend.repo.get_commit('300').raw_id
307 commit_id = backend.repo.get_commit('300').raw_id
308 text = u'CommentOnCommit'
308 text = u'CommentOnCommit'
309
309
310 params = {'text': text, 'csrf_token': self.csrf_token}
310 params = {'text': text, 'csrf_token': self.csrf_token}
311 self.app.post(
311 self.app.post(
312 route_path(
312 route_path(
313 'repo_commit_comment_create',
313 'repo_commit_comment_create',
314 repo_name=backend.repo_name, commit_id=commit_id),
314 repo_name=backend.repo_name, commit_id=commit_id),
315 params=params)
315 params=params)
316
316
317 comments = ChangesetComment.query().all()
317 comments = ChangesetComment.query().all()
318 assert len(comments) == 1
318 assert len(comments) == 1
319 comment_id = comments[0].comment_id
319 comment_id = comments[0].comment_id
320
320
321 response = self.app.post(
321 response = self.app.post(
322 route_path(
322 route_path(
323 'repo_commit_comment_edit',
323 'repo_commit_comment_edit',
324 repo_name=backend.repo_name,
324 repo_name=backend.repo_name,
325 commit_id=commit_id,
325 commit_id=commit_id,
326 comment_id=comment_id,
326 comment_id=comment_id,
327 ),
327 ),
328 params={
328 params={
329 'csrf_token': self.csrf_token,
329 'csrf_token': self.csrf_token,
330 'text': text,
330 'text': text,
331 'version': '0',
331 'version': '0',
332 },
332 },
333 status=404,
333 status=404,
334 )
334 )
335 assert response.status_int == 404
335 assert response.status_int == 404
336
336
337 def test_edit_try_edit_already_edited(self, backend):
337 def test_edit_try_edit_already_edited(self, backend):
338 self.log_user()
338 self.log_user()
339 commit_id = backend.repo.get_commit('300').raw_id
339 commit_id = backend.repo.get_commit('300').raw_id
340 text = u'CommentOnCommit'
340 text = u'CommentOnCommit'
341
341
342 params = {'text': text, 'csrf_token': self.csrf_token}
342 params = {'text': text, 'csrf_token': self.csrf_token}
343 self.app.post(
343 self.app.post(
344 route_path(
344 route_path(
345 'repo_commit_comment_create',
345 'repo_commit_comment_create',
346 repo_name=backend.repo_name, commit_id=commit_id
346 repo_name=backend.repo_name, commit_id=commit_id
347 ),
347 ),
348 params=params,
348 params=params,
349 )
349 )
350
350
351 comments = ChangesetComment.query().all()
351 comments = ChangesetComment.query().all()
352 assert len(comments) == 1
352 assert len(comments) == 1
353 comment_id = comments[0].comment_id
353 comment_id = comments[0].comment_id
354 test_text = 'test_text'
354 test_text = 'test_text'
355 self.app.post(
355 self.app.post(
356 route_path(
356 route_path(
357 'repo_commit_comment_edit',
357 'repo_commit_comment_edit',
358 repo_name=backend.repo_name,
358 repo_name=backend.repo_name,
359 commit_id=commit_id,
359 commit_id=commit_id,
360 comment_id=comment_id,
360 comment_id=comment_id,
361 ),
361 ),
362 params={
362 params={
363 'csrf_token': self.csrf_token,
363 'csrf_token': self.csrf_token,
364 'text': test_text,
364 'text': test_text,
365 'version': '0',
365 'version': '0',
366 }
366 }
367 )
367 )
368 test_text_v2 = 'test_v2'
368 test_text_v2 = 'test_v2'
369 response = self.app.post(
369 response = self.app.post(
370 route_path(
370 route_path(
371 'repo_commit_comment_edit',
371 'repo_commit_comment_edit',
372 repo_name=backend.repo_name,
372 repo_name=backend.repo_name,
373 commit_id=commit_id,
373 commit_id=commit_id,
374 comment_id=comment_id,
374 comment_id=comment_id,
375 ),
375 ),
376 params={
376 params={
377 'csrf_token': self.csrf_token,
377 'csrf_token': self.csrf_token,
378 'text': test_text_v2,
378 'text': test_text_v2,
379 'version': '0',
379 'version': '0',
380 },
380 },
381 status=409,
381 status=409,
382 )
382 )
383 assert response.status_int == 409
383 assert response.status_int == 409
384
384
385 text_form_db = ChangesetComment.query().filter(
385 text_form_db = ChangesetComment.query().filter(
386 ChangesetComment.comment_id == comment_id).first().text
386 ChangesetComment.comment_id == comment_id).first().text
387
387
388 assert test_text == text_form_db
388 assert test_text == text_form_db
389 assert test_text_v2 != text_form_db
389 assert test_text_v2 != text_form_db
390
390
391 def test_edit_forbidden_for_immutable_comments(self, backend):
391 def test_edit_forbidden_for_immutable_comments(self, backend):
392 self.log_user()
392 self.log_user()
393 commit_id = backend.repo.get_commit('300').raw_id
393 commit_id = backend.repo.get_commit('300').raw_id
394 text = u'CommentOnCommit'
394 text = u'CommentOnCommit'
395
395
396 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
396 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
397 self.app.post(
397 self.app.post(
398 route_path(
398 route_path(
399 'repo_commit_comment_create',
399 'repo_commit_comment_create',
400 repo_name=backend.repo_name,
400 repo_name=backend.repo_name,
401 commit_id=commit_id,
401 commit_id=commit_id,
402 ),
402 ),
403 params=params
403 params=params
404 )
404 )
405
405
406 comments = ChangesetComment.query().all()
406 comments = ChangesetComment.query().all()
407 assert len(comments) == 1
407 assert len(comments) == 1
408 comment_id = comments[0].comment_id
408 comment_id = comments[0].comment_id
409
409
410 comment = ChangesetComment.get(comment_id)
410 comment = ChangesetComment.get(comment_id)
411 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
411 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
412 Session().add(comment)
412 Session().add(comment)
413 Session().commit()
413 Session().commit()
414
414
415 response = self.app.post(
415 response = self.app.post(
416 route_path(
416 route_path(
417 'repo_commit_comment_edit',
417 'repo_commit_comment_edit',
418 repo_name=backend.repo_name,
418 repo_name=backend.repo_name,
419 commit_id=commit_id,
419 commit_id=commit_id,
420 comment_id=comment_id,
420 comment_id=comment_id,
421 ),
421 ),
422 params={
422 params={
423 'csrf_token': self.csrf_token,
423 'csrf_token': self.csrf_token,
424 'text': 'test_text',
424 'text': 'test_text',
425 },
425 },
426 status=403,
426 status=403,
427 )
427 )
428 assert response.status_int == 403
428 assert response.status_int == 403
429
429
430 def test_delete_forbidden_for_immutable_comments(self, backend):
430 def test_delete_forbidden_for_immutable_comments(self, backend):
431 self.log_user()
431 self.log_user()
432 commit_id = backend.repo.get_commit('300').raw_id
432 commit_id = backend.repo.get_commit('300').raw_id
433 text = u'CommentOnCommit'
433 text = u'CommentOnCommit'
434
434
435 params = {'text': text, 'csrf_token': self.csrf_token}
435 params = {'text': text, 'csrf_token': self.csrf_token}
436 self.app.post(
436 self.app.post(
437 route_path(
437 route_path(
438 'repo_commit_comment_create',
438 'repo_commit_comment_create',
439 repo_name=backend.repo_name, commit_id=commit_id),
439 repo_name=backend.repo_name, commit_id=commit_id),
440 params=params)
440 params=params)
441
441
442 comments = ChangesetComment.query().all()
442 comments = ChangesetComment.query().all()
443 assert len(comments) == 1
443 assert len(comments) == 1
444 comment_id = comments[0].comment_id
444 comment_id = comments[0].comment_id
445
445
446 comment = ChangesetComment.get(comment_id)
446 comment = ChangesetComment.get(comment_id)
447 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
447 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
448 Session().add(comment)
448 Session().add(comment)
449 Session().commit()
449 Session().commit()
450
450
451 self.app.post(
451 self.app.post(
452 route_path('repo_commit_comment_delete',
452 route_path('repo_commit_comment_delete',
453 repo_name=backend.repo_name,
453 repo_name=backend.repo_name,
454 commit_id=commit_id,
454 commit_id=commit_id,
455 comment_id=comment_id),
455 comment_id=comment_id),
456 params={'csrf_token': self.csrf_token},
456 params={'csrf_token': self.csrf_token},
457 status=403)
457 status=403)
458
458
459 @pytest.mark.parametrize('renderer, text_input, output', [
459 @pytest.mark.parametrize('renderer, text_input, output', [
460 ('rst', 'plain text', '<p>plain text</p>'),
460 ('rst', 'plain text', '<p>plain text</p>'),
461 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
461 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
462 ('rst', '*italics*', '<em>italics</em>'),
462 ('rst', '*italics*', '<em>italics</em>'),
463 ('rst', '**bold**', '<strong>bold</strong>'),
463 ('rst', '**bold**', '<strong>bold</strong>'),
464 ('markdown', 'plain text', '<p>plain text</p>'),
464 ('markdown', 'plain text', '<p>plain text</p>'),
465 ('markdown', '# header', '<h1>header</h1>'),
465 ('markdown', '# header', '<h1>header</h1>'),
466 ('markdown', '*italics*', '<em>italics</em>'),
466 ('markdown', '*italics*', '<em>italics</em>'),
467 ('markdown', '**bold**', '<strong>bold</strong>'),
467 ('markdown', '**bold**', '<strong>bold</strong>'),
468 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
468 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
469 'md-header', 'md-italics', 'md-bold', ])
469 'md-header', 'md-italics', 'md-bold', ])
470 def test_preview(self, renderer, text_input, output, backend, xhr_header):
470 def test_preview(self, renderer, text_input, output, backend, xhr_header):
471 self.log_user()
471 self.log_user()
472 params = {
472 params = {
473 'renderer': renderer,
473 'renderer': renderer,
474 'text': text_input,
474 'text': text_input,
475 'csrf_token': self.csrf_token
475 'csrf_token': self.csrf_token
476 }
476 }
477 commit_id = '0' * 16 # fake this for tests
477 commit_id = '0' * 16 # fake this for tests
478 response = self.app.post(
478 response = self.app.post(
479 route_path('repo_commit_comment_preview',
479 route_path('repo_commit_comment_preview',
480 repo_name=backend.repo_name, commit_id=commit_id,),
480 repo_name=backend.repo_name, commit_id=commit_id,),
481 params=params,
481 params=params,
482 extra_environ=xhr_header)
482 extra_environ=xhr_header)
483
483
484 response.mustcontain(output)
484 response.mustcontain(output)
485
485
486
486
487 def assert_comment_links(response, comments, inline_comments):
487 def assert_comment_links(response, comments, inline_comments):
488 response.mustcontain(
488 response.mustcontain(
489 '<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
489 '<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
490 response.mustcontain(
490 response.mustcontain(
491 '<span class="display-none" id="inline-comments-count">{}</span>'.format(inline_comments))
491 '<span class="display-none" id="inline-comments-count">{}</span>'.format(inline_comments))
492
492
493
493
494
494
@@ -1,327 +1,327 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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.apps.repository.tests.test_repo_compare import ComparePage
23 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
24 from rhodecode.lib.helpers import _shorten_commit_id
24 from rhodecode.lib.helpers import _shorten_commit_id
25
25
26
26
27 def route_path(name, params=None, **kwargs):
27 def route_path(name, params=None, **kwargs):
28 import urllib
28 import urllib.request, urllib.parse, urllib.error
29
29
30 base_url = {
30 base_url = {
31 'repo_commit': '/{repo_name}/changeset/{commit_id}',
31 'repo_commit': '/{repo_name}/changeset/{commit_id}',
32 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
32 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
33 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
33 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
34 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
34 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
35 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
35 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
36 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
36 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
37 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
37 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
38 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
38 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
39 }[name].format(**kwargs)
39 }[name].format(**kwargs)
40
40
41 if params:
41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 return base_url
43 return base_url
44
44
45
45
46 @pytest.mark.usefixtures("app")
46 @pytest.mark.usefixtures("app")
47 class TestRepoCommitView(object):
47 class TestRepoCommitView(object):
48
48
49 def test_show_commit(self, backend):
49 def test_show_commit(self, backend):
50 commit_id = self.commit_id[backend.alias]
50 commit_id = self.commit_id[backend.alias]
51 response = self.app.get(route_path(
51 response = self.app.get(route_path(
52 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
52 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
53 response.mustcontain('Added a symlink')
53 response.mustcontain('Added a symlink')
54 response.mustcontain(commit_id)
54 response.mustcontain(commit_id)
55 response.mustcontain('No newline at end of file')
55 response.mustcontain('No newline at end of file')
56
56
57 def test_show_raw(self, backend):
57 def test_show_raw(self, backend):
58 commit_id = self.commit_id[backend.alias]
58 commit_id = self.commit_id[backend.alias]
59 response = self.app.get(route_path(
59 response = self.app.get(route_path(
60 'repo_commit_raw',
60 'repo_commit_raw',
61 repo_name=backend.repo_name, commit_id=commit_id))
61 repo_name=backend.repo_name, commit_id=commit_id))
62 assert response.body == self.diffs[backend.alias]
62 assert response.body == self.diffs[backend.alias]
63
63
64 def test_show_raw_patch(self, backend):
64 def test_show_raw_patch(self, backend):
65 response = self.app.get(route_path(
65 response = self.app.get(route_path(
66 'repo_commit_patch', repo_name=backend.repo_name,
66 'repo_commit_patch', repo_name=backend.repo_name,
67 commit_id=self.commit_id[backend.alias]))
67 commit_id=self.commit_id[backend.alias]))
68 assert response.body == self.patches[backend.alias]
68 assert response.body == self.patches[backend.alias]
69
69
70 def test_commit_download(self, backend):
70 def test_commit_download(self, backend):
71 response = self.app.get(route_path(
71 response = self.app.get(route_path(
72 'repo_commit_download',
72 'repo_commit_download',
73 repo_name=backend.repo_name,
73 repo_name=backend.repo_name,
74 commit_id=self.commit_id[backend.alias]))
74 commit_id=self.commit_id[backend.alias]))
75 assert response.body == self.diffs[backend.alias]
75 assert response.body == self.diffs[backend.alias]
76
76
77 def test_single_commit_page_different_ops(self, backend):
77 def test_single_commit_page_different_ops(self, backend):
78 commit_id = {
78 commit_id = {
79 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
79 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
80 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
80 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
81 'svn': '337',
81 'svn': '337',
82 }
82 }
83 diff_stat = {
83 diff_stat = {
84 'hg': (21, 943, 288),
84 'hg': (21, 943, 288),
85 'git': (20, 941, 286),
85 'git': (20, 941, 286),
86 'svn': (21, 943, 288),
86 'svn': (21, 943, 288),
87 }
87 }
88
88
89 commit_id = commit_id[backend.alias]
89 commit_id = commit_id[backend.alias]
90 response = self.app.get(route_path(
90 response = self.app.get(route_path(
91 'repo_commit',
91 'repo_commit',
92 repo_name=backend.repo_name, commit_id=commit_id))
92 repo_name=backend.repo_name, commit_id=commit_id))
93
93
94 response.mustcontain(_shorten_commit_id(commit_id))
94 response.mustcontain(_shorten_commit_id(commit_id))
95
95
96 compare_page = ComparePage(response)
96 compare_page = ComparePage(response)
97 file_changes = diff_stat[backend.alias]
97 file_changes = diff_stat[backend.alias]
98 compare_page.contains_change_summary(*file_changes)
98 compare_page.contains_change_summary(*file_changes)
99
99
100 # files op files
100 # files op files
101 response.mustcontain('File not present at commit: %s' %
101 response.mustcontain('File not present at commit: %s' %
102 _shorten_commit_id(commit_id))
102 _shorten_commit_id(commit_id))
103
103
104 # svn uses a different filename
104 # svn uses a different filename
105 if backend.alias == 'svn':
105 if backend.alias == 'svn':
106 response.mustcontain('new file 10644')
106 response.mustcontain('new file 10644')
107 else:
107 else:
108 response.mustcontain('new file 100644')
108 response.mustcontain('new file 100644')
109 response.mustcontain('Changed theme to ADC theme') # commit msg
109 response.mustcontain('Changed theme to ADC theme') # commit msg
110
110
111 self._check_new_diff_menus(response, right_menu=True)
111 self._check_new_diff_menus(response, right_menu=True)
112
112
113 def test_commit_range_page_different_ops(self, backend):
113 def test_commit_range_page_different_ops(self, backend):
114 commit_id_range = {
114 commit_id_range = {
115 'hg': (
115 'hg': (
116 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
116 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
117 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
117 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
118 'git': (
118 'git': (
119 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
119 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
120 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
120 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
121 'svn': (
121 'svn': (
122 '335',
122 '335',
123 '337'),
123 '337'),
124 }
124 }
125 commit_ids = commit_id_range[backend.alias]
125 commit_ids = commit_id_range[backend.alias]
126 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
126 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
127 response = self.app.get(route_path(
127 response = self.app.get(route_path(
128 'repo_commit',
128 'repo_commit',
129 repo_name=backend.repo_name, commit_id=commit_id))
129 repo_name=backend.repo_name, commit_id=commit_id))
130
130
131 response.mustcontain(_shorten_commit_id(commit_ids[0]))
131 response.mustcontain(_shorten_commit_id(commit_ids[0]))
132 response.mustcontain(_shorten_commit_id(commit_ids[1]))
132 response.mustcontain(_shorten_commit_id(commit_ids[1]))
133
133
134 compare_page = ComparePage(response)
134 compare_page = ComparePage(response)
135
135
136 # svn is special
136 # svn is special
137 if backend.alias == 'svn':
137 if backend.alias == 'svn':
138 response.mustcontain('new file 10644')
138 response.mustcontain('new file 10644')
139 for file_changes in [(1, 5, 1), (12, 236, 22), (21, 943, 288)]:
139 for file_changes in [(1, 5, 1), (12, 236, 22), (21, 943, 288)]:
140 compare_page.contains_change_summary(*file_changes)
140 compare_page.contains_change_summary(*file_changes)
141 elif backend.alias == 'git':
141 elif backend.alias == 'git':
142 response.mustcontain('new file 100644')
142 response.mustcontain('new file 100644')
143 for file_changes in [(12, 222, 20), (20, 941, 286)]:
143 for file_changes in [(12, 222, 20), (20, 941, 286)]:
144 compare_page.contains_change_summary(*file_changes)
144 compare_page.contains_change_summary(*file_changes)
145 else:
145 else:
146 response.mustcontain('new file 100644')
146 response.mustcontain('new file 100644')
147 for file_changes in [(12, 222, 20), (21, 943, 288)]:
147 for file_changes in [(12, 222, 20), (21, 943, 288)]:
148 compare_page.contains_change_summary(*file_changes)
148 compare_page.contains_change_summary(*file_changes)
149
149
150 # files op files
150 # files op files
151 response.mustcontain('File not present at commit: %s' % _shorten_commit_id(commit_ids[1]))
151 response.mustcontain('File not present at commit: %s' % _shorten_commit_id(commit_ids[1]))
152 response.mustcontain('Added docstrings to vcs.cli') # commit msg
152 response.mustcontain('Added docstrings to vcs.cli') # commit msg
153 response.mustcontain('Changed theme to ADC theme') # commit msg
153 response.mustcontain('Changed theme to ADC theme') # commit msg
154
154
155 self._check_new_diff_menus(response)
155 self._check_new_diff_menus(response)
156
156
157 def test_combined_compare_commit_page_different_ops(self, backend):
157 def test_combined_compare_commit_page_different_ops(self, backend):
158 commit_id_range = {
158 commit_id_range = {
159 'hg': (
159 'hg': (
160 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
160 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
161 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
161 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
162 'git': (
162 'git': (
163 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
163 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
164 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
164 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
165 'svn': (
165 'svn': (
166 '335',
166 '335',
167 '337'),
167 '337'),
168 }
168 }
169 commit_ids = commit_id_range[backend.alias]
169 commit_ids = commit_id_range[backend.alias]
170 response = self.app.get(route_path(
170 response = self.app.get(route_path(
171 'repo_compare',
171 'repo_compare',
172 repo_name=backend.repo_name,
172 repo_name=backend.repo_name,
173 source_ref_type='rev', source_ref=commit_ids[0],
173 source_ref_type='rev', source_ref=commit_ids[0],
174 target_ref_type='rev', target_ref=commit_ids[1], ))
174 target_ref_type='rev', target_ref=commit_ids[1], ))
175
175
176 response.mustcontain(_shorten_commit_id(commit_ids[0]))
176 response.mustcontain(_shorten_commit_id(commit_ids[0]))
177 response.mustcontain(_shorten_commit_id(commit_ids[1]))
177 response.mustcontain(_shorten_commit_id(commit_ids[1]))
178
178
179 # files op files
179 # files op files
180 response.mustcontain('File not present at commit: %s' %
180 response.mustcontain('File not present at commit: %s' %
181 _shorten_commit_id(commit_ids[1]))
181 _shorten_commit_id(commit_ids[1]))
182
182
183 compare_page = ComparePage(response)
183 compare_page = ComparePage(response)
184
184
185 # svn is special
185 # svn is special
186 if backend.alias == 'svn':
186 if backend.alias == 'svn':
187 response.mustcontain('new file 10644')
187 response.mustcontain('new file 10644')
188 file_changes = (32, 1179, 310)
188 file_changes = (32, 1179, 310)
189 compare_page.contains_change_summary(*file_changes)
189 compare_page.contains_change_summary(*file_changes)
190 elif backend.alias == 'git':
190 elif backend.alias == 'git':
191 response.mustcontain('new file 100644')
191 response.mustcontain('new file 100644')
192 file_changes = (31, 1163, 306)
192 file_changes = (31, 1163, 306)
193 compare_page.contains_change_summary(*file_changes)
193 compare_page.contains_change_summary(*file_changes)
194 else:
194 else:
195 response.mustcontain('new file 100644')
195 response.mustcontain('new file 100644')
196 file_changes = (32, 1165, 308)
196 file_changes = (32, 1165, 308)
197 compare_page.contains_change_summary(*file_changes)
197 compare_page.contains_change_summary(*file_changes)
198
198
199 response.mustcontain('Added docstrings to vcs.cli') # commit msg
199 response.mustcontain('Added docstrings to vcs.cli') # commit msg
200 response.mustcontain('Changed theme to ADC theme') # commit msg
200 response.mustcontain('Changed theme to ADC theme') # commit msg
201
201
202 self._check_new_diff_menus(response)
202 self._check_new_diff_menus(response)
203
203
204 def test_changeset_range(self, backend):
204 def test_changeset_range(self, backend):
205 self._check_changeset_range(
205 self._check_changeset_range(
206 backend, self.commit_id_range, self.commit_id_range_result)
206 backend, self.commit_id_range, self.commit_id_range_result)
207
207
208 def test_changeset_range_with_initial_commit(self, backend):
208 def test_changeset_range_with_initial_commit(self, backend):
209 commit_id_range = {
209 commit_id_range = {
210 'hg': (
210 'hg': (
211 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
211 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
212 '...6cba7170863a2411822803fa77a0a264f1310b35'),
212 '...6cba7170863a2411822803fa77a0a264f1310b35'),
213 'git': (
213 'git': (
214 'c1214f7e79e02fc37156ff215cd71275450cffc3'
214 'c1214f7e79e02fc37156ff215cd71275450cffc3'
215 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
215 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
216 'svn': '1...3',
216 'svn': '1...3',
217 }
217 }
218 commit_id_range_result = {
218 commit_id_range_result = {
219 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
219 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
220 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
220 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
221 'svn': ['1', '2', '3'],
221 'svn': ['1', '2', '3'],
222 }
222 }
223 self._check_changeset_range(
223 self._check_changeset_range(
224 backend, commit_id_range, commit_id_range_result)
224 backend, commit_id_range, commit_id_range_result)
225
225
226 def _check_changeset_range(
226 def _check_changeset_range(
227 self, backend, commit_id_ranges, commit_id_range_result):
227 self, backend, commit_id_ranges, commit_id_range_result):
228 response = self.app.get(
228 response = self.app.get(
229 route_path('repo_commit',
229 route_path('repo_commit',
230 repo_name=backend.repo_name,
230 repo_name=backend.repo_name,
231 commit_id=commit_id_ranges[backend.alias]))
231 commit_id=commit_id_ranges[backend.alias]))
232
232
233 expected_result = commit_id_range_result[backend.alias]
233 expected_result = commit_id_range_result[backend.alias]
234 response.mustcontain('{} commits'.format(len(expected_result)))
234 response.mustcontain('{} commits'.format(len(expected_result)))
235 for commit_id in expected_result:
235 for commit_id in expected_result:
236 response.mustcontain(commit_id)
236 response.mustcontain(commit_id)
237
237
238 commit_id = {
238 commit_id = {
239 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
239 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
240 'svn': '393',
240 'svn': '393',
241 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
241 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
242 }
242 }
243
243
244 commit_id_range = {
244 commit_id_range = {
245 'hg': (
245 'hg': (
246 'a53d9201d4bc278910d416d94941b7ea007ecd52'
246 'a53d9201d4bc278910d416d94941b7ea007ecd52'
247 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
247 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
248 'git': (
248 'git': (
249 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
249 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
250 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
250 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
251 'svn': '391...393',
251 'svn': '391...393',
252 }
252 }
253
253
254 commit_id_range_result = {
254 commit_id_range_result = {
255 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
255 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
256 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
256 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
257 'svn': ['391', '392', '393'],
257 'svn': ['391', '392', '393'],
258 }
258 }
259
259
260 diffs = {
260 diffs = {
261 'hg': r"""diff --git a/README b/README
261 'hg': r"""diff --git a/README b/README
262 new file mode 120000
262 new file mode 120000
263 --- /dev/null
263 --- /dev/null
264 +++ b/README
264 +++ b/README
265 @@ -0,0 +1,1 @@
265 @@ -0,0 +1,1 @@
266 +README.rst
266 +README.rst
267 \ No newline at end of file
267 \ No newline at end of file
268 """,
268 """,
269 'git': r"""diff --git a/README b/README
269 'git': r"""diff --git a/README b/README
270 new file mode 120000
270 new file mode 120000
271 index 0000000..92cacd2
271 index 0000000..92cacd2
272 --- /dev/null
272 --- /dev/null
273 +++ b/README
273 +++ b/README
274 @@ -0,0 +1 @@
274 @@ -0,0 +1 @@
275 +README.rst
275 +README.rst
276 \ No newline at end of file
276 \ No newline at end of file
277 """,
277 """,
278 'svn': """Index: README
278 'svn': """Index: README
279 ===================================================================
279 ===================================================================
280 diff --git a/README b/README
280 diff --git a/README b/README
281 new file mode 10644
281 new file mode 10644
282 --- /dev/null\t(revision 0)
282 --- /dev/null\t(revision 0)
283 +++ b/README\t(revision 393)
283 +++ b/README\t(revision 393)
284 @@ -0,0 +1 @@
284 @@ -0,0 +1 @@
285 +link README.rst
285 +link README.rst
286 \\ No newline at end of file
286 \\ No newline at end of file
287 """,
287 """,
288 }
288 }
289
289
290 patches = {
290 patches = {
291 'hg': r"""# HG changeset patch
291 'hg': r"""# HG changeset patch
292 # User Marcin Kuzminski <marcin@python-works.com>
292 # User Marcin Kuzminski <marcin@python-works.com>
293 # Date 2014-01-07 12:21:40
293 # Date 2014-01-07 12:21:40
294 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
294 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
295 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
295 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
296
296
297 Added a symlink
297 Added a symlink
298
298
299 """ + diffs['hg'],
299 """ + diffs['hg'],
300 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
300 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
301 From: Marcin Kuzminski <marcin@python-works.com>
301 From: Marcin Kuzminski <marcin@python-works.com>
302 Date: 2014-01-07 12:22:20
302 Date: 2014-01-07 12:22:20
303 Subject: [PATCH] Added a symlink
303 Subject: [PATCH] Added a symlink
304
304
305 ---
305 ---
306
306
307 """ + diffs['git'],
307 """ + diffs['git'],
308 'svn': r"""# SVN changeset patch
308 'svn': r"""# SVN changeset patch
309 # User marcin
309 # User marcin
310 # Date 2014-09-02 12:25:22.071142
310 # Date 2014-09-02 12:25:22.071142
311 # Revision 393
311 # Revision 393
312
312
313 Added a symlink
313 Added a symlink
314
314
315 """ + diffs['svn'],
315 """ + diffs['svn'],
316 }
316 }
317
317
318 def _check_new_diff_menus(self, response, right_menu=False,):
318 def _check_new_diff_menus(self, response, right_menu=False,):
319 # individual file diff menus
319 # individual file diff menus
320 for elem in ['Show file before', 'Show file after']:
320 for elem in ['Show file before', 'Show file after']:
321 response.mustcontain(elem)
321 response.mustcontain(elem)
322
322
323 # right pane diff menus
323 # right pane diff menus
324 if right_menu:
324 if right_menu:
325 for elem in ['Hide whitespace changes', 'Toggle wide diff',
325 for elem in ['Hide whitespace changes', 'Toggle wide diff',
326 'Show full context diff']:
326 'Show full context diff']:
327 response.mustcontain(elem)
327 response.mustcontain(elem)
@@ -1,672 +1,672 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 lxml.html
23 import lxml.html
24
24
25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 from rhodecode.tests import assert_session_flash
26 from rhodecode.tests import assert_session_flash
27 from rhodecode.tests.utils import AssertResponse, commit_change
27 from rhodecode.tests.utils import AssertResponse, commit_change
28
28
29
29
30 def route_path(name, params=None, **kwargs):
30 def route_path(name, params=None, **kwargs):
31 import urllib
31 import urllib.request, urllib.parse, urllib.error
32
32
33 base_url = {
33 base_url = {
34 'repo_compare_select': '/{repo_name}/compare',
34 'repo_compare_select': '/{repo_name}/compare',
35 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
35 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
36 }[name].format(**kwargs)
36 }[name].format(**kwargs)
37
37
38 if params:
38 if params:
39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
39 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 return base_url
40 return base_url
41
41
42
42
43 @pytest.mark.usefixtures("autologin_user", "app")
43 @pytest.mark.usefixtures("autologin_user", "app")
44 class TestCompareView(object):
44 class TestCompareView(object):
45
45
46 def test_compare_index_is_reached_at_least_once(self, backend):
46 def test_compare_index_is_reached_at_least_once(self, backend):
47 repo = backend.repo
47 repo = backend.repo
48 self.app.get(
48 self.app.get(
49 route_path('repo_compare_select', repo_name=repo.repo_name))
49 route_path('repo_compare_select', repo_name=repo.repo_name))
50
50
51 @pytest.mark.xfail_backends("svn", reason="Requires pull")
51 @pytest.mark.xfail_backends("svn", reason="Requires pull")
52 def test_compare_remote_with_different_commit_indexes(self, backend):
52 def test_compare_remote_with_different_commit_indexes(self, backend):
53 # Preparing the following repository structure:
53 # Preparing the following repository structure:
54 #
54 #
55 # Origin repository has two commits:
55 # Origin repository has two commits:
56 #
56 #
57 # 0 1
57 # 0 1
58 # A -- D
58 # A -- D
59 #
59 #
60 # The fork of it has a few more commits and "D" has a commit index
60 # The fork of it has a few more commits and "D" has a commit index
61 # which does not exist in origin.
61 # which does not exist in origin.
62 #
62 #
63 # 0 1 2 3 4
63 # 0 1 2 3 4
64 # A -- -- -- D -- E
64 # A -- -- -- D -- E
65 # \- B -- C
65 # \- B -- C
66 #
66 #
67
67
68 fork = backend.create_repo()
68 fork = backend.create_repo()
69
69
70 # prepare fork
70 # prepare fork
71 commit0 = commit_change(
71 commit0 = commit_change(
72 fork.repo_name, filename='file1', content='A',
72 fork.repo_name, filename='file1', content='A',
73 message='A', vcs_type=backend.alias, parent=None, newfile=True)
73 message='A', vcs_type=backend.alias, parent=None, newfile=True)
74
74
75 commit1 = commit_change(
75 commit1 = commit_change(
76 fork.repo_name, filename='file1', content='B',
76 fork.repo_name, filename='file1', content='B',
77 message='B, child of A', vcs_type=backend.alias, parent=commit0)
77 message='B, child of A', vcs_type=backend.alias, parent=commit0)
78
78
79 commit_change( # commit 2
79 commit_change( # commit 2
80 fork.repo_name, filename='file1', content='C',
80 fork.repo_name, filename='file1', content='C',
81 message='C, child of B', vcs_type=backend.alias, parent=commit1)
81 message='C, child of B', vcs_type=backend.alias, parent=commit1)
82
82
83 commit3 = commit_change(
83 commit3 = commit_change(
84 fork.repo_name, filename='file1', content='D',
84 fork.repo_name, filename='file1', content='D',
85 message='D, child of A', vcs_type=backend.alias, parent=commit0)
85 message='D, child of A', vcs_type=backend.alias, parent=commit0)
86
86
87 commit4 = commit_change(
87 commit4 = commit_change(
88 fork.repo_name, filename='file1', content='E',
88 fork.repo_name, filename='file1', content='E',
89 message='E, child of D', vcs_type=backend.alias, parent=commit3)
89 message='E, child of D', vcs_type=backend.alias, parent=commit3)
90
90
91 # prepare origin repository, taking just the history up to D
91 # prepare origin repository, taking just the history up to D
92 origin = backend.create_repo()
92 origin = backend.create_repo()
93
93
94 origin_repo = origin.scm_instance(cache=False)
94 origin_repo = origin.scm_instance(cache=False)
95 origin_repo.config.clear_section('hooks')
95 origin_repo.config.clear_section('hooks')
96 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
96 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
97 origin_repo = origin.scm_instance(cache=False) # cache rebuild
97 origin_repo = origin.scm_instance(cache=False) # cache rebuild
98
98
99 # Verify test fixture setup
99 # Verify test fixture setup
100 # This does not work for git
100 # This does not work for git
101 if backend.alias != 'git':
101 if backend.alias != 'git':
102 assert 5 == len(fork.scm_instance().commit_ids)
102 assert 5 == len(fork.scm_instance().commit_ids)
103 assert 2 == len(origin_repo.commit_ids)
103 assert 2 == len(origin_repo.commit_ids)
104
104
105 # Comparing the revisions
105 # Comparing the revisions
106 response = self.app.get(
106 response = self.app.get(
107 route_path('repo_compare',
107 route_path('repo_compare',
108 repo_name=origin.repo_name,
108 repo_name=origin.repo_name,
109 source_ref_type="rev", source_ref=commit3.raw_id,
109 source_ref_type="rev", source_ref=commit3.raw_id,
110 target_ref_type="rev", target_ref=commit4.raw_id,
110 target_ref_type="rev", target_ref=commit4.raw_id,
111 params=dict(merge='1', target_repo=fork.repo_name)
111 params=dict(merge='1', target_repo=fork.repo_name)
112 ))
112 ))
113
113
114 compare_page = ComparePage(response)
114 compare_page = ComparePage(response)
115 compare_page.contains_commits([commit4])
115 compare_page.contains_commits([commit4])
116
116
117 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
117 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
118 def test_compare_forks_on_branch_extra_commits(self, backend):
118 def test_compare_forks_on_branch_extra_commits(self, backend):
119 repo1 = backend.create_repo()
119 repo1 = backend.create_repo()
120
120
121 # commit something !
121 # commit something !
122 commit0 = commit_change(
122 commit0 = commit_change(
123 repo1.repo_name, filename='file1', content='line1\n',
123 repo1.repo_name, filename='file1', content='line1\n',
124 message='commit1', vcs_type=backend.alias, parent=None,
124 message='commit1', vcs_type=backend.alias, parent=None,
125 newfile=True)
125 newfile=True)
126
126
127 # fork this repo
127 # fork this repo
128 repo2 = backend.create_fork()
128 repo2 = backend.create_fork()
129
129
130 # add two extra commit into fork
130 # add two extra commit into fork
131 commit1 = commit_change(
131 commit1 = commit_change(
132 repo2.repo_name, filename='file1', content='line1\nline2\n',
132 repo2.repo_name, filename='file1', content='line1\nline2\n',
133 message='commit2', vcs_type=backend.alias, parent=commit0)
133 message='commit2', vcs_type=backend.alias, parent=commit0)
134
134
135 commit2 = commit_change(
135 commit2 = commit_change(
136 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
136 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
137 message='commit3', vcs_type=backend.alias, parent=commit1)
137 message='commit3', vcs_type=backend.alias, parent=commit1)
138
138
139 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
139 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
140 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
140 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
141
141
142 response = self.app.get(
142 response = self.app.get(
143 route_path('repo_compare',
143 route_path('repo_compare',
144 repo_name=repo1.repo_name,
144 repo_name=repo1.repo_name,
145 source_ref_type="branch", source_ref=commit_id2,
145 source_ref_type="branch", source_ref=commit_id2,
146 target_ref_type="branch", target_ref=commit_id1,
146 target_ref_type="branch", target_ref=commit_id1,
147 params=dict(merge='1', target_repo=repo2.repo_name)
147 params=dict(merge='1', target_repo=repo2.repo_name)
148 ))
148 ))
149
149
150 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
150 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
151 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
151 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
152
152
153 compare_page = ComparePage(response)
153 compare_page = ComparePage(response)
154 compare_page.contains_change_summary(1, 2, 0)
154 compare_page.contains_change_summary(1, 2, 0)
155 compare_page.contains_commits([commit1, commit2])
155 compare_page.contains_commits([commit1, commit2])
156
156
157 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
157 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
158 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
158 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
159
159
160 # Swap is removed when comparing branches since it's a PR feature and
160 # Swap is removed when comparing branches since it's a PR feature and
161 # it is then a preview mode
161 # it is then a preview mode
162 compare_page.swap_is_hidden()
162 compare_page.swap_is_hidden()
163 compare_page.target_source_are_disabled()
163 compare_page.target_source_are_disabled()
164
164
165 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
165 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
166 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(self, backend):
166 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(self, backend):
167 repo1 = backend.create_repo()
167 repo1 = backend.create_repo()
168
168
169 # commit something !
169 # commit something !
170 commit0 = commit_change(
170 commit0 = commit_change(
171 repo1.repo_name, filename='file1', content='line1\n',
171 repo1.repo_name, filename='file1', content='line1\n',
172 message='commit1', vcs_type=backend.alias, parent=None,
172 message='commit1', vcs_type=backend.alias, parent=None,
173 newfile=True)
173 newfile=True)
174
174
175 # fork this repo
175 # fork this repo
176 repo2 = backend.create_fork()
176 repo2 = backend.create_fork()
177
177
178 # now commit something to origin repo
178 # now commit something to origin repo
179 commit_change(
179 commit_change(
180 repo1.repo_name, filename='file2', content='line1file2\n',
180 repo1.repo_name, filename='file2', content='line1file2\n',
181 message='commit2', vcs_type=backend.alias, parent=commit0,
181 message='commit2', vcs_type=backend.alias, parent=commit0,
182 newfile=True)
182 newfile=True)
183
183
184 # add two extra commit into fork
184 # add two extra commit into fork
185 commit1 = commit_change(
185 commit1 = commit_change(
186 repo2.repo_name, filename='file1', content='line1\nline2\n',
186 repo2.repo_name, filename='file1', content='line1\nline2\n',
187 message='commit2', vcs_type=backend.alias, parent=commit0)
187 message='commit2', vcs_type=backend.alias, parent=commit0)
188
188
189 commit2 = commit_change(
189 commit2 = commit_change(
190 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
190 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
191 message='commit3', vcs_type=backend.alias, parent=commit1)
191 message='commit3', vcs_type=backend.alias, parent=commit1)
192
192
193 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
193 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
194 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
194 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
195
195
196 response = self.app.get(
196 response = self.app.get(
197 route_path('repo_compare',
197 route_path('repo_compare',
198 repo_name=repo1.repo_name,
198 repo_name=repo1.repo_name,
199 source_ref_type="branch", source_ref=commit_id2,
199 source_ref_type="branch", source_ref=commit_id2,
200 target_ref_type="branch", target_ref=commit_id1,
200 target_ref_type="branch", target_ref=commit_id1,
201 params=dict(merge='1', target_repo=repo2.repo_name),
201 params=dict(merge='1', target_repo=repo2.repo_name),
202 ))
202 ))
203
203
204 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
204 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
205 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
205 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
206
206
207 compare_page = ComparePage(response)
207 compare_page = ComparePage(response)
208 compare_page.contains_change_summary(1, 2, 0)
208 compare_page.contains_change_summary(1, 2, 0)
209 compare_page.contains_commits([commit1, commit2])
209 compare_page.contains_commits([commit1, commit2])
210 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
210 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
211 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
211 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
212
212
213 # Swap is removed when comparing branches since it's a PR feature and
213 # Swap is removed when comparing branches since it's a PR feature and
214 # it is then a preview mode
214 # it is then a preview mode
215 compare_page.swap_is_hidden()
215 compare_page.swap_is_hidden()
216 compare_page.target_source_are_disabled()
216 compare_page.target_source_are_disabled()
217
217
218 @pytest.mark.xfail_backends("svn")
218 @pytest.mark.xfail_backends("svn")
219 # TODO(marcink): no svn support for compare two seperate repos
219 # TODO(marcink): no svn support for compare two seperate repos
220 def test_compare_of_unrelated_forks(self, backend):
220 def test_compare_of_unrelated_forks(self, backend):
221 orig = backend.create_repo(number_of_commits=1)
221 orig = backend.create_repo(number_of_commits=1)
222 fork = backend.create_repo(number_of_commits=1)
222 fork = backend.create_repo(number_of_commits=1)
223
223
224 response = self.app.get(
224 response = self.app.get(
225 route_path('repo_compare',
225 route_path('repo_compare',
226 repo_name=orig.repo_name,
226 repo_name=orig.repo_name,
227 source_ref_type="rev", source_ref="tip",
227 source_ref_type="rev", source_ref="tip",
228 target_ref_type="rev", target_ref="tip",
228 target_ref_type="rev", target_ref="tip",
229 params=dict(merge='1', target_repo=fork.repo_name),
229 params=dict(merge='1', target_repo=fork.repo_name),
230 ),
230 ),
231 status=302)
231 status=302)
232 response = response.follow()
232 response = response.follow()
233 response.mustcontain("Repositories unrelated.")
233 response.mustcontain("Repositories unrelated.")
234
234
235 @pytest.mark.xfail_backends("svn")
235 @pytest.mark.xfail_backends("svn")
236 def test_compare_cherry_pick_commits_from_bottom(self, backend):
236 def test_compare_cherry_pick_commits_from_bottom(self, backend):
237
237
238 # repo1:
238 # repo1:
239 # commit0:
239 # commit0:
240 # commit1:
240 # commit1:
241 # repo1-fork- in which we will cherry pick bottom commits
241 # repo1-fork- in which we will cherry pick bottom commits
242 # commit0:
242 # commit0:
243 # commit1:
243 # commit1:
244 # commit2: x
244 # commit2: x
245 # commit3: x
245 # commit3: x
246 # commit4: x
246 # commit4: x
247 # commit5:
247 # commit5:
248 # make repo1, and commit1+commit2
248 # make repo1, and commit1+commit2
249
249
250 repo1 = backend.create_repo()
250 repo1 = backend.create_repo()
251
251
252 # commit something !
252 # commit something !
253 commit0 = commit_change(
253 commit0 = commit_change(
254 repo1.repo_name, filename='file1', content='line1\n',
254 repo1.repo_name, filename='file1', content='line1\n',
255 message='commit1', vcs_type=backend.alias, parent=None,
255 message='commit1', vcs_type=backend.alias, parent=None,
256 newfile=True)
256 newfile=True)
257 commit1 = commit_change(
257 commit1 = commit_change(
258 repo1.repo_name, filename='file1', content='line1\nline2\n',
258 repo1.repo_name, filename='file1', content='line1\nline2\n',
259 message='commit2', vcs_type=backend.alias, parent=commit0)
259 message='commit2', vcs_type=backend.alias, parent=commit0)
260
260
261 # fork this repo
261 # fork this repo
262 repo2 = backend.create_fork()
262 repo2 = backend.create_fork()
263
263
264 # now make commit3-6
264 # now make commit3-6
265 commit2 = commit_change(
265 commit2 = commit_change(
266 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
266 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
267 message='commit3', vcs_type=backend.alias, parent=commit1)
267 message='commit3', vcs_type=backend.alias, parent=commit1)
268 commit3 = commit_change(
268 commit3 = commit_change(
269 repo1.repo_name, filename='file1',
269 repo1.repo_name, filename='file1',
270 content='line1\nline2\nline3\nline4\n', message='commit4',
270 content='line1\nline2\nline3\nline4\n', message='commit4',
271 vcs_type=backend.alias, parent=commit2)
271 vcs_type=backend.alias, parent=commit2)
272 commit4 = commit_change(
272 commit4 = commit_change(
273 repo1.repo_name, filename='file1',
273 repo1.repo_name, filename='file1',
274 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
274 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
275 vcs_type=backend.alias, parent=commit3)
275 vcs_type=backend.alias, parent=commit3)
276 commit_change( # commit 5
276 commit_change( # commit 5
277 repo1.repo_name, filename='file1',
277 repo1.repo_name, filename='file1',
278 content='line1\nline2\nline3\nline4\nline5\nline6\n',
278 content='line1\nline2\nline3\nline4\nline5\nline6\n',
279 message='commit6', vcs_type=backend.alias, parent=commit4)
279 message='commit6', vcs_type=backend.alias, parent=commit4)
280
280
281 response = self.app.get(
281 response = self.app.get(
282 route_path('repo_compare',
282 route_path('repo_compare',
283 repo_name=repo2.repo_name,
283 repo_name=repo2.repo_name,
284 # parent of commit2, in target repo2
284 # parent of commit2, in target repo2
285 source_ref_type="rev", source_ref=commit1.raw_id,
285 source_ref_type="rev", source_ref=commit1.raw_id,
286 target_ref_type="rev", target_ref=commit4.raw_id,
286 target_ref_type="rev", target_ref=commit4.raw_id,
287 params=dict(merge='1', target_repo=repo1.repo_name),
287 params=dict(merge='1', target_repo=repo1.repo_name),
288 ))
288 ))
289 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
289 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
290 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
290 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
291
291
292 # files
292 # files
293 compare_page = ComparePage(response)
293 compare_page = ComparePage(response)
294 compare_page.contains_change_summary(1, 3, 0)
294 compare_page.contains_change_summary(1, 3, 0)
295 compare_page.contains_commits([commit2, commit3, commit4])
295 compare_page.contains_commits([commit2, commit3, commit4])
296 anchor = 'a_c-{}-826e8142e6ba'.format(commit1.short_id)
296 anchor = 'a_c-{}-826e8142e6ba'.format(commit1.short_id)
297 compare_page.contains_file_links_and_anchors([('file1', anchor),])
297 compare_page.contains_file_links_and_anchors([('file1', anchor),])
298
298
299 @pytest.mark.xfail_backends("svn")
299 @pytest.mark.xfail_backends("svn")
300 def test_compare_cherry_pick_commits_from_top(self, backend):
300 def test_compare_cherry_pick_commits_from_top(self, backend):
301 # repo1:
301 # repo1:
302 # commit0:
302 # commit0:
303 # commit1:
303 # commit1:
304 # repo1-fork- in which we will cherry pick bottom commits
304 # repo1-fork- in which we will cherry pick bottom commits
305 # commit0:
305 # commit0:
306 # commit1:
306 # commit1:
307 # commit2:
307 # commit2:
308 # commit3: x
308 # commit3: x
309 # commit4: x
309 # commit4: x
310 # commit5: x
310 # commit5: x
311
311
312 # make repo1, and commit1+commit2
312 # make repo1, and commit1+commit2
313 repo1 = backend.create_repo()
313 repo1 = backend.create_repo()
314
314
315 # commit something !
315 # commit something !
316 commit0 = commit_change(
316 commit0 = commit_change(
317 repo1.repo_name, filename='file1', content='line1\n',
317 repo1.repo_name, filename='file1', content='line1\n',
318 message='commit1', vcs_type=backend.alias, parent=None,
318 message='commit1', vcs_type=backend.alias, parent=None,
319 newfile=True)
319 newfile=True)
320 commit1 = commit_change(
320 commit1 = commit_change(
321 repo1.repo_name, filename='file1', content='line1\nline2\n',
321 repo1.repo_name, filename='file1', content='line1\nline2\n',
322 message='commit2', vcs_type=backend.alias, parent=commit0)
322 message='commit2', vcs_type=backend.alias, parent=commit0)
323
323
324 # fork this repo
324 # fork this repo
325 backend.create_fork()
325 backend.create_fork()
326
326
327 # now make commit3-6
327 # now make commit3-6
328 commit2 = commit_change(
328 commit2 = commit_change(
329 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
329 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
330 message='commit3', vcs_type=backend.alias, parent=commit1)
330 message='commit3', vcs_type=backend.alias, parent=commit1)
331 commit3 = commit_change(
331 commit3 = commit_change(
332 repo1.repo_name, filename='file1',
332 repo1.repo_name, filename='file1',
333 content='line1\nline2\nline3\nline4\n', message='commit4',
333 content='line1\nline2\nline3\nline4\n', message='commit4',
334 vcs_type=backend.alias, parent=commit2)
334 vcs_type=backend.alias, parent=commit2)
335 commit4 = commit_change(
335 commit4 = commit_change(
336 repo1.repo_name, filename='file1',
336 repo1.repo_name, filename='file1',
337 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
337 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
338 vcs_type=backend.alias, parent=commit3)
338 vcs_type=backend.alias, parent=commit3)
339 commit5 = commit_change(
339 commit5 = commit_change(
340 repo1.repo_name, filename='file1',
340 repo1.repo_name, filename='file1',
341 content='line1\nline2\nline3\nline4\nline5\nline6\n',
341 content='line1\nline2\nline3\nline4\nline5\nline6\n',
342 message='commit6', vcs_type=backend.alias, parent=commit4)
342 message='commit6', vcs_type=backend.alias, parent=commit4)
343
343
344 response = self.app.get(
344 response = self.app.get(
345 route_path('repo_compare',
345 route_path('repo_compare',
346 repo_name=repo1.repo_name,
346 repo_name=repo1.repo_name,
347 # parent of commit3, not in source repo2
347 # parent of commit3, not in source repo2
348 source_ref_type="rev", source_ref=commit2.raw_id,
348 source_ref_type="rev", source_ref=commit2.raw_id,
349 target_ref_type="rev", target_ref=commit5.raw_id,
349 target_ref_type="rev", target_ref=commit5.raw_id,
350 params=dict(merge='1'),))
350 params=dict(merge='1'),))
351
351
352 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
352 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
353 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
353 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
354
354
355 compare_page = ComparePage(response)
355 compare_page = ComparePage(response)
356 compare_page.contains_change_summary(1, 3, 0)
356 compare_page.contains_change_summary(1, 3, 0)
357 compare_page.contains_commits([commit3, commit4, commit5])
357 compare_page.contains_commits([commit3, commit4, commit5])
358
358
359 # files
359 # files
360 anchor = 'a_c-{}-826e8142e6ba'.format(commit2.short_id)
360 anchor = 'a_c-{}-826e8142e6ba'.format(commit2.short_id)
361 compare_page.contains_file_links_and_anchors([('file1', anchor),])
361 compare_page.contains_file_links_and_anchors([('file1', anchor),])
362
362
363 @pytest.mark.xfail_backends("svn")
363 @pytest.mark.xfail_backends("svn")
364 def test_compare_remote_branches(self, backend):
364 def test_compare_remote_branches(self, backend):
365 repo1 = backend.repo
365 repo1 = backend.repo
366 repo2 = backend.create_fork()
366 repo2 = backend.create_fork()
367
367
368 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
368 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
369 commit_id1_short = repo1.get_commit(commit_idx=3).short_id
369 commit_id1_short = repo1.get_commit(commit_idx=3).short_id
370 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
370 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
371 commit_id2_short = repo1.get_commit(commit_idx=6).short_id
371 commit_id2_short = repo1.get_commit(commit_idx=6).short_id
372
372
373 response = self.app.get(
373 response = self.app.get(
374 route_path('repo_compare',
374 route_path('repo_compare',
375 repo_name=repo1.repo_name,
375 repo_name=repo1.repo_name,
376 source_ref_type="rev", source_ref=commit_id1,
376 source_ref_type="rev", source_ref=commit_id1,
377 target_ref_type="rev", target_ref=commit_id2,
377 target_ref_type="rev", target_ref=commit_id2,
378 params=dict(merge='1', target_repo=repo2.repo_name),
378 params=dict(merge='1', target_repo=repo2.repo_name),
379 ))
379 ))
380
380
381 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
381 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
382 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
382 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
383
383
384 compare_page = ComparePage(response)
384 compare_page = ComparePage(response)
385
385
386 # outgoing commits between those commits
386 # outgoing commits between those commits
387 compare_page.contains_commits(
387 compare_page.contains_commits(
388 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
388 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
389
389
390 # files
390 # files
391 compare_page.contains_file_links_and_anchors([
391 compare_page.contains_file_links_and_anchors([
392 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(commit_id2_short)),
392 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(commit_id2_short)),
393 ('vcs/backends/__init__.py', 'a_c-{}-41b41c1f2796'.format(commit_id1_short)),
393 ('vcs/backends/__init__.py', 'a_c-{}-41b41c1f2796'.format(commit_id1_short)),
394 ('vcs/backends/base.py', 'a_c-{}-2f574d260608'.format(commit_id1_short)),
394 ('vcs/backends/base.py', 'a_c-{}-2f574d260608'.format(commit_id1_short)),
395 ])
395 ])
396
396
397 @pytest.mark.xfail_backends("svn")
397 @pytest.mark.xfail_backends("svn")
398 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
398 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
399 repo1 = backend.create_repo()
399 repo1 = backend.create_repo()
400 r1_name = repo1.repo_name
400 r1_name = repo1.repo_name
401
401
402 commit0 = commit_change(
402 commit0 = commit_change(
403 repo=r1_name, filename='file1',
403 repo=r1_name, filename='file1',
404 content='line1', message='commit1', vcs_type=backend.alias,
404 content='line1', message='commit1', vcs_type=backend.alias,
405 newfile=True)
405 newfile=True)
406 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
406 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
407
407
408 # fork the repo1
408 # fork the repo1
409 repo2 = backend.create_fork()
409 repo2 = backend.create_fork()
410 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
410 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
411
411
412 self.r2_id = repo2.repo_id
412 self.r2_id = repo2.repo_id
413 r2_name = repo2.repo_name
413 r2_name = repo2.repo_name
414
414
415 commit1 = commit_change(
415 commit1 = commit_change(
416 repo=r2_name, filename='file1-fork',
416 repo=r2_name, filename='file1-fork',
417 content='file1-line1-from-fork', message='commit1-fork',
417 content='file1-line1-from-fork', message='commit1-fork',
418 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
418 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
419 newfile=True)
419 newfile=True)
420
420
421 commit2 = commit_change(
421 commit2 = commit_change(
422 repo=r2_name, filename='file2-fork',
422 repo=r2_name, filename='file2-fork',
423 content='file2-line1-from-fork', message='commit2-fork',
423 content='file2-line1-from-fork', message='commit2-fork',
424 vcs_type=backend.alias, parent=commit1,
424 vcs_type=backend.alias, parent=commit1,
425 newfile=True)
425 newfile=True)
426
426
427 commit_change( # commit 3
427 commit_change( # commit 3
428 repo=r2_name, filename='file3-fork',
428 repo=r2_name, filename='file3-fork',
429 content='file3-line1-from-fork', message='commit3-fork',
429 content='file3-line1-from-fork', message='commit3-fork',
430 vcs_type=backend.alias, parent=commit2, newfile=True)
430 vcs_type=backend.alias, parent=commit2, newfile=True)
431
431
432 # compare !
432 # compare !
433 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
433 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
434 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
434 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
435
435
436 response = self.app.get(
436 response = self.app.get(
437 route_path('repo_compare',
437 route_path('repo_compare',
438 repo_name=r2_name,
438 repo_name=r2_name,
439 source_ref_type="branch", source_ref=commit_id1,
439 source_ref_type="branch", source_ref=commit_id1,
440 target_ref_type="branch", target_ref=commit_id2,
440 target_ref_type="branch", target_ref=commit_id2,
441 params=dict(merge='1', target_repo=r1_name),
441 params=dict(merge='1', target_repo=r1_name),
442 ))
442 ))
443
443
444 response.mustcontain('%s@%s' % (r2_name, commit_id1))
444 response.mustcontain('%s@%s' % (r2_name, commit_id1))
445 response.mustcontain('%s@%s' % (r1_name, commit_id2))
445 response.mustcontain('%s@%s' % (r1_name, commit_id2))
446 response.mustcontain('No files')
446 response.mustcontain('No files')
447 response.mustcontain('No commits in this compare')
447 response.mustcontain('No commits in this compare')
448
448
449 commit0 = commit_change(
449 commit0 = commit_change(
450 repo=r1_name, filename='file2',
450 repo=r1_name, filename='file2',
451 content='line1-added-after-fork', message='commit2-parent',
451 content='line1-added-after-fork', message='commit2-parent',
452 vcs_type=backend.alias, parent=None, newfile=True)
452 vcs_type=backend.alias, parent=None, newfile=True)
453
453
454 # compare !
454 # compare !
455 response = self.app.get(
455 response = self.app.get(
456 route_path('repo_compare',
456 route_path('repo_compare',
457 repo_name=r2_name,
457 repo_name=r2_name,
458 source_ref_type="branch", source_ref=commit_id1,
458 source_ref_type="branch", source_ref=commit_id1,
459 target_ref_type="branch", target_ref=commit_id2,
459 target_ref_type="branch", target_ref=commit_id2,
460 params=dict(merge='1', target_repo=r1_name),
460 params=dict(merge='1', target_repo=r1_name),
461 ))
461 ))
462
462
463 response.mustcontain('%s@%s' % (r2_name, commit_id1))
463 response.mustcontain('%s@%s' % (r2_name, commit_id1))
464 response.mustcontain('%s@%s' % (r1_name, commit_id2))
464 response.mustcontain('%s@%s' % (r1_name, commit_id2))
465
465
466 response.mustcontain("""commit2-parent""")
466 response.mustcontain("""commit2-parent""")
467 response.mustcontain("""line1-added-after-fork""")
467 response.mustcontain("""line1-added-after-fork""")
468 compare_page = ComparePage(response)
468 compare_page = ComparePage(response)
469 compare_page.contains_change_summary(1, 1, 0)
469 compare_page.contains_change_summary(1, 1, 0)
470
470
471 @pytest.mark.xfail_backends("svn")
471 @pytest.mark.xfail_backends("svn")
472 def test_compare_commits(self, backend, xhr_header):
472 def test_compare_commits(self, backend, xhr_header):
473 commit0 = backend.repo.get_commit(commit_idx=0)
473 commit0 = backend.repo.get_commit(commit_idx=0)
474 commit1 = backend.repo.get_commit(commit_idx=1)
474 commit1 = backend.repo.get_commit(commit_idx=1)
475
475
476 response = self.app.get(
476 response = self.app.get(
477 route_path('repo_compare',
477 route_path('repo_compare',
478 repo_name=backend.repo_name,
478 repo_name=backend.repo_name,
479 source_ref_type="rev", source_ref=commit0.raw_id,
479 source_ref_type="rev", source_ref=commit0.raw_id,
480 target_ref_type="rev", target_ref=commit1.raw_id,
480 target_ref_type="rev", target_ref=commit1.raw_id,
481 params=dict(merge='1')
481 params=dict(merge='1')
482 ),
482 ),
483 extra_environ=xhr_header, )
483 extra_environ=xhr_header, )
484
484
485 # outgoing commits between those commits
485 # outgoing commits between those commits
486 compare_page = ComparePage(response)
486 compare_page = ComparePage(response)
487 compare_page.contains_commits(commits=[commit1])
487 compare_page.contains_commits(commits=[commit1])
488
488
489 def test_errors_when_comparing_unknown_source_repo(self, backend):
489 def test_errors_when_comparing_unknown_source_repo(self, backend):
490 repo = backend.repo
490 repo = backend.repo
491 badrepo = 'badrepo'
491 badrepo = 'badrepo'
492
492
493 response = self.app.get(
493 response = self.app.get(
494 route_path('repo_compare',
494 route_path('repo_compare',
495 repo_name=badrepo,
495 repo_name=badrepo,
496 source_ref_type="rev", source_ref='tip',
496 source_ref_type="rev", source_ref='tip',
497 target_ref_type="rev", target_ref='tip',
497 target_ref_type="rev", target_ref='tip',
498 params=dict(merge='1', target_repo=repo.repo_name)
498 params=dict(merge='1', target_repo=repo.repo_name)
499 ),
499 ),
500 status=404)
500 status=404)
501
501
502 def test_errors_when_comparing_unknown_target_repo(self, backend):
502 def test_errors_when_comparing_unknown_target_repo(self, backend):
503 repo = backend.repo
503 repo = backend.repo
504 badrepo = 'badrepo'
504 badrepo = 'badrepo'
505
505
506 response = self.app.get(
506 response = self.app.get(
507 route_path('repo_compare',
507 route_path('repo_compare',
508 repo_name=repo.repo_name,
508 repo_name=repo.repo_name,
509 source_ref_type="rev", source_ref='tip',
509 source_ref_type="rev", source_ref='tip',
510 target_ref_type="rev", target_ref='tip',
510 target_ref_type="rev", target_ref='tip',
511 params=dict(merge='1', target_repo=badrepo),
511 params=dict(merge='1', target_repo=badrepo),
512 ),
512 ),
513 status=302)
513 status=302)
514 redirected = response.follow()
514 redirected = response.follow()
515 redirected.mustcontain(
515 redirected.mustcontain(
516 'Could not find the target repo: `{}`'.format(badrepo))
516 'Could not find the target repo: `{}`'.format(badrepo))
517
517
518 def test_compare_not_in_preview_mode(self, backend_stub):
518 def test_compare_not_in_preview_mode(self, backend_stub):
519 commit0 = backend_stub.repo.get_commit(commit_idx=0)
519 commit0 = backend_stub.repo.get_commit(commit_idx=0)
520 commit1 = backend_stub.repo.get_commit(commit_idx=1)
520 commit1 = backend_stub.repo.get_commit(commit_idx=1)
521
521
522 response = self.app.get(
522 response = self.app.get(
523 route_path('repo_compare',
523 route_path('repo_compare',
524 repo_name=backend_stub.repo_name,
524 repo_name=backend_stub.repo_name,
525 source_ref_type="rev", source_ref=commit0.raw_id,
525 source_ref_type="rev", source_ref=commit0.raw_id,
526 target_ref_type="rev", target_ref=commit1.raw_id,
526 target_ref_type="rev", target_ref=commit1.raw_id,
527 ))
527 ))
528
528
529 # outgoing commits between those commits
529 # outgoing commits between those commits
530 compare_page = ComparePage(response)
530 compare_page = ComparePage(response)
531 compare_page.swap_is_visible()
531 compare_page.swap_is_visible()
532 compare_page.target_source_are_enabled()
532 compare_page.target_source_are_enabled()
533
533
534 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
534 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
535 orig = backend_hg.create_repo(number_of_commits=1)
535 orig = backend_hg.create_repo(number_of_commits=1)
536 fork = backend_hg.create_fork()
536 fork = backend_hg.create_fork()
537
537
538 settings_util.create_repo_rhodecode_ui(
538 settings_util.create_repo_rhodecode_ui(
539 orig, 'extensions', value='', key='largefiles', active=False)
539 orig, 'extensions', value='', key='largefiles', active=False)
540 settings_util.create_repo_rhodecode_ui(
540 settings_util.create_repo_rhodecode_ui(
541 fork, 'extensions', value='', key='largefiles', active=True)
541 fork, 'extensions', value='', key='largefiles', active=True)
542
542
543 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
543 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
544 'MercurialRepository.compare')
544 'MercurialRepository.compare')
545 with mock.patch(compare_module) as compare_mock:
545 with mock.patch(compare_module) as compare_mock:
546 compare_mock.side_effect = RepositoryRequirementError()
546 compare_mock.side_effect = RepositoryRequirementError()
547
547
548 response = self.app.get(
548 response = self.app.get(
549 route_path('repo_compare',
549 route_path('repo_compare',
550 repo_name=orig.repo_name,
550 repo_name=orig.repo_name,
551 source_ref_type="rev", source_ref="tip",
551 source_ref_type="rev", source_ref="tip",
552 target_ref_type="rev", target_ref="tip",
552 target_ref_type="rev", target_ref="tip",
553 params=dict(merge='1', target_repo=fork.repo_name),
553 params=dict(merge='1', target_repo=fork.repo_name),
554 ),
554 ),
555 status=302)
555 status=302)
556
556
557 assert_session_flash(
557 assert_session_flash(
558 response,
558 response,
559 'Could not compare repos with different large file settings')
559 'Could not compare repos with different large file settings')
560
560
561
561
562 @pytest.mark.usefixtures("autologin_user")
562 @pytest.mark.usefixtures("autologin_user")
563 class TestCompareControllerSvn(object):
563 class TestCompareControllerSvn(object):
564
564
565 def test_supports_references_with_path(self, app, backend_svn):
565 def test_supports_references_with_path(self, app, backend_svn):
566 repo = backend_svn['svn-simple-layout']
566 repo = backend_svn['svn-simple-layout']
567 commit_id = repo.get_commit(commit_idx=-1).raw_id
567 commit_id = repo.get_commit(commit_idx=-1).raw_id
568 response = app.get(
568 response = app.get(
569 route_path('repo_compare',
569 route_path('repo_compare',
570 repo_name=repo.repo_name,
570 repo_name=repo.repo_name,
571 source_ref_type="tag",
571 source_ref_type="tag",
572 source_ref="%s@%s" % ('tags/v0.1', commit_id),
572 source_ref="%s@%s" % ('tags/v0.1', commit_id),
573 target_ref_type="tag",
573 target_ref_type="tag",
574 target_ref="%s@%s" % ('tags/v0.2', commit_id),
574 target_ref="%s@%s" % ('tags/v0.2', commit_id),
575 params=dict(merge='1'),
575 params=dict(merge='1'),
576 ),
576 ),
577 status=200)
577 status=200)
578
578
579 # Expecting no commits, since both paths are at the same revision
579 # Expecting no commits, since both paths are at the same revision
580 response.mustcontain('No commits in this compare')
580 response.mustcontain('No commits in this compare')
581
581
582 # Should find only one file changed when comparing those two tags
582 # Should find only one file changed when comparing those two tags
583 response.mustcontain('example.py')
583 response.mustcontain('example.py')
584 compare_page = ComparePage(response)
584 compare_page = ComparePage(response)
585 compare_page.contains_change_summary(1, 5, 1)
585 compare_page.contains_change_summary(1, 5, 1)
586
586
587 def test_shows_commits_if_different_ids(self, app, backend_svn):
587 def test_shows_commits_if_different_ids(self, app, backend_svn):
588 repo = backend_svn['svn-simple-layout']
588 repo = backend_svn['svn-simple-layout']
589 source_id = repo.get_commit(commit_idx=-6).raw_id
589 source_id = repo.get_commit(commit_idx=-6).raw_id
590 target_id = repo.get_commit(commit_idx=-1).raw_id
590 target_id = repo.get_commit(commit_idx=-1).raw_id
591 response = app.get(
591 response = app.get(
592 route_path('repo_compare',
592 route_path('repo_compare',
593 repo_name=repo.repo_name,
593 repo_name=repo.repo_name,
594 source_ref_type="tag",
594 source_ref_type="tag",
595 source_ref="%s@%s" % ('tags/v0.1', source_id),
595 source_ref="%s@%s" % ('tags/v0.1', source_id),
596 target_ref_type="tag",
596 target_ref_type="tag",
597 target_ref="%s@%s" % ('tags/v0.2', target_id),
597 target_ref="%s@%s" % ('tags/v0.2', target_id),
598 params=dict(merge='1')
598 params=dict(merge='1')
599 ),
599 ),
600 status=200)
600 status=200)
601
601
602 # It should show commits
602 # It should show commits
603 assert 'No commits in this compare' not in response.body
603 assert 'No commits in this compare' not in response.body
604
604
605 # Should find only one file changed when comparing those two tags
605 # Should find only one file changed when comparing those two tags
606 response.mustcontain('example.py')
606 response.mustcontain('example.py')
607 compare_page = ComparePage(response)
607 compare_page = ComparePage(response)
608 compare_page.contains_change_summary(1, 5, 1)
608 compare_page.contains_change_summary(1, 5, 1)
609
609
610
610
611 class ComparePage(AssertResponse):
611 class ComparePage(AssertResponse):
612 """
612 """
613 Abstracts the page template from the tests
613 Abstracts the page template from the tests
614 """
614 """
615
615
616 def contains_file_links_and_anchors(self, files):
616 def contains_file_links_and_anchors(self, files):
617 doc = lxml.html.fromstring(self.response.body)
617 doc = lxml.html.fromstring(self.response.body)
618 for filename, file_id in files:
618 for filename, file_id in files:
619 self.contains_one_anchor(file_id)
619 self.contains_one_anchor(file_id)
620 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
620 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
621 assert len(diffblock) == 2
621 assert len(diffblock) == 2
622 for lnk in diffblock[0].cssselect('a'):
622 for lnk in diffblock[0].cssselect('a'):
623 if 'permalink' in lnk.text:
623 if 'permalink' in lnk.text:
624 assert '#{}'.format(file_id) in lnk.attrib['href']
624 assert '#{}'.format(file_id) in lnk.attrib['href']
625 break
625 break
626 else:
626 else:
627 pytest.fail('Unable to find permalink')
627 pytest.fail('Unable to find permalink')
628
628
629 def contains_change_summary(self, files_changed, inserted, deleted):
629 def contains_change_summary(self, files_changed, inserted, deleted):
630 template = (
630 template = (
631 '{files_changed} file{plural} changed: '
631 '{files_changed} file{plural} changed: '
632 '<span class="op-added">{inserted} inserted</span>, <span class="op-deleted">{deleted} deleted</span>')
632 '<span class="op-added">{inserted} inserted</span>, <span class="op-deleted">{deleted} deleted</span>')
633 self.response.mustcontain(template.format(
633 self.response.mustcontain(template.format(
634 files_changed=files_changed,
634 files_changed=files_changed,
635 plural="s" if files_changed > 1 else "",
635 plural="s" if files_changed > 1 else "",
636 inserted=inserted,
636 inserted=inserted,
637 deleted=deleted))
637 deleted=deleted))
638
638
639 def contains_commits(self, commits, ancestors=None):
639 def contains_commits(self, commits, ancestors=None):
640 response = self.response
640 response = self.response
641
641
642 for commit in commits:
642 for commit in commits:
643 # Expecting to see the commit message in an element which
643 # Expecting to see the commit message in an element which
644 # has the ID "c-{commit.raw_id}"
644 # has the ID "c-{commit.raw_id}"
645 self.element_contains('#c-' + commit.raw_id, commit.message)
645 self.element_contains('#c-' + commit.raw_id, commit.message)
646 self.contains_one_link(
646 self.contains_one_link(
647 'r%s:%s' % (commit.idx, commit.short_id),
647 'r%s:%s' % (commit.idx, commit.short_id),
648 self._commit_url(commit))
648 self._commit_url(commit))
649
649
650 if ancestors:
650 if ancestors:
651 response.mustcontain('Ancestor')
651 response.mustcontain('Ancestor')
652 for ancestor in ancestors:
652 for ancestor in ancestors:
653 self.contains_one_link(
653 self.contains_one_link(
654 ancestor.short_id, self._commit_url(ancestor))
654 ancestor.short_id, self._commit_url(ancestor))
655
655
656 def _commit_url(self, commit):
656 def _commit_url(self, commit):
657 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
657 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
658
658
659 def swap_is_hidden(self):
659 def swap_is_hidden(self):
660 assert '<a id="btn-swap"' not in self.response.text
660 assert '<a id="btn-swap"' not in self.response.text
661
661
662 def swap_is_visible(self):
662 def swap_is_visible(self):
663 assert '<a id="btn-swap"' in self.response.text
663 assert '<a id="btn-swap"' in self.response.text
664
664
665 def target_source_are_disabled(self):
665 def target_source_are_disabled(self):
666 response = self.response
666 response = self.response
667 response.mustcontain("var enable_fields = false;")
667 response.mustcontain("var enable_fields = false;")
668 response.mustcontain('.select2("enable", enable_fields)')
668 response.mustcontain('.select2("enable", enable_fields)')
669
669
670 def target_source_are_enabled(self):
670 def target_source_are_enabled(self):
671 response = self.response
671 response = self.response
672 response.mustcontain("var enable_fields = true;")
672 response.mustcontain("var enable_fields = true;")
@@ -1,167 +1,167 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 .test_repo_compare import ComparePage
23 from .test_repo_compare import ComparePage
24
24
25
25
26 def route_path(name, params=None, **kwargs):
26 def route_path(name, params=None, **kwargs):
27 import urllib
27 import urllib.request, urllib.parse, urllib.error
28
28
29 base_url = {
29 base_url = {
30 'repo_compare_select': '/{repo_name}/compare',
30 'repo_compare_select': '/{repo_name}/compare',
31 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
31 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
32 }[name].format(**kwargs)
32 }[name].format(**kwargs)
33
33
34 if params:
34 if params:
35 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
36 return base_url
36 return base_url
37
37
38
38
39 @pytest.mark.usefixtures("autologin_user", "app")
39 @pytest.mark.usefixtures("autologin_user", "app")
40 class TestCompareView(object):
40 class TestCompareView(object):
41
41
42 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
42 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
43 def test_compare_tag(self, backend):
43 def test_compare_tag(self, backend):
44 tag1 = 'v0.1.2'
44 tag1 = 'v0.1.2'
45 tag2 = 'v0.1.3'
45 tag2 = 'v0.1.3'
46 response = self.app.get(
46 response = self.app.get(
47 route_path(
47 route_path(
48 'repo_compare',
48 'repo_compare',
49 repo_name=backend.repo_name,
49 repo_name=backend.repo_name,
50 source_ref_type="tag", source_ref=tag1,
50 source_ref_type="tag", source_ref=tag1,
51 target_ref_type="tag", target_ref=tag2),
51 target_ref_type="tag", target_ref=tag2),
52 status=200)
52 status=200)
53
53
54 response.mustcontain('%s@%s' % (backend.repo_name, tag1))
54 response.mustcontain('%s@%s' % (backend.repo_name, tag1))
55 response.mustcontain('%s@%s' % (backend.repo_name, tag2))
55 response.mustcontain('%s@%s' % (backend.repo_name, tag2))
56
56
57 # outgoing commits between tags
57 # outgoing commits between tags
58 commit_indexes = {
58 commit_indexes = {
59 'git': [113] + range(115, 121),
59 'git': [113] + range(115, 121),
60 'hg': [112] + range(115, 121),
60 'hg': [112] + range(115, 121),
61 }
61 }
62 repo = backend.repo
62 repo = backend.repo
63 commits = (repo.get_commit(commit_idx=idx)
63 commits = (repo.get_commit(commit_idx=idx)
64 for idx in commit_indexes[backend.alias])
64 for idx in commit_indexes[backend.alias])
65 compare_page = ComparePage(response)
65 compare_page = ComparePage(response)
66 compare_page.contains_change_summary(11, 94, 64)
66 compare_page.contains_change_summary(11, 94, 64)
67 compare_page.contains_commits(commits)
67 compare_page.contains_commits(commits)
68
68
69 # files diff
69 # files diff
70 short_id = short_id_new = ''
70 short_id = short_id_new = ''
71 if backend.alias == 'git':
71 if backend.alias == 'git':
72 short_id = '5a3a8fb00555'
72 short_id = '5a3a8fb00555'
73 short_id_new = '0ba5f8a46600'
73 short_id_new = '0ba5f8a46600'
74 if backend.alias == 'hg':
74 if backend.alias == 'hg':
75 short_id = '17544fbfcd33'
75 short_id = '17544fbfcd33'
76 short_id_new = 'a7e60bff65d5'
76 short_id_new = 'a7e60bff65d5'
77
77
78 compare_page.contains_file_links_and_anchors([
78 compare_page.contains_file_links_and_anchors([
79 # modified
79 # modified
80 ('docs/api/utils/index.rst', 'a_c-{}-1c5cf9e91c12'.format(short_id)),
80 ('docs/api/utils/index.rst', 'a_c-{}-1c5cf9e91c12'.format(short_id)),
81 ('test_and_report.sh', 'a_c-{}-e3305437df55'.format(short_id)),
81 ('test_and_report.sh', 'a_c-{}-e3305437df55'.format(short_id)),
82 # added
82 # added
83 ('.hgignore', 'a_c-{}-c8e92ef85cd1'.format(short_id_new)),
83 ('.hgignore', 'a_c-{}-c8e92ef85cd1'.format(short_id_new)),
84 ('.hgtags', 'a_c-{}-6e08b694d687'.format(short_id_new)),
84 ('.hgtags', 'a_c-{}-6e08b694d687'.format(short_id_new)),
85 ('docs/api/index.rst', 'a_c-{}-2c14b00f3393'.format(short_id_new)),
85 ('docs/api/index.rst', 'a_c-{}-2c14b00f3393'.format(short_id_new)),
86 ('vcs/__init__.py', 'a_c-{}-430ccbc82bdf'.format(short_id_new)),
86 ('vcs/__init__.py', 'a_c-{}-430ccbc82bdf'.format(short_id_new)),
87 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(short_id_new)),
87 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(short_id_new)),
88 ('vcs/utils/__init__.py', 'a_c-{}-ebb592c595c0'.format(short_id_new)),
88 ('vcs/utils/__init__.py', 'a_c-{}-ebb592c595c0'.format(short_id_new)),
89 ('vcs/utils/annotate.py', 'a_c-{}-7abc741b5052'.format(short_id_new)),
89 ('vcs/utils/annotate.py', 'a_c-{}-7abc741b5052'.format(short_id_new)),
90 ('vcs/utils/diffs.py', 'a_c-{}-2ef0ef106c56'.format(short_id_new)),
90 ('vcs/utils/diffs.py', 'a_c-{}-2ef0ef106c56'.format(short_id_new)),
91 ('vcs/utils/lazy.py', 'a_c-{}-3150cb87d4b7'.format(short_id_new)),
91 ('vcs/utils/lazy.py', 'a_c-{}-3150cb87d4b7'.format(short_id_new)),
92 ])
92 ])
93
93
94 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
94 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
95 def test_compare_tag_branch(self, backend):
95 def test_compare_tag_branch(self, backend):
96 revisions = {
96 revisions = {
97 'hg': {
97 'hg': {
98 'tag': 'v0.2.0',
98 'tag': 'v0.2.0',
99 'branch': 'default',
99 'branch': 'default',
100 'response': (147, 5701, 10177)
100 'response': (147, 5701, 10177)
101 },
101 },
102 'git': {
102 'git': {
103 'tag': 'v0.2.2',
103 'tag': 'v0.2.2',
104 'branch': 'master',
104 'branch': 'master',
105 'response': (70, 1855, 3002)
105 'response': (70, 1855, 3002)
106 },
106 },
107 }
107 }
108
108
109 # Backend specific data, depends on the test repository for
109 # Backend specific data, depends on the test repository for
110 # functional tests.
110 # functional tests.
111 data = revisions[backend.alias]
111 data = revisions[backend.alias]
112
112
113 response = self.app.get(
113 response = self.app.get(
114 route_path(
114 route_path(
115 'repo_compare',
115 'repo_compare',
116 repo_name=backend.repo_name,
116 repo_name=backend.repo_name,
117 source_ref_type='branch', source_ref=data['branch'],
117 source_ref_type='branch', source_ref=data['branch'],
118 target_ref_type="tag", target_ref=data['tag'],
118 target_ref_type="tag", target_ref=data['tag'],
119 ))
119 ))
120
120
121 response.mustcontain('%s@%s' % (backend.repo_name, data['branch']))
121 response.mustcontain('%s@%s' % (backend.repo_name, data['branch']))
122 response.mustcontain('%s@%s' % (backend.repo_name, data['tag']))
122 response.mustcontain('%s@%s' % (backend.repo_name, data['tag']))
123 compare_page = ComparePage(response)
123 compare_page = ComparePage(response)
124 compare_page.contains_change_summary(*data['response'])
124 compare_page.contains_change_summary(*data['response'])
125
125
126 def test_index_branch(self, backend):
126 def test_index_branch(self, backend):
127 head_id = backend.default_head_id
127 head_id = backend.default_head_id
128 response = self.app.get(
128 response = self.app.get(
129 route_path(
129 route_path(
130 'repo_compare',
130 'repo_compare',
131 repo_name=backend.repo_name,
131 repo_name=backend.repo_name,
132 source_ref_type="branch", source_ref=head_id,
132 source_ref_type="branch", source_ref=head_id,
133 target_ref_type="branch", target_ref=head_id,
133 target_ref_type="branch", target_ref=head_id,
134 ))
134 ))
135
135
136 response.mustcontain('%s@%s' % (backend.repo_name, head_id))
136 response.mustcontain('%s@%s' % (backend.repo_name, head_id))
137
137
138 # branches are equal
138 # branches are equal
139 response.mustcontain('No files')
139 response.mustcontain('No files')
140 response.mustcontain('No commits in this compare')
140 response.mustcontain('No commits in this compare')
141
141
142 def test_compare_commits(self, backend):
142 def test_compare_commits(self, backend):
143 repo = backend.repo
143 repo = backend.repo
144 commit1 = repo.get_commit(commit_idx=0)
144 commit1 = repo.get_commit(commit_idx=0)
145 commit1_short_id = commit1.short_id
145 commit1_short_id = commit1.short_id
146 commit2 = repo.get_commit(commit_idx=1)
146 commit2 = repo.get_commit(commit_idx=1)
147 commit2_short_id = commit2.short_id
147 commit2_short_id = commit2.short_id
148
148
149 response = self.app.get(
149 response = self.app.get(
150 route_path(
150 route_path(
151 'repo_compare',
151 'repo_compare',
152 repo_name=backend.repo_name,
152 repo_name=backend.repo_name,
153 source_ref_type="rev", source_ref=commit1.raw_id,
153 source_ref_type="rev", source_ref=commit1.raw_id,
154 target_ref_type="rev", target_ref=commit2.raw_id,
154 target_ref_type="rev", target_ref=commit2.raw_id,
155 ))
155 ))
156 response.mustcontain('%s@%s' % (backend.repo_name, commit1.raw_id))
156 response.mustcontain('%s@%s' % (backend.repo_name, commit1.raw_id))
157 response.mustcontain('%s@%s' % (backend.repo_name, commit2.raw_id))
157 response.mustcontain('%s@%s' % (backend.repo_name, commit2.raw_id))
158 compare_page = ComparePage(response)
158 compare_page = ComparePage(response)
159
159
160 # files
160 # files
161 compare_page.contains_change_summary(1, 7, 0)
161 compare_page.contains_change_summary(1, 7, 0)
162
162
163 # outgoing commits between those commits
163 # outgoing commits between those commits
164 compare_page.contains_commits([commit2])
164 compare_page.contains_commits([commit2])
165 anchor = 'a_c-{}-c8e92ef85cd1'.format(commit2_short_id)
165 anchor = 'a_c-{}-c8e92ef85cd1'.format(commit2_short_id)
166 response.mustcontain(anchor)
166 response.mustcontain(anchor)
167 compare_page.contains_file_links_and_anchors([('.hgignore', anchor),])
167 compare_page.contains_file_links_and_anchors([('.hgignore', anchor),])
@@ -1,291 +1,291 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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.apps.repository.tests.test_repo_compare import ComparePage
23 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
24 from rhodecode.lib.vcs import nodes
24 from rhodecode.lib.vcs import nodes
25 from rhodecode.lib.vcs.backends.base import EmptyCommit
25 from rhodecode.lib.vcs.backends.base import EmptyCommit
26 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.fixture import Fixture
27 from rhodecode.tests.utils import commit_change
27 from rhodecode.tests.utils import commit_change
28
28
29 fixture = Fixture()
29 fixture = Fixture()
30
30
31
31
32 def route_path(name, params=None, **kwargs):
32 def route_path(name, params=None, **kwargs):
33 import urllib
33 import urllib.request, urllib.parse, urllib.error
34
34
35 base_url = {
35 base_url = {
36 'repo_compare_select': '/{repo_name}/compare',
36 'repo_compare_select': '/{repo_name}/compare',
37 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
37 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
38 }[name].format(**kwargs)
38 }[name].format(**kwargs)
39
39
40 if params:
40 if params:
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 return base_url
42 return base_url
43
43
44
44
45 @pytest.mark.usefixtures("autologin_user", "app")
45 @pytest.mark.usefixtures("autologin_user", "app")
46 class TestSideBySideDiff(object):
46 class TestSideBySideDiff(object):
47
47
48 def test_diff_sidebyside_single_commit(self, app, backend):
48 def test_diff_sidebyside_single_commit(self, app, backend):
49 commit_id_range = {
49 commit_id_range = {
50 'hg': {
50 'hg': {
51 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
51 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
52 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
52 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
53 'changes': (21, 943, 288),
53 'changes': (21, 943, 288),
54 },
54 },
55 'git': {
55 'git': {
56 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
56 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
57 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
57 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
58 'changes': (20, 941, 286),
58 'changes': (20, 941, 286),
59 },
59 },
60
60
61 'svn': {
61 'svn': {
62 'commits': ['336',
62 'commits': ['336',
63 '337'],
63 '337'],
64 'changes': (21, 943, 288),
64 'changes': (21, 943, 288),
65 },
65 },
66 }
66 }
67
67
68 commit_info = commit_id_range[backend.alias]
68 commit_info = commit_id_range[backend.alias]
69 commit2, commit1 = commit_info['commits']
69 commit2, commit1 = commit_info['commits']
70 file_changes = commit_info['changes']
70 file_changes = commit_info['changes']
71
71
72 response = self.app.get(route_path(
72 response = self.app.get(route_path(
73 'repo_compare',
73 'repo_compare',
74 repo_name=backend.repo_name,
74 repo_name=backend.repo_name,
75 source_ref_type='rev',
75 source_ref_type='rev',
76 source_ref=commit2,
76 source_ref=commit2,
77 target_repo=backend.repo_name,
77 target_repo=backend.repo_name,
78 target_ref_type='rev',
78 target_ref_type='rev',
79 target_ref=commit1,
79 target_ref=commit1,
80 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
80 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
81 ))
81 ))
82
82
83 compare_page = ComparePage(response)
83 compare_page = ComparePage(response)
84 compare_page.contains_change_summary(*file_changes)
84 compare_page.contains_change_summary(*file_changes)
85 response.mustcontain('Collapse 1 commit')
85 response.mustcontain('Collapse 1 commit')
86
86
87 def test_diff_sidebyside_two_commits(self, app, backend):
87 def test_diff_sidebyside_two_commits(self, app, backend):
88 commit_id_range = {
88 commit_id_range = {
89 'hg': {
89 'hg': {
90 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
90 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
91 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
91 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
92 'changes': (32, 1165, 308),
92 'changes': (32, 1165, 308),
93 },
93 },
94 'git': {
94 'git': {
95 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
95 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
96 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
96 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
97 'changes': (31, 1163, 306),
97 'changes': (31, 1163, 306),
98 },
98 },
99
99
100 'svn': {
100 'svn': {
101 'commits': ['335',
101 'commits': ['335',
102 '337'],
102 '337'],
103 'changes': (32, 1179, 310),
103 'changes': (32, 1179, 310),
104 },
104 },
105 }
105 }
106
106
107 commit_info = commit_id_range[backend.alias]
107 commit_info = commit_id_range[backend.alias]
108 commit2, commit1 = commit_info['commits']
108 commit2, commit1 = commit_info['commits']
109 file_changes = commit_info['changes']
109 file_changes = commit_info['changes']
110
110
111 response = self.app.get(route_path(
111 response = self.app.get(route_path(
112 'repo_compare',
112 'repo_compare',
113 repo_name=backend.repo_name,
113 repo_name=backend.repo_name,
114 source_ref_type='rev',
114 source_ref_type='rev',
115 source_ref=commit2,
115 source_ref=commit2,
116 target_repo=backend.repo_name,
116 target_repo=backend.repo_name,
117 target_ref_type='rev',
117 target_ref_type='rev',
118 target_ref=commit1,
118 target_ref=commit1,
119 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
119 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
120 ))
120 ))
121
121
122 compare_page = ComparePage(response)
122 compare_page = ComparePage(response)
123 compare_page.contains_change_summary(*file_changes)
123 compare_page.contains_change_summary(*file_changes)
124
124
125 response.mustcontain('Collapse 2 commits')
125 response.mustcontain('Collapse 2 commits')
126
126
127 def test_diff_sidebyside_collapsed_commits(self, app, backend_svn):
127 def test_diff_sidebyside_collapsed_commits(self, app, backend_svn):
128 commit_id_range = {
128 commit_id_range = {
129
129
130 'svn': {
130 'svn': {
131 'commits': ['330',
131 'commits': ['330',
132 '337'],
132 '337'],
133
133
134 },
134 },
135 }
135 }
136
136
137 commit_info = commit_id_range['svn']
137 commit_info = commit_id_range['svn']
138 commit2, commit1 = commit_info['commits']
138 commit2, commit1 = commit_info['commits']
139
139
140 response = self.app.get(route_path(
140 response = self.app.get(route_path(
141 'repo_compare',
141 'repo_compare',
142 repo_name=backend_svn.repo_name,
142 repo_name=backend_svn.repo_name,
143 source_ref_type='rev',
143 source_ref_type='rev',
144 source_ref=commit2,
144 source_ref=commit2,
145 target_repo=backend_svn.repo_name,
145 target_repo=backend_svn.repo_name,
146 target_ref_type='rev',
146 target_ref_type='rev',
147 target_ref=commit1,
147 target_ref=commit1,
148 params=dict(target_repo=backend_svn.repo_name, diffmode='sidebyside')
148 params=dict(target_repo=backend_svn.repo_name, diffmode='sidebyside')
149 ))
149 ))
150
150
151 response.mustcontain('Expand 7 commits')
151 response.mustcontain('Expand 7 commits')
152
152
153 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
153 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
154 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
154 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
155 f_path = 'test_sidebyside_file.py'
155 f_path = 'test_sidebyside_file.py'
156 commit1_content = 'content-25d7e49c18b159446c\n'
156 commit1_content = 'content-25d7e49c18b159446c\n'
157 commit2_content = 'content-603d6c72c46d953420\n'
157 commit2_content = 'content-603d6c72c46d953420\n'
158 repo = backend.create_repo()
158 repo = backend.create_repo()
159
159
160 commit1 = commit_change(
160 commit1 = commit_change(
161 repo.repo_name, filename=f_path, content=commit1_content,
161 repo.repo_name, filename=f_path, content=commit1_content,
162 message='A', vcs_type=backend.alias, parent=None, newfile=True)
162 message='A', vcs_type=backend.alias, parent=None, newfile=True)
163
163
164 commit2 = commit_change(
164 commit2 = commit_change(
165 repo.repo_name, filename=f_path, content=commit2_content,
165 repo.repo_name, filename=f_path, content=commit2_content,
166 message='B, child of A', vcs_type=backend.alias, parent=commit1)
166 message='B, child of A', vcs_type=backend.alias, parent=commit1)
167
167
168 response = self.app.get(route_path(
168 response = self.app.get(route_path(
169 'repo_compare',
169 'repo_compare',
170 repo_name=repo.repo_name,
170 repo_name=repo.repo_name,
171 source_ref_type='rev',
171 source_ref_type='rev',
172 source_ref=EmptyCommit().raw_id,
172 source_ref=EmptyCommit().raw_id,
173 target_ref_type='rev',
173 target_ref_type='rev',
174 target_ref=commit2.raw_id,
174 target_ref=commit2.raw_id,
175 params=dict(diffmode='sidebyside')
175 params=dict(diffmode='sidebyside')
176 ))
176 ))
177
177
178 response.mustcontain('Collapse 2 commits')
178 response.mustcontain('Collapse 2 commits')
179 response.mustcontain('123 file changed')
179 response.mustcontain('123 file changed')
180
180
181 response.mustcontain(
181 response.mustcontain(
182 'r%s:%s...r%s:%s' % (
182 'r%s:%s...r%s:%s' % (
183 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
183 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
184
184
185 response.mustcontain(f_path)
185 response.mustcontain(f_path)
186
186
187 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
187 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
188 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
188 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
189 f_path = 'test_sidebyside_file.py'
189 f_path = 'test_sidebyside_file.py'
190 commit1_content = 'content-25d7e49c18b159446c\n'
190 commit1_content = 'content-25d7e49c18b159446c\n'
191 commit2_content = 'content-603d6c72c46d953420\n'
191 commit2_content = 'content-603d6c72c46d953420\n'
192 repo = backend.create_repo()
192 repo = backend.create_repo()
193
193
194 commit1 = commit_change(
194 commit1 = commit_change(
195 repo.repo_name, filename=f_path, content=commit1_content,
195 repo.repo_name, filename=f_path, content=commit1_content,
196 message='A', vcs_type=backend.alias, parent=None, newfile=True)
196 message='A', vcs_type=backend.alias, parent=None, newfile=True)
197
197
198 commit2 = commit_change(
198 commit2 = commit_change(
199 repo.repo_name, filename=f_path, content=commit2_content,
199 repo.repo_name, filename=f_path, content=commit2_content,
200 message='B, child of A', vcs_type=backend.alias, parent=commit1)
200 message='B, child of A', vcs_type=backend.alias, parent=commit1)
201
201
202 response = self.app.get(route_path(
202 response = self.app.get(route_path(
203 'repo_compare',
203 'repo_compare',
204 repo_name=repo.repo_name,
204 repo_name=repo.repo_name,
205 source_ref_type='rev',
205 source_ref_type='rev',
206 source_ref=EmptyCommit().raw_id,
206 source_ref=EmptyCommit().raw_id,
207 target_ref_type='rev',
207 target_ref_type='rev',
208 target_ref=commit2.raw_id,
208 target_ref=commit2.raw_id,
209 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
209 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
210 ))
210 ))
211
211
212 response.mustcontain('Collapse 2 commits')
212 response.mustcontain('Collapse 2 commits')
213 response.mustcontain('1 file changed')
213 response.mustcontain('1 file changed')
214
214
215 response.mustcontain(
215 response.mustcontain(
216 'r%s:%s...r%s:%s' % (
216 'r%s:%s...r%s:%s' % (
217 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
217 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
218
218
219 response.mustcontain(f_path)
219 response.mustcontain(f_path)
220
220
221 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
221 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
222 commits = [
222 commits = [
223 {'message': 'First commit'},
223 {'message': 'First commit'},
224 {'message': 'Second commit'},
224 {'message': 'Second commit'},
225 {'message': 'Commit with binary',
225 {'message': 'Commit with binary',
226 'added': [nodes.FileNode('file.empty', content='')]},
226 'added': [nodes.FileNode('file.empty', content='')]},
227 ]
227 ]
228 f_path = 'file.empty'
228 f_path = 'file.empty'
229 repo = backend.create_repo(commits=commits)
229 repo = backend.create_repo(commits=commits)
230 commit1 = repo.get_commit(commit_idx=0)
230 commit1 = repo.get_commit(commit_idx=0)
231 commit2 = repo.get_commit(commit_idx=1)
231 commit2 = repo.get_commit(commit_idx=1)
232 commit3 = repo.get_commit(commit_idx=2)
232 commit3 = repo.get_commit(commit_idx=2)
233
233
234 response = self.app.get(route_path(
234 response = self.app.get(route_path(
235 'repo_compare',
235 'repo_compare',
236 repo_name=repo.repo_name,
236 repo_name=repo.repo_name,
237 source_ref_type='rev',
237 source_ref_type='rev',
238 source_ref=commit1.raw_id,
238 source_ref=commit1.raw_id,
239 target_ref_type='rev',
239 target_ref_type='rev',
240 target_ref=commit3.raw_id,
240 target_ref=commit3.raw_id,
241 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
241 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
242 ))
242 ))
243
243
244 response.mustcontain('Collapse 2 commits')
244 response.mustcontain('Collapse 2 commits')
245 response.mustcontain('1 file changed')
245 response.mustcontain('1 file changed')
246
246
247 response.mustcontain(
247 response.mustcontain(
248 'r%s:%s...r%s:%s' % (
248 'r%s:%s...r%s:%s' % (
249 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
249 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
250
250
251 response.mustcontain(f_path)
251 response.mustcontain(f_path)
252
252
253 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
253 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
254 commit_id_range = {
254 commit_id_range = {
255 'hg': {
255 'hg': {
256 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
256 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
257 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
257 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
258 'changes': (1, 3, 3)
258 'changes': (1, 3, 3)
259 },
259 },
260 'git': {
260 'git': {
261 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
261 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
262 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
262 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
263 'changes': (1, 3, 3)
263 'changes': (1, 3, 3)
264 },
264 },
265
265
266 'svn': {
266 'svn': {
267 'commits': ['335',
267 'commits': ['335',
268 '337'],
268 '337'],
269 'changes': (1, 3, 3)
269 'changes': (1, 3, 3)
270 },
270 },
271 }
271 }
272 f_path = 'docs/conf.py'
272 f_path = 'docs/conf.py'
273
273
274 commit_info = commit_id_range[backend.alias]
274 commit_info = commit_id_range[backend.alias]
275 commit2, commit1 = commit_info['commits']
275 commit2, commit1 = commit_info['commits']
276 file_changes = commit_info['changes']
276 file_changes = commit_info['changes']
277
277
278 response = self.app.get(route_path(
278 response = self.app.get(route_path(
279 'repo_compare',
279 'repo_compare',
280 repo_name=backend.repo_name,
280 repo_name=backend.repo_name,
281 source_ref_type='rev',
281 source_ref_type='rev',
282 source_ref=commit2,
282 source_ref=commit2,
283 target_ref_type='rev',
283 target_ref_type='rev',
284 target_ref=commit1,
284 target_ref=commit1,
285 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
285 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
286 ))
286 ))
287
287
288 response.mustcontain('Collapse 2 commits')
288 response.mustcontain('Collapse 2 commits')
289
289
290 compare_page = ComparePage(response)
290 compare_page = ComparePage(response)
291 compare_page.contains_change_summary(*file_changes)
291 compare_page.contains_change_summary(*file_changes)
@@ -1,137 +1,137 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 rhodecode.model.auth_token import AuthTokenModel
22 from rhodecode.model.auth_token import AuthTokenModel
23 from rhodecode.tests import TestController
23 from rhodecode.tests import TestController
24
24
25
25
26 def route_path(name, params=None, **kwargs):
26 def route_path(name, params=None, **kwargs):
27 import urllib
27 import urllib.request, urllib.parse, urllib.error
28
28
29 base_url = {
29 base_url = {
30 'rss_feed_home': '/{repo_name}/feed-rss',
30 'rss_feed_home': '/{repo_name}/feed-rss',
31 'atom_feed_home': '/{repo_name}/feed-atom',
31 'atom_feed_home': '/{repo_name}/feed-atom',
32 'rss_feed_home_old': '/{repo_name}/feed/rss',
32 'rss_feed_home_old': '/{repo_name}/feed/rss',
33 'atom_feed_home_old': '/{repo_name}/feed/atom',
33 'atom_feed_home_old': '/{repo_name}/feed/atom',
34 }[name].format(**kwargs)
34 }[name].format(**kwargs)
35
35
36 if params:
36 if params:
37 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 return base_url
38 return base_url
39
39
40
40
41 class TestFeedView(TestController):
41 class TestFeedView(TestController):
42
42
43 @pytest.mark.parametrize("feed_type,response_types,content_type",[
43 @pytest.mark.parametrize("feed_type,response_types,content_type",[
44 ('rss', ['<rss version="2.0"'],
44 ('rss', ['<rss version="2.0"'],
45 "application/rss+xml"),
45 "application/rss+xml"),
46 ('atom', ['xmlns="http://www.w3.org/2005/Atom"', 'xml:lang="en-us"'],
46 ('atom', ['xmlns="http://www.w3.org/2005/Atom"', 'xml:lang="en-us"'],
47 "application/atom+xml"),
47 "application/atom+xml"),
48 ])
48 ])
49 def test_feed(self, backend, feed_type, response_types, content_type):
49 def test_feed(self, backend, feed_type, response_types, content_type):
50 self.log_user()
50 self.log_user()
51 response = self.app.get(
51 response = self.app.get(
52 route_path('{}_feed_home'.format(feed_type),
52 route_path('{}_feed_home'.format(feed_type),
53 repo_name=backend.repo_name))
53 repo_name=backend.repo_name))
54
54
55 for content in response_types:
55 for content in response_types:
56 response.mustcontain(content)
56 response.mustcontain(content)
57
57
58 assert response.content_type == content_type
58 assert response.content_type == content_type
59
59
60 @pytest.mark.parametrize("feed_type, content_type", [
60 @pytest.mark.parametrize("feed_type, content_type", [
61 ('rss', "application/rss+xml"),
61 ('rss', "application/rss+xml"),
62 ('atom', "application/atom+xml")
62 ('atom', "application/atom+xml")
63 ])
63 ])
64 def test_feed_with_auth_token(
64 def test_feed_with_auth_token(
65 self, backend, user_admin, feed_type, content_type):
65 self, backend, user_admin, feed_type, content_type):
66 auth_token = user_admin.feed_token
66 auth_token = user_admin.feed_token
67 assert auth_token != ''
67 assert auth_token != ''
68
68
69 response = self.app.get(
69 response = self.app.get(
70 route_path(
70 route_path(
71 '{}_feed_home'.format(feed_type),
71 '{}_feed_home'.format(feed_type),
72 repo_name=backend.repo_name,
72 repo_name=backend.repo_name,
73 params=dict(auth_token=auth_token)),
73 params=dict(auth_token=auth_token)),
74 status=200)
74 status=200)
75
75
76 assert response.content_type == content_type
76 assert response.content_type == content_type
77
77
78 @pytest.mark.parametrize("feed_type, content_type", [
78 @pytest.mark.parametrize("feed_type, content_type", [
79 ('rss', "application/rss+xml"),
79 ('rss', "application/rss+xml"),
80 ('atom', "application/atom+xml")
80 ('atom', "application/atom+xml")
81 ])
81 ])
82 def test_feed_with_auth_token_by_uid(
82 def test_feed_with_auth_token_by_uid(
83 self, backend, user_admin, feed_type, content_type):
83 self, backend, user_admin, feed_type, content_type):
84 auth_token = user_admin.feed_token
84 auth_token = user_admin.feed_token
85 assert auth_token != ''
85 assert auth_token != ''
86
86
87 response = self.app.get(
87 response = self.app.get(
88 route_path(
88 route_path(
89 '{}_feed_home'.format(feed_type),
89 '{}_feed_home'.format(feed_type),
90 repo_name='_{}'.format(backend.repo.repo_id),
90 repo_name='_{}'.format(backend.repo.repo_id),
91 params=dict(auth_token=auth_token)),
91 params=dict(auth_token=auth_token)),
92 status=200)
92 status=200)
93
93
94 assert response.content_type == content_type
94 assert response.content_type == content_type
95
95
96 @pytest.mark.parametrize("feed_type, content_type", [
96 @pytest.mark.parametrize("feed_type, content_type", [
97 ('rss', "application/rss+xml"),
97 ('rss', "application/rss+xml"),
98 ('atom', "application/atom+xml")
98 ('atom', "application/atom+xml")
99 ])
99 ])
100 def test_feed_old_urls_with_auth_token(
100 def test_feed_old_urls_with_auth_token(
101 self, backend, user_admin, feed_type, content_type):
101 self, backend, user_admin, feed_type, content_type):
102 auth_token = user_admin.feed_token
102 auth_token = user_admin.feed_token
103 assert auth_token != ''
103 assert auth_token != ''
104
104
105 response = self.app.get(
105 response = self.app.get(
106 route_path(
106 route_path(
107 '{}_feed_home_old'.format(feed_type),
107 '{}_feed_home_old'.format(feed_type),
108 repo_name=backend.repo_name,
108 repo_name=backend.repo_name,
109 params=dict(auth_token=auth_token)),
109 params=dict(auth_token=auth_token)),
110 status=200)
110 status=200)
111
111
112 assert response.content_type == content_type
112 assert response.content_type == content_type
113
113
114 @pytest.mark.parametrize("feed_type", ['rss', 'atom'])
114 @pytest.mark.parametrize("feed_type", ['rss', 'atom'])
115 def test_feed_with_auth_token_of_wrong_type(
115 def test_feed_with_auth_token_of_wrong_type(
116 self, backend, user_util, feed_type):
116 self, backend, user_util, feed_type):
117 user = user_util.create_user()
117 user = user_util.create_user()
118 auth_token = AuthTokenModel().create(
118 auth_token = AuthTokenModel().create(
119 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_API)
119 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_API)
120 auth_token = auth_token.api_key
120 auth_token = auth_token.api_key
121
121
122 self.app.get(
122 self.app.get(
123 route_path(
123 route_path(
124 '{}_feed_home'.format(feed_type),
124 '{}_feed_home'.format(feed_type),
125 repo_name=backend.repo_name,
125 repo_name=backend.repo_name,
126 params=dict(auth_token=auth_token)),
126 params=dict(auth_token=auth_token)),
127 status=302)
127 status=302)
128
128
129 auth_token = AuthTokenModel().create(
129 auth_token = AuthTokenModel().create(
130 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
130 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
131 auth_token = auth_token.api_key
131 auth_token = auth_token.api_key
132 self.app.get(
132 self.app.get(
133 route_path(
133 route_path(
134 '{}_feed_home'.format(feed_type),
134 '{}_feed_home'.format(feed_type),
135 repo_name=backend.repo_name,
135 repo_name=backend.repo_name,
136 params=dict(auth_token=auth_token)),
136 params=dict(auth_token=auth_token)),
137 status=200)
137 status=200)
@@ -1,1092 +1,1092 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
26 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
27 from rhodecode.apps.repository.views.repo_files import RepoFilesView
27 from rhodecode.apps.repository.views.repo_files import RepoFilesView
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.compat import OrderedDict
29 from rhodecode.lib.compat import OrderedDict
30 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.ext_json import json
31 from rhodecode.lib.vcs import nodes
31 from rhodecode.lib.vcs import nodes
32
32
33 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.lib.vcs.conf import settings
34 from rhodecode.tests import assert_session_flash
34 from rhodecode.tests import assert_session_flash
35 from rhodecode.tests.fixture import Fixture
35 from rhodecode.tests.fixture import Fixture
36 from rhodecode.model.db import Session
36 from rhodecode.model.db import Session
37
37
38 fixture = Fixture()
38 fixture = Fixture()
39
39
40
40
41 def get_node_history(backend_type):
41 def get_node_history(backend_type):
42 return {
42 return {
43 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
43 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
44 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
44 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
45 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
45 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
46 }[backend_type]
46 }[backend_type]
47
47
48
48
49 def route_path(name, params=None, **kwargs):
49 def route_path(name, params=None, **kwargs):
50 import urllib
50 import urllib.request, urllib.parse, urllib.error
51
51
52 base_url = {
52 base_url = {
53 'repo_summary': '/{repo_name}',
53 'repo_summary': '/{repo_name}',
54 'repo_archivefile': '/{repo_name}/archive/{fname}',
54 'repo_archivefile': '/{repo_name}/archive/{fname}',
55 'repo_files_diff': '/{repo_name}/diff/{f_path}',
55 'repo_files_diff': '/{repo_name}/diff/{f_path}',
56 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
56 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
57 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
57 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
58 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
58 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
59 'repo_files:default_commit': '/{repo_name}/files',
59 'repo_files:default_commit': '/{repo_name}/files',
60 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
60 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
61 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
61 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
62 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
62 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
63 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
63 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
64 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
64 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
65 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
65 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
66 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
66 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
67 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
67 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
68 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
68 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
69 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
69 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
70 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
70 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
71 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
71 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
72 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
72 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
73 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
73 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
74 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
74 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
75 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
75 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
76 }[name].format(**kwargs)
76 }[name].format(**kwargs)
77
77
78 if params:
78 if params:
79 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
79 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
80 return base_url
80 return base_url
81
81
82
82
83 def assert_files_in_response(response, files, params):
83 def assert_files_in_response(response, files, params):
84 template = (
84 template = (
85 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
85 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
86 _assert_items_in_response(response, files, template, params)
86 _assert_items_in_response(response, files, template, params)
87
87
88
88
89 def assert_dirs_in_response(response, dirs, params):
89 def assert_dirs_in_response(response, dirs, params):
90 template = (
90 template = (
91 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
91 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
92 _assert_items_in_response(response, dirs, template, params)
92 _assert_items_in_response(response, dirs, template, params)
93
93
94
94
95 def _assert_items_in_response(response, items, template, params):
95 def _assert_items_in_response(response, items, template, params):
96 for item in items:
96 for item in items:
97 item_params = {'name': item}
97 item_params = {'name': item}
98 item_params.update(params)
98 item_params.update(params)
99 response.mustcontain(template % item_params)
99 response.mustcontain(template % item_params)
100
100
101
101
102 def assert_timeago_in_response(response, items, params):
102 def assert_timeago_in_response(response, items, params):
103 for item in items:
103 for item in items:
104 response.mustcontain(h.age_component(params['date']))
104 response.mustcontain(h.age_component(params['date']))
105
105
106
106
107 @pytest.mark.usefixtures("app")
107 @pytest.mark.usefixtures("app")
108 class TestFilesViews(object):
108 class TestFilesViews(object):
109
109
110 def test_show_files(self, backend):
110 def test_show_files(self, backend):
111 response = self.app.get(
111 response = self.app.get(
112 route_path('repo_files',
112 route_path('repo_files',
113 repo_name=backend.repo_name,
113 repo_name=backend.repo_name,
114 commit_id='tip', f_path='/'))
114 commit_id='tip', f_path='/'))
115 commit = backend.repo.get_commit()
115 commit = backend.repo.get_commit()
116
116
117 params = {
117 params = {
118 'repo_name': backend.repo_name,
118 'repo_name': backend.repo_name,
119 'commit_id': commit.raw_id,
119 'commit_id': commit.raw_id,
120 'date': commit.date
120 'date': commit.date
121 }
121 }
122 assert_dirs_in_response(response, ['docs', 'vcs'], params)
122 assert_dirs_in_response(response, ['docs', 'vcs'], params)
123 files = [
123 files = [
124 '.gitignore',
124 '.gitignore',
125 '.hgignore',
125 '.hgignore',
126 '.hgtags',
126 '.hgtags',
127 # TODO: missing in Git
127 # TODO: missing in Git
128 # '.travis.yml',
128 # '.travis.yml',
129 'MANIFEST.in',
129 'MANIFEST.in',
130 'README.rst',
130 'README.rst',
131 # TODO: File is missing in svn repository
131 # TODO: File is missing in svn repository
132 # 'run_test_and_report.sh',
132 # 'run_test_and_report.sh',
133 'setup.cfg',
133 'setup.cfg',
134 'setup.py',
134 'setup.py',
135 'test_and_report.sh',
135 'test_and_report.sh',
136 'tox.ini',
136 'tox.ini',
137 ]
137 ]
138 assert_files_in_response(response, files, params)
138 assert_files_in_response(response, files, params)
139 assert_timeago_in_response(response, files, params)
139 assert_timeago_in_response(response, files, params)
140
140
141 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
141 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
142 repo = backend_hg['subrepos']
142 repo = backend_hg['subrepos']
143 response = self.app.get(
143 response = self.app.get(
144 route_path('repo_files',
144 route_path('repo_files',
145 repo_name=repo.repo_name,
145 repo_name=repo.repo_name,
146 commit_id='tip', f_path='/'))
146 commit_id='tip', f_path='/'))
147 assert_response = response.assert_response()
147 assert_response = response.assert_response()
148 assert_response.contains_one_link(
148 assert_response.contains_one_link(
149 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
149 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
150
150
151 def test_show_files_links_submodules_with_absolute_url_subpaths(
151 def test_show_files_links_submodules_with_absolute_url_subpaths(
152 self, backend_hg):
152 self, backend_hg):
153 repo = backend_hg['subrepos']
153 repo = backend_hg['subrepos']
154 response = self.app.get(
154 response = self.app.get(
155 route_path('repo_files',
155 route_path('repo_files',
156 repo_name=repo.repo_name,
156 repo_name=repo.repo_name,
157 commit_id='tip', f_path='/'))
157 commit_id='tip', f_path='/'))
158 assert_response = response.assert_response()
158 assert_response = response.assert_response()
159 assert_response.contains_one_link(
159 assert_response.contains_one_link(
160 'subpaths-path @ 000000000000',
160 'subpaths-path @ 000000000000',
161 'http://sub-base.example.com/subpaths-path')
161 'http://sub-base.example.com/subpaths-path')
162
162
163 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
163 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
164 def test_files_menu(self, backend):
164 def test_files_menu(self, backend):
165 new_branch = "temp_branch_name"
165 new_branch = "temp_branch_name"
166 commits = [
166 commits = [
167 {'message': 'a'},
167 {'message': 'a'},
168 {'message': 'b', 'branch': new_branch}
168 {'message': 'b', 'branch': new_branch}
169 ]
169 ]
170 backend.create_repo(commits)
170 backend.create_repo(commits)
171 backend.repo.landing_rev = "branch:%s" % new_branch
171 backend.repo.landing_rev = "branch:%s" % new_branch
172 Session().commit()
172 Session().commit()
173
173
174 # get response based on tip and not new commit
174 # get response based on tip and not new commit
175 response = self.app.get(
175 response = self.app.get(
176 route_path('repo_files',
176 route_path('repo_files',
177 repo_name=backend.repo_name,
177 repo_name=backend.repo_name,
178 commit_id='tip', f_path='/'))
178 commit_id='tip', f_path='/'))
179
179
180 # make sure Files menu url is not tip but new commit
180 # make sure Files menu url is not tip but new commit
181 landing_rev = backend.repo.landing_ref_name
181 landing_rev = backend.repo.landing_ref_name
182 files_url = route_path('repo_files:default_path',
182 files_url = route_path('repo_files:default_path',
183 repo_name=backend.repo_name,
183 repo_name=backend.repo_name,
184 commit_id=landing_rev, params={'at': landing_rev})
184 commit_id=landing_rev, params={'at': landing_rev})
185
185
186 assert landing_rev != 'tip'
186 assert landing_rev != 'tip'
187 response.mustcontain(
187 response.mustcontain(
188 '<li class="active"><a class="menulink" href="%s">' % files_url)
188 '<li class="active"><a class="menulink" href="%s">' % files_url)
189
189
190 def test_show_files_commit(self, backend):
190 def test_show_files_commit(self, backend):
191 commit = backend.repo.get_commit(commit_idx=32)
191 commit = backend.repo.get_commit(commit_idx=32)
192
192
193 response = self.app.get(
193 response = self.app.get(
194 route_path('repo_files',
194 route_path('repo_files',
195 repo_name=backend.repo_name,
195 repo_name=backend.repo_name,
196 commit_id=commit.raw_id, f_path='/'))
196 commit_id=commit.raw_id, f_path='/'))
197
197
198 dirs = ['docs', 'tests']
198 dirs = ['docs', 'tests']
199 files = ['README.rst']
199 files = ['README.rst']
200 params = {
200 params = {
201 'repo_name': backend.repo_name,
201 'repo_name': backend.repo_name,
202 'commit_id': commit.raw_id,
202 'commit_id': commit.raw_id,
203 }
203 }
204 assert_dirs_in_response(response, dirs, params)
204 assert_dirs_in_response(response, dirs, params)
205 assert_files_in_response(response, files, params)
205 assert_files_in_response(response, files, params)
206
206
207 def test_show_files_different_branch(self, backend):
207 def test_show_files_different_branch(self, backend):
208 branches = dict(
208 branches = dict(
209 hg=(150, ['git']),
209 hg=(150, ['git']),
210 # TODO: Git test repository does not contain other branches
210 # TODO: Git test repository does not contain other branches
211 git=(633, ['master']),
211 git=(633, ['master']),
212 # TODO: Branch support in Subversion
212 # TODO: Branch support in Subversion
213 svn=(150, [])
213 svn=(150, [])
214 )
214 )
215 idx, branches = branches[backend.alias]
215 idx, branches = branches[backend.alias]
216 commit = backend.repo.get_commit(commit_idx=idx)
216 commit = backend.repo.get_commit(commit_idx=idx)
217 response = self.app.get(
217 response = self.app.get(
218 route_path('repo_files',
218 route_path('repo_files',
219 repo_name=backend.repo_name,
219 repo_name=backend.repo_name,
220 commit_id=commit.raw_id, f_path='/'))
220 commit_id=commit.raw_id, f_path='/'))
221
221
222 assert_response = response.assert_response()
222 assert_response = response.assert_response()
223 for branch in branches:
223 for branch in branches:
224 assert_response.element_contains('.tags .branchtag', branch)
224 assert_response.element_contains('.tags .branchtag', branch)
225
225
226 def test_show_files_paging(self, backend):
226 def test_show_files_paging(self, backend):
227 repo = backend.repo
227 repo = backend.repo
228 indexes = [73, 92, 109, 1, 0]
228 indexes = [73, 92, 109, 1, 0]
229 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
229 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
230 for rev in indexes]
230 for rev in indexes]
231
231
232 for idx in idx_map:
232 for idx in idx_map:
233 response = self.app.get(
233 response = self.app.get(
234 route_path('repo_files',
234 route_path('repo_files',
235 repo_name=backend.repo_name,
235 repo_name=backend.repo_name,
236 commit_id=idx[1], f_path='/'))
236 commit_id=idx[1], f_path='/'))
237
237
238 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
238 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
239
239
240 def test_file_source(self, backend):
240 def test_file_source(self, backend):
241 commit = backend.repo.get_commit(commit_idx=167)
241 commit = backend.repo.get_commit(commit_idx=167)
242 response = self.app.get(
242 response = self.app.get(
243 route_path('repo_files',
243 route_path('repo_files',
244 repo_name=backend.repo_name,
244 repo_name=backend.repo_name,
245 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
245 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
246
246
247 msgbox = """<div class="commit">%s</div>"""
247 msgbox = """<div class="commit">%s</div>"""
248 response.mustcontain(msgbox % (commit.message, ))
248 response.mustcontain(msgbox % (commit.message, ))
249
249
250 assert_response = response.assert_response()
250 assert_response = response.assert_response()
251 if commit.branch:
251 if commit.branch:
252 assert_response.element_contains(
252 assert_response.element_contains(
253 '.tags.tags-main .branchtag', commit.branch)
253 '.tags.tags-main .branchtag', commit.branch)
254 if commit.tags:
254 if commit.tags:
255 for tag in commit.tags:
255 for tag in commit.tags:
256 assert_response.element_contains('.tags.tags-main .tagtag', tag)
256 assert_response.element_contains('.tags.tags-main .tagtag', tag)
257
257
258 def test_file_source_annotated(self, backend):
258 def test_file_source_annotated(self, backend):
259 response = self.app.get(
259 response = self.app.get(
260 route_path('repo_files:annotated',
260 route_path('repo_files:annotated',
261 repo_name=backend.repo_name,
261 repo_name=backend.repo_name,
262 commit_id='tip', f_path='vcs/nodes.py'))
262 commit_id='tip', f_path='vcs/nodes.py'))
263 expected_commits = {
263 expected_commits = {
264 'hg': 'r356',
264 'hg': 'r356',
265 'git': 'r345',
265 'git': 'r345',
266 'svn': 'r208',
266 'svn': 'r208',
267 }
267 }
268 response.mustcontain(expected_commits[backend.alias])
268 response.mustcontain(expected_commits[backend.alias])
269
269
270 def test_file_source_authors(self, backend):
270 def test_file_source_authors(self, backend):
271 response = self.app.get(
271 response = self.app.get(
272 route_path('repo_file_authors',
272 route_path('repo_file_authors',
273 repo_name=backend.repo_name,
273 repo_name=backend.repo_name,
274 commit_id='tip', f_path='vcs/nodes.py'))
274 commit_id='tip', f_path='vcs/nodes.py'))
275 expected_authors = {
275 expected_authors = {
276 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
276 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
278 'svn': ('marcin', 'lukasz'),
278 'svn': ('marcin', 'lukasz'),
279 }
279 }
280
280
281 for author in expected_authors[backend.alias]:
281 for author in expected_authors[backend.alias]:
282 response.mustcontain(author)
282 response.mustcontain(author)
283
283
284 def test_file_source_authors_with_annotation(self, backend):
284 def test_file_source_authors_with_annotation(self, backend):
285 response = self.app.get(
285 response = self.app.get(
286 route_path('repo_file_authors',
286 route_path('repo_file_authors',
287 repo_name=backend.repo_name,
287 repo_name=backend.repo_name,
288 commit_id='tip', f_path='vcs/nodes.py',
288 commit_id='tip', f_path='vcs/nodes.py',
289 params=dict(annotate=1)))
289 params=dict(annotate=1)))
290 expected_authors = {
290 expected_authors = {
291 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
291 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
293 'svn': ('marcin', 'lukasz'),
293 'svn': ('marcin', 'lukasz'),
294 }
294 }
295
295
296 for author in expected_authors[backend.alias]:
296 for author in expected_authors[backend.alias]:
297 response.mustcontain(author)
297 response.mustcontain(author)
298
298
299 def test_file_source_history(self, backend, xhr_header):
299 def test_file_source_history(self, backend, xhr_header):
300 response = self.app.get(
300 response = self.app.get(
301 route_path('repo_file_history',
301 route_path('repo_file_history',
302 repo_name=backend.repo_name,
302 repo_name=backend.repo_name,
303 commit_id='tip', f_path='vcs/nodes.py'),
303 commit_id='tip', f_path='vcs/nodes.py'),
304 extra_environ=xhr_header)
304 extra_environ=xhr_header)
305 assert get_node_history(backend.alias) == json.loads(response.body)
305 assert get_node_history(backend.alias) == json.loads(response.body)
306
306
307 def test_file_source_history_svn(self, backend_svn, xhr_header):
307 def test_file_source_history_svn(self, backend_svn, xhr_header):
308 simple_repo = backend_svn['svn-simple-layout']
308 simple_repo = backend_svn['svn-simple-layout']
309 response = self.app.get(
309 response = self.app.get(
310 route_path('repo_file_history',
310 route_path('repo_file_history',
311 repo_name=simple_repo.repo_name,
311 repo_name=simple_repo.repo_name,
312 commit_id='tip', f_path='trunk/example.py'),
312 commit_id='tip', f_path='trunk/example.py'),
313 extra_environ=xhr_header)
313 extra_environ=xhr_header)
314
314
315 expected_data = json.loads(
315 expected_data = json.loads(
316 fixture.load_resource('svn_node_history_branches.json'))
316 fixture.load_resource('svn_node_history_branches.json'))
317
317
318 assert expected_data == response.json
318 assert expected_data == response.json
319
319
320 def test_file_source_history_with_annotation(self, backend, xhr_header):
320 def test_file_source_history_with_annotation(self, backend, xhr_header):
321 response = self.app.get(
321 response = self.app.get(
322 route_path('repo_file_history',
322 route_path('repo_file_history',
323 repo_name=backend.repo_name,
323 repo_name=backend.repo_name,
324 commit_id='tip', f_path='vcs/nodes.py',
324 commit_id='tip', f_path='vcs/nodes.py',
325 params=dict(annotate=1)),
325 params=dict(annotate=1)),
326
326
327 extra_environ=xhr_header)
327 extra_environ=xhr_header)
328 assert get_node_history(backend.alias) == json.loads(response.body)
328 assert get_node_history(backend.alias) == json.loads(response.body)
329
329
330 def test_tree_search_top_level(self, backend, xhr_header):
330 def test_tree_search_top_level(self, backend, xhr_header):
331 commit = backend.repo.get_commit(commit_idx=173)
331 commit = backend.repo.get_commit(commit_idx=173)
332 response = self.app.get(
332 response = self.app.get(
333 route_path('repo_files_nodelist',
333 route_path('repo_files_nodelist',
334 repo_name=backend.repo_name,
334 repo_name=backend.repo_name,
335 commit_id=commit.raw_id, f_path='/'),
335 commit_id=commit.raw_id, f_path='/'),
336 extra_environ=xhr_header)
336 extra_environ=xhr_header)
337 assert 'nodes' in response.json
337 assert 'nodes' in response.json
338 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
338 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
339
339
340 def test_tree_search_missing_xhr(self, backend):
340 def test_tree_search_missing_xhr(self, backend):
341 self.app.get(
341 self.app.get(
342 route_path('repo_files_nodelist',
342 route_path('repo_files_nodelist',
343 repo_name=backend.repo_name,
343 repo_name=backend.repo_name,
344 commit_id='tip', f_path='/'),
344 commit_id='tip', f_path='/'),
345 status=404)
345 status=404)
346
346
347 def test_tree_search_at_path(self, backend, xhr_header):
347 def test_tree_search_at_path(self, backend, xhr_header):
348 commit = backend.repo.get_commit(commit_idx=173)
348 commit = backend.repo.get_commit(commit_idx=173)
349 response = self.app.get(
349 response = self.app.get(
350 route_path('repo_files_nodelist',
350 route_path('repo_files_nodelist',
351 repo_name=backend.repo_name,
351 repo_name=backend.repo_name,
352 commit_id=commit.raw_id, f_path='/docs'),
352 commit_id=commit.raw_id, f_path='/docs'),
353 extra_environ=xhr_header)
353 extra_environ=xhr_header)
354 assert 'nodes' in response.json
354 assert 'nodes' in response.json
355 nodes = response.json['nodes']
355 nodes = response.json['nodes']
356 assert {'name': 'docs/api', 'type': 'dir'} in nodes
356 assert {'name': 'docs/api', 'type': 'dir'} in nodes
357 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
357 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
358
358
359 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
359 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
360 commit = backend.repo.get_commit(commit_idx=173)
360 commit = backend.repo.get_commit(commit_idx=173)
361 response = self.app.get(
361 response = self.app.get(
362 route_path('repo_files_nodelist',
362 route_path('repo_files_nodelist',
363 repo_name=backend.repo_name,
363 repo_name=backend.repo_name,
364 commit_id=commit.raw_id, f_path='/docs/api'),
364 commit_id=commit.raw_id, f_path='/docs/api'),
365 extra_environ=xhr_header)
365 extra_environ=xhr_header)
366 assert 'nodes' in response.json
366 assert 'nodes' in response.json
367 nodes = response.json['nodes']
367 nodes = response.json['nodes']
368 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
368 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
369
369
370 def test_tree_search_at_path_missing_xhr(self, backend):
370 def test_tree_search_at_path_missing_xhr(self, backend):
371 self.app.get(
371 self.app.get(
372 route_path('repo_files_nodelist',
372 route_path('repo_files_nodelist',
373 repo_name=backend.repo_name,
373 repo_name=backend.repo_name,
374 commit_id='tip', f_path='/docs'),
374 commit_id='tip', f_path='/docs'),
375 status=404)
375 status=404)
376
376
377 def test_nodetree(self, backend, xhr_header):
377 def test_nodetree(self, backend, xhr_header):
378 commit = backend.repo.get_commit(commit_idx=173)
378 commit = backend.repo.get_commit(commit_idx=173)
379 response = self.app.get(
379 response = self.app.get(
380 route_path('repo_nodetree_full',
380 route_path('repo_nodetree_full',
381 repo_name=backend.repo_name,
381 repo_name=backend.repo_name,
382 commit_id=commit.raw_id, f_path='/'),
382 commit_id=commit.raw_id, f_path='/'),
383 extra_environ=xhr_header)
383 extra_environ=xhr_header)
384
384
385 assert_response = response.assert_response()
385 assert_response = response.assert_response()
386
386
387 for attr in ['data-commit-id', 'data-date', 'data-author']:
387 for attr in ['data-commit-id', 'data-date', 'data-author']:
388 elements = assert_response.get_elements('[{}]'.format(attr))
388 elements = assert_response.get_elements('[{}]'.format(attr))
389 assert len(elements) > 1
389 assert len(elements) > 1
390
390
391 for element in elements:
391 for element in elements:
392 assert element.get(attr)
392 assert element.get(attr)
393
393
394 def test_nodetree_if_file(self, backend, xhr_header):
394 def test_nodetree_if_file(self, backend, xhr_header):
395 commit = backend.repo.get_commit(commit_idx=173)
395 commit = backend.repo.get_commit(commit_idx=173)
396 response = self.app.get(
396 response = self.app.get(
397 route_path('repo_nodetree_full',
397 route_path('repo_nodetree_full',
398 repo_name=backend.repo_name,
398 repo_name=backend.repo_name,
399 commit_id=commit.raw_id, f_path='README.rst'),
399 commit_id=commit.raw_id, f_path='README.rst'),
400 extra_environ=xhr_header)
400 extra_environ=xhr_header)
401 assert response.body == ''
401 assert response.body == ''
402
402
403 def test_nodetree_wrong_path(self, backend, xhr_header):
403 def test_nodetree_wrong_path(self, backend, xhr_header):
404 commit = backend.repo.get_commit(commit_idx=173)
404 commit = backend.repo.get_commit(commit_idx=173)
405 response = self.app.get(
405 response = self.app.get(
406 route_path('repo_nodetree_full',
406 route_path('repo_nodetree_full',
407 repo_name=backend.repo_name,
407 repo_name=backend.repo_name,
408 commit_id=commit.raw_id, f_path='/dont-exist'),
408 commit_id=commit.raw_id, f_path='/dont-exist'),
409 extra_environ=xhr_header)
409 extra_environ=xhr_header)
410
410
411 err = 'error: There is no file nor ' \
411 err = 'error: There is no file nor ' \
412 'directory at the given path'
412 'directory at the given path'
413 assert err in response.body
413 assert err in response.body
414
414
415 def test_nodetree_missing_xhr(self, backend):
415 def test_nodetree_missing_xhr(self, backend):
416 self.app.get(
416 self.app.get(
417 route_path('repo_nodetree_full',
417 route_path('repo_nodetree_full',
418 repo_name=backend.repo_name,
418 repo_name=backend.repo_name,
419 commit_id='tip', f_path='/'),
419 commit_id='tip', f_path='/'),
420 status=404)
420 status=404)
421
421
422
422
423 @pytest.mark.usefixtures("app", "autologin_user")
423 @pytest.mark.usefixtures("app", "autologin_user")
424 class TestRawFileHandling(object):
424 class TestRawFileHandling(object):
425
425
426 def test_download_file(self, backend):
426 def test_download_file(self, backend):
427 commit = backend.repo.get_commit(commit_idx=173)
427 commit = backend.repo.get_commit(commit_idx=173)
428 response = self.app.get(
428 response = self.app.get(
429 route_path('repo_file_download',
429 route_path('repo_file_download',
430 repo_name=backend.repo_name,
430 repo_name=backend.repo_name,
431 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
431 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
432
432
433 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
433 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
434 assert response.content_type == "text/x-python"
434 assert response.content_type == "text/x-python"
435
435
436 def test_download_file_wrong_cs(self, backend):
436 def test_download_file_wrong_cs(self, backend):
437 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
437 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
438
438
439 response = self.app.get(
439 response = self.app.get(
440 route_path('repo_file_download',
440 route_path('repo_file_download',
441 repo_name=backend.repo_name,
441 repo_name=backend.repo_name,
442 commit_id=raw_id, f_path='vcs/nodes.svg'),
442 commit_id=raw_id, f_path='vcs/nodes.svg'),
443 status=404)
443 status=404)
444
444
445 msg = """No such commit exists for this repository"""
445 msg = """No such commit exists for this repository"""
446 response.mustcontain(msg)
446 response.mustcontain(msg)
447
447
448 def test_download_file_wrong_f_path(self, backend):
448 def test_download_file_wrong_f_path(self, backend):
449 commit = backend.repo.get_commit(commit_idx=173)
449 commit = backend.repo.get_commit(commit_idx=173)
450 f_path = 'vcs/ERRORnodes.py'
450 f_path = 'vcs/ERRORnodes.py'
451
451
452 response = self.app.get(
452 response = self.app.get(
453 route_path('repo_file_download',
453 route_path('repo_file_download',
454 repo_name=backend.repo_name,
454 repo_name=backend.repo_name,
455 commit_id=commit.raw_id, f_path=f_path),
455 commit_id=commit.raw_id, f_path=f_path),
456 status=404)
456 status=404)
457
457
458 msg = (
458 msg = (
459 "There is no file nor directory at the given path: "
459 "There is no file nor directory at the given path: "
460 "`%s` at commit %s" % (f_path, commit.short_id))
460 "`%s` at commit %s" % (f_path, commit.short_id))
461 response.mustcontain(msg)
461 response.mustcontain(msg)
462
462
463 def test_file_raw(self, backend):
463 def test_file_raw(self, backend):
464 commit = backend.repo.get_commit(commit_idx=173)
464 commit = backend.repo.get_commit(commit_idx=173)
465 response = self.app.get(
465 response = self.app.get(
466 route_path('repo_file_raw',
466 route_path('repo_file_raw',
467 repo_name=backend.repo_name,
467 repo_name=backend.repo_name,
468 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
468 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
469
469
470 assert response.content_type == "text/plain"
470 assert response.content_type == "text/plain"
471
471
472 def test_file_raw_binary(self, backend):
472 def test_file_raw_binary(self, backend):
473 commit = backend.repo.get_commit()
473 commit = backend.repo.get_commit()
474 response = self.app.get(
474 response = self.app.get(
475 route_path('repo_file_raw',
475 route_path('repo_file_raw',
476 repo_name=backend.repo_name,
476 repo_name=backend.repo_name,
477 commit_id=commit.raw_id,
477 commit_id=commit.raw_id,
478 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
478 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
479
479
480 assert response.content_disposition == 'inline'
480 assert response.content_disposition == 'inline'
481
481
482 def test_raw_file_wrong_cs(self, backend):
482 def test_raw_file_wrong_cs(self, backend):
483 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
483 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
484
484
485 response = self.app.get(
485 response = self.app.get(
486 route_path('repo_file_raw',
486 route_path('repo_file_raw',
487 repo_name=backend.repo_name,
487 repo_name=backend.repo_name,
488 commit_id=raw_id, f_path='vcs/nodes.svg'),
488 commit_id=raw_id, f_path='vcs/nodes.svg'),
489 status=404)
489 status=404)
490
490
491 msg = """No such commit exists for this repository"""
491 msg = """No such commit exists for this repository"""
492 response.mustcontain(msg)
492 response.mustcontain(msg)
493
493
494 def test_raw_wrong_f_path(self, backend):
494 def test_raw_wrong_f_path(self, backend):
495 commit = backend.repo.get_commit(commit_idx=173)
495 commit = backend.repo.get_commit(commit_idx=173)
496 f_path = 'vcs/ERRORnodes.py'
496 f_path = 'vcs/ERRORnodes.py'
497 response = self.app.get(
497 response = self.app.get(
498 route_path('repo_file_raw',
498 route_path('repo_file_raw',
499 repo_name=backend.repo_name,
499 repo_name=backend.repo_name,
500 commit_id=commit.raw_id, f_path=f_path),
500 commit_id=commit.raw_id, f_path=f_path),
501 status=404)
501 status=404)
502
502
503 msg = (
503 msg = (
504 "There is no file nor directory at the given path: "
504 "There is no file nor directory at the given path: "
505 "`%s` at commit %s" % (f_path, commit.short_id))
505 "`%s` at commit %s" % (f_path, commit.short_id))
506 response.mustcontain(msg)
506 response.mustcontain(msg)
507
507
508 def test_raw_svg_should_not_be_rendered(self, backend):
508 def test_raw_svg_should_not_be_rendered(self, backend):
509 backend.create_repo()
509 backend.create_repo()
510 backend.ensure_file("xss.svg")
510 backend.ensure_file("xss.svg")
511 response = self.app.get(
511 response = self.app.get(
512 route_path('repo_file_raw',
512 route_path('repo_file_raw',
513 repo_name=backend.repo_name,
513 repo_name=backend.repo_name,
514 commit_id='tip', f_path='xss.svg'),)
514 commit_id='tip', f_path='xss.svg'),)
515 # If the content type is image/svg+xml then it allows to render HTML
515 # If the content type is image/svg+xml then it allows to render HTML
516 # and malicious SVG.
516 # and malicious SVG.
517 assert response.content_type == "text/plain"
517 assert response.content_type == "text/plain"
518
518
519
519
520 @pytest.mark.usefixtures("app")
520 @pytest.mark.usefixtures("app")
521 class TestRepositoryArchival(object):
521 class TestRepositoryArchival(object):
522
522
523 def test_archival(self, backend):
523 def test_archival(self, backend):
524 backend.enable_downloads()
524 backend.enable_downloads()
525 commit = backend.repo.get_commit(commit_idx=173)
525 commit = backend.repo.get_commit(commit_idx=173)
526 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
526 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
527
527
528 short = commit.short_id + extension
528 short = commit.short_id + extension
529 fname = commit.raw_id + extension
529 fname = commit.raw_id + extension
530 filename = '%s-%s' % (backend.repo_name, short)
530 filename = '%s-%s' % (backend.repo_name, short)
531 response = self.app.get(
531 response = self.app.get(
532 route_path('repo_archivefile',
532 route_path('repo_archivefile',
533 repo_name=backend.repo_name,
533 repo_name=backend.repo_name,
534 fname=fname))
534 fname=fname))
535
535
536 assert response.status == '200 OK'
536 assert response.status == '200 OK'
537 headers = [
537 headers = [
538 ('Content-Disposition', 'attachment; filename=%s' % filename),
538 ('Content-Disposition', 'attachment; filename=%s' % filename),
539 ('Content-Type', '%s' % content_type),
539 ('Content-Type', '%s' % content_type),
540 ]
540 ]
541
541
542 for header in headers:
542 for header in headers:
543 assert header in response.headers.items()
543 assert header in response.headers.items()
544
544
545 def test_archival_no_hash(self, backend):
545 def test_archival_no_hash(self, backend):
546 backend.enable_downloads()
546 backend.enable_downloads()
547 commit = backend.repo.get_commit(commit_idx=173)
547 commit = backend.repo.get_commit(commit_idx=173)
548 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
548 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
549
549
550 short = 'plain' + extension
550 short = 'plain' + extension
551 fname = commit.raw_id + extension
551 fname = commit.raw_id + extension
552 filename = '%s-%s' % (backend.repo_name, short)
552 filename = '%s-%s' % (backend.repo_name, short)
553 response = self.app.get(
553 response = self.app.get(
554 route_path('repo_archivefile',
554 route_path('repo_archivefile',
555 repo_name=backend.repo_name,
555 repo_name=backend.repo_name,
556 fname=fname, params={'with_hash': 0}))
556 fname=fname, params={'with_hash': 0}))
557
557
558 assert response.status == '200 OK'
558 assert response.status == '200 OK'
559 headers = [
559 headers = [
560 ('Content-Disposition', 'attachment; filename=%s' % filename),
560 ('Content-Disposition', 'attachment; filename=%s' % filename),
561 ('Content-Type', '%s' % content_type),
561 ('Content-Type', '%s' % content_type),
562 ]
562 ]
563
563
564 for header in headers:
564 for header in headers:
565 assert header in response.headers.items()
565 assert header in response.headers.items()
566
566
567 @pytest.mark.parametrize('arch_ext',[
567 @pytest.mark.parametrize('arch_ext',[
568 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
568 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
569 def test_archival_wrong_ext(self, backend, arch_ext):
569 def test_archival_wrong_ext(self, backend, arch_ext):
570 backend.enable_downloads()
570 backend.enable_downloads()
571 commit = backend.repo.get_commit(commit_idx=173)
571 commit = backend.repo.get_commit(commit_idx=173)
572
572
573 fname = commit.raw_id + '.' + arch_ext
573 fname = commit.raw_id + '.' + arch_ext
574
574
575 response = self.app.get(
575 response = self.app.get(
576 route_path('repo_archivefile',
576 route_path('repo_archivefile',
577 repo_name=backend.repo_name,
577 repo_name=backend.repo_name,
578 fname=fname))
578 fname=fname))
579 response.mustcontain(
579 response.mustcontain(
580 'Unknown archive type for: `{}`'.format(fname))
580 'Unknown archive type for: `{}`'.format(fname))
581
581
582 @pytest.mark.parametrize('commit_id', [
582 @pytest.mark.parametrize('commit_id', [
583 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
583 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
584 def test_archival_wrong_commit_id(self, backend, commit_id):
584 def test_archival_wrong_commit_id(self, backend, commit_id):
585 backend.enable_downloads()
585 backend.enable_downloads()
586 fname = '%s.zip' % commit_id
586 fname = '%s.zip' % commit_id
587
587
588 response = self.app.get(
588 response = self.app.get(
589 route_path('repo_archivefile',
589 route_path('repo_archivefile',
590 repo_name=backend.repo_name,
590 repo_name=backend.repo_name,
591 fname=fname))
591 fname=fname))
592 response.mustcontain('Unknown commit_id')
592 response.mustcontain('Unknown commit_id')
593
593
594
594
595 @pytest.mark.usefixtures("app")
595 @pytest.mark.usefixtures("app")
596 class TestFilesDiff(object):
596 class TestFilesDiff(object):
597
597
598 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
598 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
599 def test_file_full_diff(self, backend, diff):
599 def test_file_full_diff(self, backend, diff):
600 commit1 = backend.repo.get_commit(commit_idx=-1)
600 commit1 = backend.repo.get_commit(commit_idx=-1)
601 commit2 = backend.repo.get_commit(commit_idx=-2)
601 commit2 = backend.repo.get_commit(commit_idx=-2)
602
602
603 response = self.app.get(
603 response = self.app.get(
604 route_path('repo_files_diff',
604 route_path('repo_files_diff',
605 repo_name=backend.repo_name,
605 repo_name=backend.repo_name,
606 f_path='README'),
606 f_path='README'),
607 params={
607 params={
608 'diff1': commit2.raw_id,
608 'diff1': commit2.raw_id,
609 'diff2': commit1.raw_id,
609 'diff2': commit1.raw_id,
610 'fulldiff': '1',
610 'fulldiff': '1',
611 'diff': diff,
611 'diff': diff,
612 })
612 })
613
613
614 if diff == 'diff':
614 if diff == 'diff':
615 # use redirect since this is OLD view redirecting to compare page
615 # use redirect since this is OLD view redirecting to compare page
616 response = response.follow()
616 response = response.follow()
617
617
618 # It's a symlink to README.rst
618 # It's a symlink to README.rst
619 response.mustcontain('README.rst')
619 response.mustcontain('README.rst')
620 response.mustcontain('No newline at end of file')
620 response.mustcontain('No newline at end of file')
621
621
622 def test_file_binary_diff(self, backend):
622 def test_file_binary_diff(self, backend):
623 commits = [
623 commits = [
624 {'message': 'First commit'},
624 {'message': 'First commit'},
625 {'message': 'Commit with binary',
625 {'message': 'Commit with binary',
626 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
626 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
627 ]
627 ]
628 repo = backend.create_repo(commits=commits)
628 repo = backend.create_repo(commits=commits)
629
629
630 response = self.app.get(
630 response = self.app.get(
631 route_path('repo_files_diff',
631 route_path('repo_files_diff',
632 repo_name=backend.repo_name,
632 repo_name=backend.repo_name,
633 f_path='file.bin'),
633 f_path='file.bin'),
634 params={
634 params={
635 'diff1': repo.get_commit(commit_idx=0).raw_id,
635 'diff1': repo.get_commit(commit_idx=0).raw_id,
636 'diff2': repo.get_commit(commit_idx=1).raw_id,
636 'diff2': repo.get_commit(commit_idx=1).raw_id,
637 'fulldiff': '1',
637 'fulldiff': '1',
638 'diff': 'diff',
638 'diff': 'diff',
639 })
639 })
640 # use redirect since this is OLD view redirecting to compare page
640 # use redirect since this is OLD view redirecting to compare page
641 response = response.follow()
641 response = response.follow()
642 response.mustcontain('Collapse 1 commit')
642 response.mustcontain('Collapse 1 commit')
643 file_changes = (1, 0, 0)
643 file_changes = (1, 0, 0)
644
644
645 compare_page = ComparePage(response)
645 compare_page = ComparePage(response)
646 compare_page.contains_change_summary(*file_changes)
646 compare_page.contains_change_summary(*file_changes)
647
647
648 if backend.alias == 'svn':
648 if backend.alias == 'svn':
649 response.mustcontain('new file 10644')
649 response.mustcontain('new file 10644')
650 # TODO(marcink): SVN doesn't yet detect binary changes
650 # TODO(marcink): SVN doesn't yet detect binary changes
651 else:
651 else:
652 response.mustcontain('new file 100644')
652 response.mustcontain('new file 100644')
653 response.mustcontain('binary diff hidden')
653 response.mustcontain('binary diff hidden')
654
654
655 def test_diff_2way(self, backend):
655 def test_diff_2way(self, backend):
656 commit1 = backend.repo.get_commit(commit_idx=-1)
656 commit1 = backend.repo.get_commit(commit_idx=-1)
657 commit2 = backend.repo.get_commit(commit_idx=-2)
657 commit2 = backend.repo.get_commit(commit_idx=-2)
658 response = self.app.get(
658 response = self.app.get(
659 route_path('repo_files_diff_2way_redirect',
659 route_path('repo_files_diff_2way_redirect',
660 repo_name=backend.repo_name,
660 repo_name=backend.repo_name,
661 f_path='README'),
661 f_path='README'),
662 params={
662 params={
663 'diff1': commit2.raw_id,
663 'diff1': commit2.raw_id,
664 'diff2': commit1.raw_id,
664 'diff2': commit1.raw_id,
665 })
665 })
666 # use redirect since this is OLD view redirecting to compare page
666 # use redirect since this is OLD view redirecting to compare page
667 response = response.follow()
667 response = response.follow()
668
668
669 # It's a symlink to README.rst
669 # It's a symlink to README.rst
670 response.mustcontain('README.rst')
670 response.mustcontain('README.rst')
671 response.mustcontain('No newline at end of file')
671 response.mustcontain('No newline at end of file')
672
672
673 def test_requires_one_commit_id(self, backend, autologin_user):
673 def test_requires_one_commit_id(self, backend, autologin_user):
674 response = self.app.get(
674 response = self.app.get(
675 route_path('repo_files_diff',
675 route_path('repo_files_diff',
676 repo_name=backend.repo_name,
676 repo_name=backend.repo_name,
677 f_path='README.rst'),
677 f_path='README.rst'),
678 status=400)
678 status=400)
679 response.mustcontain(
679 response.mustcontain(
680 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
680 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
681
681
682 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
682 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
683 repo = vcsbackend.repo
683 repo = vcsbackend.repo
684 response = self.app.get(
684 response = self.app.get(
685 route_path('repo_files_diff',
685 route_path('repo_files_diff',
686 repo_name=repo.name,
686 repo_name=repo.name,
687 f_path='does-not-exist-in-any-commit'),
687 f_path='does-not-exist-in-any-commit'),
688 params={
688 params={
689 'diff1': repo[0].raw_id,
689 'diff1': repo[0].raw_id,
690 'diff2': repo[1].raw_id
690 'diff2': repo[1].raw_id
691 })
691 })
692
692
693 response = response.follow()
693 response = response.follow()
694 response.mustcontain('No files')
694 response.mustcontain('No files')
695
695
696 def test_returns_redirect_if_file_not_changed(self, backend):
696 def test_returns_redirect_if_file_not_changed(self, backend):
697 commit = backend.repo.get_commit(commit_idx=-1)
697 commit = backend.repo.get_commit(commit_idx=-1)
698 response = self.app.get(
698 response = self.app.get(
699 route_path('repo_files_diff_2way_redirect',
699 route_path('repo_files_diff_2way_redirect',
700 repo_name=backend.repo_name,
700 repo_name=backend.repo_name,
701 f_path='README'),
701 f_path='README'),
702 params={
702 params={
703 'diff1': commit.raw_id,
703 'diff1': commit.raw_id,
704 'diff2': commit.raw_id,
704 'diff2': commit.raw_id,
705 })
705 })
706
706
707 response = response.follow()
707 response = response.follow()
708 response.mustcontain('No files')
708 response.mustcontain('No files')
709 response.mustcontain('No commits in this compare')
709 response.mustcontain('No commits in this compare')
710
710
711 def test_supports_diff_to_different_path_svn(self, backend_svn):
711 def test_supports_diff_to_different_path_svn(self, backend_svn):
712 #TODO: check this case
712 #TODO: check this case
713 return
713 return
714
714
715 repo = backend_svn['svn-simple-layout'].scm_instance()
715 repo = backend_svn['svn-simple-layout'].scm_instance()
716 commit_id_1 = '24'
716 commit_id_1 = '24'
717 commit_id_2 = '26'
717 commit_id_2 = '26'
718
718
719 response = self.app.get(
719 response = self.app.get(
720 route_path('repo_files_diff',
720 route_path('repo_files_diff',
721 repo_name=backend_svn.repo_name,
721 repo_name=backend_svn.repo_name,
722 f_path='trunk/example.py'),
722 f_path='trunk/example.py'),
723 params={
723 params={
724 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
724 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
725 'diff2': commit_id_2,
725 'diff2': commit_id_2,
726 })
726 })
727
727
728 response = response.follow()
728 response = response.follow()
729 response.mustcontain(
729 response.mustcontain(
730 # diff contains this
730 # diff contains this
731 "Will print out a useful message on invocation.")
731 "Will print out a useful message on invocation.")
732
732
733 # Note: Expecting that we indicate the user what's being compared
733 # Note: Expecting that we indicate the user what's being compared
734 response.mustcontain("trunk/example.py")
734 response.mustcontain("trunk/example.py")
735 response.mustcontain("tags/v0.2/example.py")
735 response.mustcontain("tags/v0.2/example.py")
736
736
737 def test_show_rev_redirects_to_svn_path(self, backend_svn):
737 def test_show_rev_redirects_to_svn_path(self, backend_svn):
738 #TODO: check this case
738 #TODO: check this case
739 return
739 return
740
740
741 repo = backend_svn['svn-simple-layout'].scm_instance()
741 repo = backend_svn['svn-simple-layout'].scm_instance()
742 commit_id = repo[-1].raw_id
742 commit_id = repo[-1].raw_id
743
743
744 response = self.app.get(
744 response = self.app.get(
745 route_path('repo_files_diff',
745 route_path('repo_files_diff',
746 repo_name=backend_svn.repo_name,
746 repo_name=backend_svn.repo_name,
747 f_path='trunk/example.py'),
747 f_path='trunk/example.py'),
748 params={
748 params={
749 'diff1': 'branches/argparse/example.py@' + commit_id,
749 'diff1': 'branches/argparse/example.py@' + commit_id,
750 'diff2': commit_id,
750 'diff2': commit_id,
751 },
751 },
752 status=302)
752 status=302)
753 response = response.follow()
753 response = response.follow()
754 assert response.headers['Location'].endswith(
754 assert response.headers['Location'].endswith(
755 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
755 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
756
756
757 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
757 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
758 #TODO: check this case
758 #TODO: check this case
759 return
759 return
760
760
761 repo = backend_svn['svn-simple-layout'].scm_instance()
761 repo = backend_svn['svn-simple-layout'].scm_instance()
762 commit_id = repo[-1].raw_id
762 commit_id = repo[-1].raw_id
763 response = self.app.get(
763 response = self.app.get(
764 route_path('repo_files_diff',
764 route_path('repo_files_diff',
765 repo_name=backend_svn.repo_name,
765 repo_name=backend_svn.repo_name,
766 f_path='trunk/example.py'),
766 f_path='trunk/example.py'),
767 params={
767 params={
768 'diff1': 'branches/argparse/example.py@' + commit_id,
768 'diff1': 'branches/argparse/example.py@' + commit_id,
769 'diff2': commit_id,
769 'diff2': commit_id,
770 'show_rev': 'Show at Revision',
770 'show_rev': 'Show at Revision',
771 'annotate': 'true',
771 'annotate': 'true',
772 },
772 },
773 status=302)
773 status=302)
774 response = response.follow()
774 response = response.follow()
775 assert response.headers['Location'].endswith(
775 assert response.headers['Location'].endswith(
776 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
776 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
777
777
778
778
779 @pytest.mark.usefixtures("app", "autologin_user")
779 @pytest.mark.usefixtures("app", "autologin_user")
780 class TestModifyFilesWithWebInterface(object):
780 class TestModifyFilesWithWebInterface(object):
781
781
782 def test_add_file_view(self, backend):
782 def test_add_file_view(self, backend):
783 self.app.get(
783 self.app.get(
784 route_path('repo_files_add_file',
784 route_path('repo_files_add_file',
785 repo_name=backend.repo_name,
785 repo_name=backend.repo_name,
786 commit_id='tip', f_path='/')
786 commit_id='tip', f_path='/')
787 )
787 )
788
788
789 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
789 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
790 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
790 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
791 backend.create_repo()
791 backend.create_repo()
792 filename = 'init.py'
792 filename = 'init.py'
793 response = self.app.post(
793 response = self.app.post(
794 route_path('repo_files_create_file',
794 route_path('repo_files_create_file',
795 repo_name=backend.repo_name,
795 repo_name=backend.repo_name,
796 commit_id='tip', f_path='/'),
796 commit_id='tip', f_path='/'),
797 params={
797 params={
798 'content': "",
798 'content': "",
799 'filename': filename,
799 'filename': filename,
800 'csrf_token': csrf_token,
800 'csrf_token': csrf_token,
801 },
801 },
802 status=302)
802 status=302)
803 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
803 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
804 assert_session_flash(response, expected_msg)
804 assert_session_flash(response, expected_msg)
805
805
806 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
806 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
807 commit_id = backend.repo.get_commit().raw_id
807 commit_id = backend.repo.get_commit().raw_id
808 response = self.app.post(
808 response = self.app.post(
809 route_path('repo_files_create_file',
809 route_path('repo_files_create_file',
810 repo_name=backend.repo_name,
810 repo_name=backend.repo_name,
811 commit_id=commit_id, f_path='/'),
811 commit_id=commit_id, f_path='/'),
812 params={
812 params={
813 'content': "foo",
813 'content': "foo",
814 'csrf_token': csrf_token,
814 'csrf_token': csrf_token,
815 },
815 },
816 status=302)
816 status=302)
817
817
818 assert_session_flash(response, 'No filename specified')
818 assert_session_flash(response, 'No filename specified')
819
819
820 def test_add_file_into_repo_errors_and_no_commits(
820 def test_add_file_into_repo_errors_and_no_commits(
821 self, backend, csrf_token):
821 self, backend, csrf_token):
822 repo = backend.create_repo()
822 repo = backend.create_repo()
823 # Create a file with no filename, it will display an error but
823 # Create a file with no filename, it will display an error but
824 # the repo has no commits yet
824 # the repo has no commits yet
825 response = self.app.post(
825 response = self.app.post(
826 route_path('repo_files_create_file',
826 route_path('repo_files_create_file',
827 repo_name=repo.repo_name,
827 repo_name=repo.repo_name,
828 commit_id='tip', f_path='/'),
828 commit_id='tip', f_path='/'),
829 params={
829 params={
830 'content': "foo",
830 'content': "foo",
831 'csrf_token': csrf_token,
831 'csrf_token': csrf_token,
832 },
832 },
833 status=302)
833 status=302)
834
834
835 assert_session_flash(response, 'No filename specified')
835 assert_session_flash(response, 'No filename specified')
836
836
837 # Not allowed, redirect to the summary
837 # Not allowed, redirect to the summary
838 redirected = response.follow()
838 redirected = response.follow()
839 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
839 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
840
840
841 # As there are no commits, displays the summary page with the error of
841 # As there are no commits, displays the summary page with the error of
842 # creating a file with no filename
842 # creating a file with no filename
843
843
844 assert redirected.request.path == summary_url
844 assert redirected.request.path == summary_url
845
845
846 @pytest.mark.parametrize("filename, clean_filename", [
846 @pytest.mark.parametrize("filename, clean_filename", [
847 ('/abs/foo', 'abs/foo'),
847 ('/abs/foo', 'abs/foo'),
848 ('../rel/foo', 'rel/foo'),
848 ('../rel/foo', 'rel/foo'),
849 ('file/../foo/foo', 'file/foo/foo'),
849 ('file/../foo/foo', 'file/foo/foo'),
850 ])
850 ])
851 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
851 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
852 repo = backend.create_repo()
852 repo = backend.create_repo()
853 commit_id = repo.get_commit().raw_id
853 commit_id = repo.get_commit().raw_id
854
854
855 response = self.app.post(
855 response = self.app.post(
856 route_path('repo_files_create_file',
856 route_path('repo_files_create_file',
857 repo_name=repo.repo_name,
857 repo_name=repo.repo_name,
858 commit_id=commit_id, f_path='/'),
858 commit_id=commit_id, f_path='/'),
859 params={
859 params={
860 'content': "foo",
860 'content': "foo",
861 'filename': filename,
861 'filename': filename,
862 'csrf_token': csrf_token,
862 'csrf_token': csrf_token,
863 },
863 },
864 status=302)
864 status=302)
865
865
866 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
866 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
867 assert_session_flash(response, expected_msg)
867 assert_session_flash(response, expected_msg)
868
868
869 @pytest.mark.parametrize("cnt, filename, content", [
869 @pytest.mark.parametrize("cnt, filename, content", [
870 (1, 'foo.txt', "Content"),
870 (1, 'foo.txt', "Content"),
871 (2, 'dir/foo.rst', "Content"),
871 (2, 'dir/foo.rst', "Content"),
872 (3, 'dir/foo-second.rst', "Content"),
872 (3, 'dir/foo-second.rst', "Content"),
873 (4, 'rel/dir/foo.bar', "Content"),
873 (4, 'rel/dir/foo.bar', "Content"),
874 ])
874 ])
875 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
875 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
876 repo = backend.create_repo()
876 repo = backend.create_repo()
877 commit_id = repo.get_commit().raw_id
877 commit_id = repo.get_commit().raw_id
878 response = self.app.post(
878 response = self.app.post(
879 route_path('repo_files_create_file',
879 route_path('repo_files_create_file',
880 repo_name=repo.repo_name,
880 repo_name=repo.repo_name,
881 commit_id=commit_id, f_path='/'),
881 commit_id=commit_id, f_path='/'),
882 params={
882 params={
883 'content': content,
883 'content': content,
884 'filename': filename,
884 'filename': filename,
885 'csrf_token': csrf_token,
885 'csrf_token': csrf_token,
886 },
886 },
887 status=302)
887 status=302)
888
888
889 expected_msg = 'Successfully committed new file `{}`'.format(filename)
889 expected_msg = 'Successfully committed new file `{}`'.format(filename)
890 assert_session_flash(response, expected_msg)
890 assert_session_flash(response, expected_msg)
891
891
892 def test_edit_file_view(self, backend):
892 def test_edit_file_view(self, backend):
893 response = self.app.get(
893 response = self.app.get(
894 route_path('repo_files_edit_file',
894 route_path('repo_files_edit_file',
895 repo_name=backend.repo_name,
895 repo_name=backend.repo_name,
896 commit_id=backend.default_head_id,
896 commit_id=backend.default_head_id,
897 f_path='vcs/nodes.py'),
897 f_path='vcs/nodes.py'),
898 status=200)
898 status=200)
899 response.mustcontain("Module holding everything related to vcs nodes.")
899 response.mustcontain("Module holding everything related to vcs nodes.")
900
900
901 def test_edit_file_view_not_on_branch(self, backend):
901 def test_edit_file_view_not_on_branch(self, backend):
902 repo = backend.create_repo()
902 repo = backend.create_repo()
903 backend.ensure_file("vcs/nodes.py")
903 backend.ensure_file("vcs/nodes.py")
904
904
905 response = self.app.get(
905 response = self.app.get(
906 route_path('repo_files_edit_file',
906 route_path('repo_files_edit_file',
907 repo_name=repo.repo_name,
907 repo_name=repo.repo_name,
908 commit_id='tip',
908 commit_id='tip',
909 f_path='vcs/nodes.py'),
909 f_path='vcs/nodes.py'),
910 status=302)
910 status=302)
911 assert_session_flash(
911 assert_session_flash(
912 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
912 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
913
913
914 def test_edit_file_view_commit_changes(self, backend, csrf_token):
914 def test_edit_file_view_commit_changes(self, backend, csrf_token):
915 repo = backend.create_repo()
915 repo = backend.create_repo()
916 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
916 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
917
917
918 response = self.app.post(
918 response = self.app.post(
919 route_path('repo_files_update_file',
919 route_path('repo_files_update_file',
920 repo_name=repo.repo_name,
920 repo_name=repo.repo_name,
921 commit_id=backend.default_head_id,
921 commit_id=backend.default_head_id,
922 f_path='vcs/nodes.py'),
922 f_path='vcs/nodes.py'),
923 params={
923 params={
924 'content': "print 'hello world'",
924 'content': "print 'hello world'",
925 'message': 'I committed',
925 'message': 'I committed',
926 'filename': "vcs/nodes.py",
926 'filename': "vcs/nodes.py",
927 'csrf_token': csrf_token,
927 'csrf_token': csrf_token,
928 },
928 },
929 status=302)
929 status=302)
930 assert_session_flash(
930 assert_session_flash(
931 response, 'Successfully committed changes to file `vcs/nodes.py`')
931 response, 'Successfully committed changes to file `vcs/nodes.py`')
932 tip = repo.get_commit(commit_idx=-1)
932 tip = repo.get_commit(commit_idx=-1)
933 assert tip.message == 'I committed'
933 assert tip.message == 'I committed'
934
934
935 def test_edit_file_view_commit_changes_default_message(self, backend,
935 def test_edit_file_view_commit_changes_default_message(self, backend,
936 csrf_token):
936 csrf_token):
937 repo = backend.create_repo()
937 repo = backend.create_repo()
938 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
938 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
939
939
940 commit_id = (
940 commit_id = (
941 backend.default_branch_name or
941 backend.default_branch_name or
942 backend.repo.scm_instance().commit_ids[-1])
942 backend.repo.scm_instance().commit_ids[-1])
943
943
944 response = self.app.post(
944 response = self.app.post(
945 route_path('repo_files_update_file',
945 route_path('repo_files_update_file',
946 repo_name=repo.repo_name,
946 repo_name=repo.repo_name,
947 commit_id=commit_id,
947 commit_id=commit_id,
948 f_path='vcs/nodes.py'),
948 f_path='vcs/nodes.py'),
949 params={
949 params={
950 'content': "print 'hello world'",
950 'content': "print 'hello world'",
951 'message': '',
951 'message': '',
952 'filename': "vcs/nodes.py",
952 'filename': "vcs/nodes.py",
953 'csrf_token': csrf_token,
953 'csrf_token': csrf_token,
954 },
954 },
955 status=302)
955 status=302)
956 assert_session_flash(
956 assert_session_flash(
957 response, 'Successfully committed changes to file `vcs/nodes.py`')
957 response, 'Successfully committed changes to file `vcs/nodes.py`')
958 tip = repo.get_commit(commit_idx=-1)
958 tip = repo.get_commit(commit_idx=-1)
959 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
959 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
960
960
961 def test_delete_file_view(self, backend):
961 def test_delete_file_view(self, backend):
962 self.app.get(
962 self.app.get(
963 route_path('repo_files_remove_file',
963 route_path('repo_files_remove_file',
964 repo_name=backend.repo_name,
964 repo_name=backend.repo_name,
965 commit_id=backend.default_head_id,
965 commit_id=backend.default_head_id,
966 f_path='vcs/nodes.py'),
966 f_path='vcs/nodes.py'),
967 status=200)
967 status=200)
968
968
969 def test_delete_file_view_not_on_branch(self, backend):
969 def test_delete_file_view_not_on_branch(self, backend):
970 repo = backend.create_repo()
970 repo = backend.create_repo()
971 backend.ensure_file('vcs/nodes.py')
971 backend.ensure_file('vcs/nodes.py')
972
972
973 response = self.app.get(
973 response = self.app.get(
974 route_path('repo_files_remove_file',
974 route_path('repo_files_remove_file',
975 repo_name=repo.repo_name,
975 repo_name=repo.repo_name,
976 commit_id='tip',
976 commit_id='tip',
977 f_path='vcs/nodes.py'),
977 f_path='vcs/nodes.py'),
978 status=302)
978 status=302)
979 assert_session_flash(
979 assert_session_flash(
980 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
980 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
981
981
982 def test_delete_file_view_commit_changes(self, backend, csrf_token):
982 def test_delete_file_view_commit_changes(self, backend, csrf_token):
983 repo = backend.create_repo()
983 repo = backend.create_repo()
984 backend.ensure_file("vcs/nodes.py")
984 backend.ensure_file("vcs/nodes.py")
985
985
986 response = self.app.post(
986 response = self.app.post(
987 route_path('repo_files_delete_file',
987 route_path('repo_files_delete_file',
988 repo_name=repo.repo_name,
988 repo_name=repo.repo_name,
989 commit_id=backend.default_head_id,
989 commit_id=backend.default_head_id,
990 f_path='vcs/nodes.py'),
990 f_path='vcs/nodes.py'),
991 params={
991 params={
992 'message': 'i committed',
992 'message': 'i committed',
993 'csrf_token': csrf_token,
993 'csrf_token': csrf_token,
994 },
994 },
995 status=302)
995 status=302)
996 assert_session_flash(
996 assert_session_flash(
997 response, 'Successfully deleted file `vcs/nodes.py`')
997 response, 'Successfully deleted file `vcs/nodes.py`')
998
998
999
999
1000 @pytest.mark.usefixtures("app")
1000 @pytest.mark.usefixtures("app")
1001 class TestFilesViewOtherCases(object):
1001 class TestFilesViewOtherCases(object):
1002
1002
1003 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
1003 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
1004 self, backend_stub, autologin_regular_user, user_regular,
1004 self, backend_stub, autologin_regular_user, user_regular,
1005 user_util):
1005 user_util):
1006
1006
1007 repo = backend_stub.create_repo()
1007 repo = backend_stub.create_repo()
1008 user_util.grant_user_permission_to_repo(
1008 user_util.grant_user_permission_to_repo(
1009 repo, user_regular, 'repository.write')
1009 repo, user_regular, 'repository.write')
1010 response = self.app.get(
1010 response = self.app.get(
1011 route_path('repo_files',
1011 route_path('repo_files',
1012 repo_name=repo.repo_name,
1012 repo_name=repo.repo_name,
1013 commit_id='tip', f_path='/'))
1013 commit_id='tip', f_path='/'))
1014
1014
1015 repo_file_add_url = route_path(
1015 repo_file_add_url = route_path(
1016 'repo_files_add_file',
1016 'repo_files_add_file',
1017 repo_name=repo.repo_name,
1017 repo_name=repo.repo_name,
1018 commit_id=0, f_path='')
1018 commit_id=0, f_path='')
1019
1019
1020 assert_session_flash(
1020 assert_session_flash(
1021 response,
1021 response,
1022 'There are no files yet. <a class="alert-link" '
1022 'There are no files yet. <a class="alert-link" '
1023 'href="{}">Click here to add a new file.</a>'
1023 'href="{}">Click here to add a new file.</a>'
1024 .format(repo_file_add_url))
1024 .format(repo_file_add_url))
1025
1025
1026 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1026 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1027 self, backend_stub, autologin_regular_user):
1027 self, backend_stub, autologin_regular_user):
1028 repo = backend_stub.create_repo()
1028 repo = backend_stub.create_repo()
1029 # init session for anon user
1029 # init session for anon user
1030 route_path('repo_summary', repo_name=repo.repo_name)
1030 route_path('repo_summary', repo_name=repo.repo_name)
1031
1031
1032 repo_file_add_url = route_path(
1032 repo_file_add_url = route_path(
1033 'repo_files_add_file',
1033 'repo_files_add_file',
1034 repo_name=repo.repo_name,
1034 repo_name=repo.repo_name,
1035 commit_id=0, f_path='')
1035 commit_id=0, f_path='')
1036
1036
1037 response = self.app.get(
1037 response = self.app.get(
1038 route_path('repo_files',
1038 route_path('repo_files',
1039 repo_name=repo.repo_name,
1039 repo_name=repo.repo_name,
1040 commit_id='tip', f_path='/'))
1040 commit_id='tip', f_path='/'))
1041
1041
1042 assert_session_flash(response, no_=repo_file_add_url)
1042 assert_session_flash(response, no_=repo_file_add_url)
1043
1043
1044 @pytest.mark.parametrize('file_node', [
1044 @pytest.mark.parametrize('file_node', [
1045 'archive/file.zip',
1045 'archive/file.zip',
1046 'diff/my-file.txt',
1046 'diff/my-file.txt',
1047 'render.py',
1047 'render.py',
1048 'render',
1048 'render',
1049 'remove_file',
1049 'remove_file',
1050 'remove_file/to-delete.txt',
1050 'remove_file/to-delete.txt',
1051 ])
1051 ])
1052 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1052 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1053 backend.create_repo()
1053 backend.create_repo()
1054 backend.ensure_file(file_node)
1054 backend.ensure_file(file_node)
1055
1055
1056 self.app.get(
1056 self.app.get(
1057 route_path('repo_files',
1057 route_path('repo_files',
1058 repo_name=backend.repo_name,
1058 repo_name=backend.repo_name,
1059 commit_id='tip', f_path=file_node),
1059 commit_id='tip', f_path=file_node),
1060 status=200)
1060 status=200)
1061
1061
1062
1062
1063 class TestAdjustFilePathForSvn(object):
1063 class TestAdjustFilePathForSvn(object):
1064 """
1064 """
1065 SVN specific adjustments of node history in RepoFilesView.
1065 SVN specific adjustments of node history in RepoFilesView.
1066 """
1066 """
1067
1067
1068 def test_returns_path_relative_to_matched_reference(self):
1068 def test_returns_path_relative_to_matched_reference(self):
1069 repo = self._repo(branches=['trunk'])
1069 repo = self._repo(branches=['trunk'])
1070 self.assert_file_adjustment('trunk/file', 'file', repo)
1070 self.assert_file_adjustment('trunk/file', 'file', repo)
1071
1071
1072 def test_does_not_modify_file_if_no_reference_matches(self):
1072 def test_does_not_modify_file_if_no_reference_matches(self):
1073 repo = self._repo(branches=['trunk'])
1073 repo = self._repo(branches=['trunk'])
1074 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1074 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1075
1075
1076 def test_does_not_adjust_partial_directory_names(self):
1076 def test_does_not_adjust_partial_directory_names(self):
1077 repo = self._repo(branches=['trun'])
1077 repo = self._repo(branches=['trun'])
1078 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1078 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1079
1079
1080 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1080 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1081 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1081 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1082 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1082 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1083
1083
1084 def assert_file_adjustment(self, f_path, expected, repo):
1084 def assert_file_adjustment(self, f_path, expected, repo):
1085 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1085 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1086 assert result == expected
1086 assert result == expected
1087
1087
1088 def _repo(self, branches=None):
1088 def _repo(self, branches=None):
1089 repo = mock.Mock()
1089 repo = mock.Mock()
1090 repo.branches = OrderedDict((name, '0') for name in branches or [])
1090 repo.branches = OrderedDict((name, '0') for name in branches or [])
1091 repo.tags = {}
1091 repo.tags = {}
1092 return repo
1092 return repo
@@ -1,333 +1,333 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import TestController, assert_session_flash, HG_FORK, GIT_FORK
23 from rhodecode.tests import TestController, assert_session_flash, HG_FORK, GIT_FORK
24
24
25 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27
27
28 from rhodecode.model.db import Repository
28 from rhodecode.model.db import Repository
29 from rhodecode.model.repo import RepoModel
29 from rhodecode.model.repo import RepoModel
30 from rhodecode.model.user import UserModel
30 from rhodecode.model.user import UserModel
31 from rhodecode.model.meta import Session
31 from rhodecode.model.meta import Session
32
32
33 fixture = Fixture()
33 fixture = Fixture()
34
34
35
35
36 def route_path(name, params=None, **kwargs):
36 def route_path(name, params=None, **kwargs):
37 import urllib
37 import urllib.request, urllib.parse, urllib.error
38
38
39 base_url = {
39 base_url = {
40 'repo_summary': '/{repo_name}',
40 'repo_summary': '/{repo_name}',
41 'repo_creating_check': '/{repo_name}/repo_creating_check',
41 'repo_creating_check': '/{repo_name}/repo_creating_check',
42 'repo_fork_new': '/{repo_name}/fork',
42 'repo_fork_new': '/{repo_name}/fork',
43 'repo_fork_create': '/{repo_name}/fork/create',
43 'repo_fork_create': '/{repo_name}/fork/create',
44 'repo_forks_show_all': '/{repo_name}/forks',
44 'repo_forks_show_all': '/{repo_name}/forks',
45 'repo_forks_data': '/{repo_name}/forks/data',
45 'repo_forks_data': '/{repo_name}/forks/data',
46 }[name].format(**kwargs)
46 }[name].format(**kwargs)
47
47
48 if params:
48 if params:
49 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
49 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
50 return base_url
50 return base_url
51
51
52
52
53 FORK_NAME = {
53 FORK_NAME = {
54 'hg': HG_FORK,
54 'hg': HG_FORK,
55 'git': GIT_FORK
55 'git': GIT_FORK
56 }
56 }
57
57
58
58
59 @pytest.mark.skip_backends('svn')
59 @pytest.mark.skip_backends('svn')
60 class TestRepoForkViewTests(TestController):
60 class TestRepoForkViewTests(TestController):
61
61
62 def test_show_forks(self, backend, xhr_header):
62 def test_show_forks(self, backend, xhr_header):
63 self.log_user()
63 self.log_user()
64 response = self.app.get(
64 response = self.app.get(
65 route_path('repo_forks_data', repo_name=backend.repo_name),
65 route_path('repo_forks_data', repo_name=backend.repo_name),
66 extra_environ=xhr_header)
66 extra_environ=xhr_header)
67
67
68 assert response.json == {u'data': [], u'draw': None,
68 assert response.json == {u'data': [], u'draw': None,
69 u'recordsFiltered': 0, u'recordsTotal': 0}
69 u'recordsFiltered': 0, u'recordsTotal': 0}
70
70
71 def test_no_permissions_to_fork_page(self, backend, user_util):
71 def test_no_permissions_to_fork_page(self, backend, user_util):
72 user = user_util.create_user(password='qweqwe')
72 user = user_util.create_user(password='qweqwe')
73 user_id = user.user_id
73 user_id = user.user_id
74 self.log_user(user.username, 'qweqwe')
74 self.log_user(user.username, 'qweqwe')
75
75
76 user_model = UserModel()
76 user_model = UserModel()
77 user_model.revoke_perm(user_id, 'hg.fork.repository')
77 user_model.revoke_perm(user_id, 'hg.fork.repository')
78 user_model.grant_perm(user_id, 'hg.fork.none')
78 user_model.grant_perm(user_id, 'hg.fork.none')
79 u = UserModel().get(user_id)
79 u = UserModel().get(user_id)
80 u.inherit_default_permissions = False
80 u.inherit_default_permissions = False
81 Session().commit()
81 Session().commit()
82 # try create a fork
82 # try create a fork
83 self.app.get(
83 self.app.get(
84 route_path('repo_fork_new', repo_name=backend.repo_name),
84 route_path('repo_fork_new', repo_name=backend.repo_name),
85 status=404)
85 status=404)
86
86
87 def test_no_permissions_to_fork_submit(self, backend, csrf_token, user_util):
87 def test_no_permissions_to_fork_submit(self, backend, csrf_token, user_util):
88 user = user_util.create_user(password='qweqwe')
88 user = user_util.create_user(password='qweqwe')
89 user_id = user.user_id
89 user_id = user.user_id
90 self.log_user(user.username, 'qweqwe')
90 self.log_user(user.username, 'qweqwe')
91
91
92 user_model = UserModel()
92 user_model = UserModel()
93 user_model.revoke_perm(user_id, 'hg.fork.repository')
93 user_model.revoke_perm(user_id, 'hg.fork.repository')
94 user_model.grant_perm(user_id, 'hg.fork.none')
94 user_model.grant_perm(user_id, 'hg.fork.none')
95 u = UserModel().get(user_id)
95 u = UserModel().get(user_id)
96 u.inherit_default_permissions = False
96 u.inherit_default_permissions = False
97 Session().commit()
97 Session().commit()
98 # try create a fork
98 # try create a fork
99 self.app.post(
99 self.app.post(
100 route_path('repo_fork_create', repo_name=backend.repo_name),
100 route_path('repo_fork_create', repo_name=backend.repo_name),
101 {'csrf_token': csrf_token},
101 {'csrf_token': csrf_token},
102 status=404)
102 status=404)
103
103
104 def test_fork_missing_data(self, autologin_user, backend, csrf_token):
104 def test_fork_missing_data(self, autologin_user, backend, csrf_token):
105 # try create a fork
105 # try create a fork
106 response = self.app.post(
106 response = self.app.post(
107 route_path('repo_fork_create', repo_name=backend.repo_name),
107 route_path('repo_fork_create', repo_name=backend.repo_name),
108 {'csrf_token': csrf_token},
108 {'csrf_token': csrf_token},
109 status=200)
109 status=200)
110 # test if html fill works fine
110 # test if html fill works fine
111 response.mustcontain('Missing value')
111 response.mustcontain('Missing value')
112
112
113 def test_create_fork_page(self, autologin_user, backend):
113 def test_create_fork_page(self, autologin_user, backend):
114 self.app.get(
114 self.app.get(
115 route_path('repo_fork_new', repo_name=backend.repo_name),
115 route_path('repo_fork_new', repo_name=backend.repo_name),
116 status=200)
116 status=200)
117
117
118 def test_create_and_show_fork(
118 def test_create_and_show_fork(
119 self, autologin_user, backend, csrf_token, xhr_header):
119 self, autologin_user, backend, csrf_token, xhr_header):
120
120
121 # create a fork
121 # create a fork
122 fork_name = FORK_NAME[backend.alias]
122 fork_name = FORK_NAME[backend.alias]
123 description = 'fork of vcs test'
123 description = 'fork of vcs test'
124 repo_name = backend.repo_name
124 repo_name = backend.repo_name
125 source_repo = Repository.get_by_repo_name(repo_name)
125 source_repo = Repository.get_by_repo_name(repo_name)
126 creation_args = {
126 creation_args = {
127 'repo_name': fork_name,
127 'repo_name': fork_name,
128 'repo_group': '',
128 'repo_group': '',
129 'fork_parent_id': source_repo.repo_id,
129 'fork_parent_id': source_repo.repo_id,
130 'repo_type': backend.alias,
130 'repo_type': backend.alias,
131 'description': description,
131 'description': description,
132 'private': 'False',
132 'private': 'False',
133 'csrf_token': csrf_token,
133 'csrf_token': csrf_token,
134 }
134 }
135
135
136 self.app.post(
136 self.app.post(
137 route_path('repo_fork_create', repo_name=repo_name), creation_args)
137 route_path('repo_fork_create', repo_name=repo_name), creation_args)
138
138
139 response = self.app.get(
139 response = self.app.get(
140 route_path('repo_forks_data', repo_name=repo_name),
140 route_path('repo_forks_data', repo_name=repo_name),
141 extra_environ=xhr_header)
141 extra_environ=xhr_header)
142
142
143 assert response.json['data'][0]['fork_name'] == \
143 assert response.json['data'][0]['fork_name'] == \
144 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
144 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
145
145
146 # remove this fork
146 # remove this fork
147 fixture.destroy_repo(fork_name)
147 fixture.destroy_repo(fork_name)
148
148
149 def test_fork_create(self, autologin_user, backend, csrf_token):
149 def test_fork_create(self, autologin_user, backend, csrf_token):
150 fork_name = FORK_NAME[backend.alias]
150 fork_name = FORK_NAME[backend.alias]
151 description = 'fork of vcs test'
151 description = 'fork of vcs test'
152 repo_name = backend.repo_name
152 repo_name = backend.repo_name
153 source_repo = Repository.get_by_repo_name(repo_name)
153 source_repo = Repository.get_by_repo_name(repo_name)
154 creation_args = {
154 creation_args = {
155 'repo_name': fork_name,
155 'repo_name': fork_name,
156 'repo_group': '',
156 'repo_group': '',
157 'fork_parent_id': source_repo.repo_id,
157 'fork_parent_id': source_repo.repo_id,
158 'repo_type': backend.alias,
158 'repo_type': backend.alias,
159 'description': description,
159 'description': description,
160 'private': 'False',
160 'private': 'False',
161 'csrf_token': csrf_token,
161 'csrf_token': csrf_token,
162 }
162 }
163 self.app.post(
163 self.app.post(
164 route_path('repo_fork_create', repo_name=repo_name), creation_args)
164 route_path('repo_fork_create', repo_name=repo_name), creation_args)
165 repo = Repository.get_by_repo_name(FORK_NAME[backend.alias])
165 repo = Repository.get_by_repo_name(FORK_NAME[backend.alias])
166 assert repo.fork.repo_name == backend.repo_name
166 assert repo.fork.repo_name == backend.repo_name
167
167
168 # run the check page that triggers the flash message
168 # run the check page that triggers the flash message
169 response = self.app.get(
169 response = self.app.get(
170 route_path('repo_creating_check', repo_name=fork_name))
170 route_path('repo_creating_check', repo_name=fork_name))
171 # test if we have a message that fork is ok
171 # test if we have a message that fork is ok
172 assert_session_flash(response,
172 assert_session_flash(response,
173 'Forked repository %s as <a href="/%s">%s</a>' % (
173 'Forked repository %s as <a href="/%s">%s</a>' % (
174 repo_name, fork_name, fork_name))
174 repo_name, fork_name, fork_name))
175
175
176 # test if the fork was created in the database
176 # test if the fork was created in the database
177 fork_repo = Session().query(Repository)\
177 fork_repo = Session().query(Repository)\
178 .filter(Repository.repo_name == fork_name).one()
178 .filter(Repository.repo_name == fork_name).one()
179
179
180 assert fork_repo.repo_name == fork_name
180 assert fork_repo.repo_name == fork_name
181 assert fork_repo.fork.repo_name == repo_name
181 assert fork_repo.fork.repo_name == repo_name
182
182
183 # test if the repository is visible in the list ?
183 # test if the repository is visible in the list ?
184 response = self.app.get(
184 response = self.app.get(
185 h.route_path('repo_summary', repo_name=fork_name))
185 h.route_path('repo_summary', repo_name=fork_name))
186 response.mustcontain(fork_name)
186 response.mustcontain(fork_name)
187 response.mustcontain(backend.alias)
187 response.mustcontain(backend.alias)
188 response.mustcontain('Fork of')
188 response.mustcontain('Fork of')
189 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
189 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
190
190
191 def test_fork_create_into_group(self, autologin_user, backend, csrf_token):
191 def test_fork_create_into_group(self, autologin_user, backend, csrf_token):
192 group = fixture.create_repo_group('vc')
192 group = fixture.create_repo_group('vc')
193 group_id = group.group_id
193 group_id = group.group_id
194 fork_name = FORK_NAME[backend.alias]
194 fork_name = FORK_NAME[backend.alias]
195 fork_name_full = 'vc/%s' % fork_name
195 fork_name_full = 'vc/%s' % fork_name
196 description = 'fork of vcs test'
196 description = 'fork of vcs test'
197 repo_name = backend.repo_name
197 repo_name = backend.repo_name
198 source_repo = Repository.get_by_repo_name(repo_name)
198 source_repo = Repository.get_by_repo_name(repo_name)
199 creation_args = {
199 creation_args = {
200 'repo_name': fork_name,
200 'repo_name': fork_name,
201 'repo_group': group_id,
201 'repo_group': group_id,
202 'fork_parent_id': source_repo.repo_id,
202 'fork_parent_id': source_repo.repo_id,
203 'repo_type': backend.alias,
203 'repo_type': backend.alias,
204 'description': description,
204 'description': description,
205 'private': 'False',
205 'private': 'False',
206 'csrf_token': csrf_token,
206 'csrf_token': csrf_token,
207 }
207 }
208 self.app.post(
208 self.app.post(
209 route_path('repo_fork_create', repo_name=repo_name), creation_args)
209 route_path('repo_fork_create', repo_name=repo_name), creation_args)
210 repo = Repository.get_by_repo_name(fork_name_full)
210 repo = Repository.get_by_repo_name(fork_name_full)
211 assert repo.fork.repo_name == backend.repo_name
211 assert repo.fork.repo_name == backend.repo_name
212
212
213 # run the check page that triggers the flash message
213 # run the check page that triggers the flash message
214 response = self.app.get(
214 response = self.app.get(
215 route_path('repo_creating_check', repo_name=fork_name_full))
215 route_path('repo_creating_check', repo_name=fork_name_full))
216 # test if we have a message that fork is ok
216 # test if we have a message that fork is ok
217 assert_session_flash(response,
217 assert_session_flash(response,
218 'Forked repository %s as <a href="/%s">%s</a>' % (
218 'Forked repository %s as <a href="/%s">%s</a>' % (
219 repo_name, fork_name_full, fork_name_full))
219 repo_name, fork_name_full, fork_name_full))
220
220
221 # test if the fork was created in the database
221 # test if the fork was created in the database
222 fork_repo = Session().query(Repository)\
222 fork_repo = Session().query(Repository)\
223 .filter(Repository.repo_name == fork_name_full).one()
223 .filter(Repository.repo_name == fork_name_full).one()
224
224
225 assert fork_repo.repo_name == fork_name_full
225 assert fork_repo.repo_name == fork_name_full
226 assert fork_repo.fork.repo_name == repo_name
226 assert fork_repo.fork.repo_name == repo_name
227
227
228 # test if the repository is visible in the list ?
228 # test if the repository is visible in the list ?
229 response = self.app.get(
229 response = self.app.get(
230 h.route_path('repo_summary', repo_name=fork_name_full))
230 h.route_path('repo_summary', repo_name=fork_name_full))
231 response.mustcontain(fork_name_full)
231 response.mustcontain(fork_name_full)
232 response.mustcontain(backend.alias)
232 response.mustcontain(backend.alias)
233
233
234 response.mustcontain('Fork of')
234 response.mustcontain('Fork of')
235 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
235 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
236
236
237 fixture.destroy_repo(fork_name_full)
237 fixture.destroy_repo(fork_name_full)
238 fixture.destroy_repo_group(group_id)
238 fixture.destroy_repo_group(group_id)
239
239
240 def test_fork_read_permission(self, backend, xhr_header, user_util):
240 def test_fork_read_permission(self, backend, xhr_header, user_util):
241 user = user_util.create_user(password='qweqwe')
241 user = user_util.create_user(password='qweqwe')
242 user_id = user.user_id
242 user_id = user.user_id
243 self.log_user(user.username, 'qweqwe')
243 self.log_user(user.username, 'qweqwe')
244
244
245 # create a fake fork
245 # create a fake fork
246 fork = user_util.create_repo(repo_type=backend.alias)
246 fork = user_util.create_repo(repo_type=backend.alias)
247 source = user_util.create_repo(repo_type=backend.alias)
247 source = user_util.create_repo(repo_type=backend.alias)
248 repo_name = source.repo_name
248 repo_name = source.repo_name
249
249
250 fork.fork_id = source.repo_id
250 fork.fork_id = source.repo_id
251 fork_name = fork.repo_name
251 fork_name = fork.repo_name
252 Session().commit()
252 Session().commit()
253
253
254 forks = Repository.query()\
254 forks = Repository.query()\
255 .filter(Repository.repo_type == backend.alias)\
255 .filter(Repository.repo_type == backend.alias)\
256 .filter(Repository.fork_id == source.repo_id).all()
256 .filter(Repository.fork_id == source.repo_id).all()
257 assert 1 == len(forks)
257 assert 1 == len(forks)
258
258
259 # set read permissions for this
259 # set read permissions for this
260 RepoModel().grant_user_permission(
260 RepoModel().grant_user_permission(
261 repo=forks[0], user=user_id, perm='repository.read')
261 repo=forks[0], user=user_id, perm='repository.read')
262 Session().commit()
262 Session().commit()
263
263
264 response = self.app.get(
264 response = self.app.get(
265 route_path('repo_forks_data', repo_name=repo_name),
265 route_path('repo_forks_data', repo_name=repo_name),
266 extra_environ=xhr_header)
266 extra_environ=xhr_header)
267
267
268 assert response.json['data'][0]['fork_name'] == \
268 assert response.json['data'][0]['fork_name'] == \
269 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
269 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
270
270
271 def test_fork_none_permission(self, backend, xhr_header, user_util):
271 def test_fork_none_permission(self, backend, xhr_header, user_util):
272 user = user_util.create_user(password='qweqwe')
272 user = user_util.create_user(password='qweqwe')
273 user_id = user.user_id
273 user_id = user.user_id
274 self.log_user(user.username, 'qweqwe')
274 self.log_user(user.username, 'qweqwe')
275
275
276 # create a fake fork
276 # create a fake fork
277 fork = user_util.create_repo(repo_type=backend.alias)
277 fork = user_util.create_repo(repo_type=backend.alias)
278 source = user_util.create_repo(repo_type=backend.alias)
278 source = user_util.create_repo(repo_type=backend.alias)
279 repo_name = source.repo_name
279 repo_name = source.repo_name
280
280
281 fork.fork_id = source.repo_id
281 fork.fork_id = source.repo_id
282
282
283 Session().commit()
283 Session().commit()
284
284
285 forks = Repository.query()\
285 forks = Repository.query()\
286 .filter(Repository.repo_type == backend.alias)\
286 .filter(Repository.repo_type == backend.alias)\
287 .filter(Repository.fork_id == source.repo_id).all()
287 .filter(Repository.fork_id == source.repo_id).all()
288 assert 1 == len(forks)
288 assert 1 == len(forks)
289
289
290 # set none
290 # set none
291 RepoModel().grant_user_permission(
291 RepoModel().grant_user_permission(
292 repo=forks[0], user=user_id, perm='repository.none')
292 repo=forks[0], user=user_id, perm='repository.none')
293 Session().commit()
293 Session().commit()
294
294
295 # fork shouldn't be there
295 # fork shouldn't be there
296 response = self.app.get(
296 response = self.app.get(
297 route_path('repo_forks_data', repo_name=repo_name),
297 route_path('repo_forks_data', repo_name=repo_name),
298 extra_environ=xhr_header)
298 extra_environ=xhr_header)
299
299
300 assert response.json == {u'data': [], u'draw': None,
300 assert response.json == {u'data': [], u'draw': None,
301 u'recordsFiltered': 0, u'recordsTotal': 0}
301 u'recordsFiltered': 0, u'recordsTotal': 0}
302
302
303 @pytest.mark.parametrize('url_type', [
303 @pytest.mark.parametrize('url_type', [
304 'repo_fork_new',
304 'repo_fork_new',
305 'repo_fork_create'
305 'repo_fork_create'
306 ])
306 ])
307 def test_fork_is_forbidden_on_archived_repo(self, backend, xhr_header, user_util, url_type):
307 def test_fork_is_forbidden_on_archived_repo(self, backend, xhr_header, user_util, url_type):
308 user = user_util.create_user(password='qweqwe')
308 user = user_util.create_user(password='qweqwe')
309 self.log_user(user.username, 'qweqwe')
309 self.log_user(user.username, 'qweqwe')
310
310
311 # create a temporary repo
311 # create a temporary repo
312 source = user_util.create_repo(repo_type=backend.alias)
312 source = user_util.create_repo(repo_type=backend.alias)
313 repo_name = source.repo_name
313 repo_name = source.repo_name
314 repo = Repository.get_by_repo_name(repo_name)
314 repo = Repository.get_by_repo_name(repo_name)
315 repo.archived = True
315 repo.archived = True
316 Session().commit()
316 Session().commit()
317
317
318 response = self.app.get(
318 response = self.app.get(
319 route_path(url_type, repo_name=repo_name), status=302)
319 route_path(url_type, repo_name=repo_name), status=302)
320
320
321 msg = 'Action not supported for archived repository.'
321 msg = 'Action not supported for archived repository.'
322 assert_session_flash(response, msg)
322 assert_session_flash(response, msg)
323
323
324
324
325 class TestSVNFork(TestController):
325 class TestSVNFork(TestController):
326 @pytest.mark.parametrize('route_name', [
326 @pytest.mark.parametrize('route_name', [
327 'repo_fork_create', 'repo_fork_new'
327 'repo_fork_create', 'repo_fork_new'
328 ])
328 ])
329 def test_fork_redirects(self, autologin_user, backend_svn, route_name):
329 def test_fork_redirects(self, autologin_user, backend_svn, route_name):
330
330
331 self.app.get(route_path(
331 self.app.get(route_path(
332 route_name, repo_name=backend_svn.repo_name),
332 route_name, repo_name=backend_svn.repo_name),
333 status=404)
333 status=404)
@@ -1,149 +1,149 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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.lib.utils2 import md5
23 from rhodecode.lib.utils2 import md5
24 from rhodecode.model.db import Repository
24 from rhodecode.model.db import Repository
25 from rhodecode.model.meta import Session
25 from rhodecode.model.meta import Session
26 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
26 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
27
27
28
28
29 def route_path(name, params=None, **kwargs):
29 def route_path(name, params=None, **kwargs):
30 import urllib
30 import urllib.request, urllib.parse, urllib.error
31
31
32 base_url = {
32 base_url = {
33 'repo_summary': '/{repo_name}',
33 'repo_summary': '/{repo_name}',
34 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
34 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
35 'edit_repo_issuetracker_test': '/{repo_name}/settings/issue_trackers/test',
35 'edit_repo_issuetracker_test': '/{repo_name}/settings/issue_trackers/test',
36 'edit_repo_issuetracker_delete': '/{repo_name}/settings/issue_trackers/delete',
36 'edit_repo_issuetracker_delete': '/{repo_name}/settings/issue_trackers/delete',
37 'edit_repo_issuetracker_update': '/{repo_name}/settings/issue_trackers/update',
37 'edit_repo_issuetracker_update': '/{repo_name}/settings/issue_trackers/update',
38 }[name].format(**kwargs)
38 }[name].format(**kwargs)
39
39
40 if params:
40 if params:
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 return base_url
42 return base_url
43
43
44
44
45 @pytest.mark.usefixtures("app")
45 @pytest.mark.usefixtures("app")
46 class TestRepoIssueTracker(object):
46 class TestRepoIssueTracker(object):
47 def test_issuetracker_index(self, autologin_user, backend):
47 def test_issuetracker_index(self, autologin_user, backend):
48 repo = backend.create_repo()
48 repo = backend.create_repo()
49 response = self.app.get(route_path('edit_repo_issuetracker',
49 response = self.app.get(route_path('edit_repo_issuetracker',
50 repo_name=repo.repo_name))
50 repo_name=repo.repo_name))
51 assert response.status_code == 200
51 assert response.status_code == 200
52
52
53 def test_add_and_test_issuetracker_patterns(
53 def test_add_and_test_issuetracker_patterns(
54 self, autologin_user, backend, csrf_token, request, xhr_header):
54 self, autologin_user, backend, csrf_token, request, xhr_header):
55 pattern = 'issuetracker_pat'
55 pattern = 'issuetracker_pat'
56 another_pattern = pattern+'1'
56 another_pattern = pattern+'1'
57 post_url = route_path(
57 post_url = route_path(
58 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
58 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
59 post_data = {
59 post_data = {
60 'new_pattern_pattern_0': pattern,
60 'new_pattern_pattern_0': pattern,
61 'new_pattern_url_0': 'http://url',
61 'new_pattern_url_0': 'http://url',
62 'new_pattern_prefix_0': 'prefix',
62 'new_pattern_prefix_0': 'prefix',
63 'new_pattern_description_0': 'description',
63 'new_pattern_description_0': 'description',
64 'new_pattern_pattern_1': another_pattern,
64 'new_pattern_pattern_1': another_pattern,
65 'new_pattern_url_1': '/url1',
65 'new_pattern_url_1': '/url1',
66 'new_pattern_prefix_1': 'prefix1',
66 'new_pattern_prefix_1': 'prefix1',
67 'new_pattern_description_1': 'description1',
67 'new_pattern_description_1': 'description1',
68 'csrf_token': csrf_token
68 'csrf_token': csrf_token
69 }
69 }
70 self.app.post(post_url, post_data, status=302)
70 self.app.post(post_url, post_data, status=302)
71 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
71 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
72 settings = self.settings_model.get_repo_settings()
72 settings = self.settings_model.get_repo_settings()
73 self.uid = md5(pattern)
73 self.uid = md5(pattern)
74 assert settings[self.uid]['pat'] == pattern
74 assert settings[self.uid]['pat'] == pattern
75 self.another_uid = md5(another_pattern)
75 self.another_uid = md5(another_pattern)
76 assert settings[self.another_uid]['pat'] == another_pattern
76 assert settings[self.another_uid]['pat'] == another_pattern
77
77
78 # test pattern
78 # test pattern
79 data = {'test_text': 'example of issuetracker_pat replacement',
79 data = {'test_text': 'example of issuetracker_pat replacement',
80 'csrf_token': csrf_token}
80 'csrf_token': csrf_token}
81 response = self.app.post(
81 response = self.app.post(
82 route_path('edit_repo_issuetracker_test',
82 route_path('edit_repo_issuetracker_test',
83 repo_name=backend.repo.repo_name),
83 repo_name=backend.repo.repo_name),
84 extra_environ=xhr_header, params=data)
84 extra_environ=xhr_header, params=data)
85
85
86 assert response.body == \
86 assert response.body == \
87 'example of <a class="tooltip issue-tracker-link" href="http://url" title="description">prefix</a> replacement'
87 'example of <a class="tooltip issue-tracker-link" href="http://url" title="description">prefix</a> replacement'
88
88
89 @request.addfinalizer
89 @request.addfinalizer
90 def cleanup():
90 def cleanup():
91 self.settings_model.delete_entries(self.uid)
91 self.settings_model.delete_entries(self.uid)
92 self.settings_model.delete_entries(self.another_uid)
92 self.settings_model.delete_entries(self.another_uid)
93
93
94 def test_edit_issuetracker_pattern(
94 def test_edit_issuetracker_pattern(
95 self, autologin_user, backend, csrf_token, request):
95 self, autologin_user, backend, csrf_token, request):
96 entry_key = 'issuetracker_pat_'
96 entry_key = 'issuetracker_pat_'
97 pattern = 'issuetracker_pat2'
97 pattern = 'issuetracker_pat2'
98 old_pattern = 'issuetracker_pat'
98 old_pattern = 'issuetracker_pat'
99 old_uid = md5(old_pattern)
99 old_uid = md5(old_pattern)
100
100
101 sett = SettingsModel(repo=backend.repo).create_or_update_setting(
101 sett = SettingsModel(repo=backend.repo).create_or_update_setting(
102 entry_key+old_uid, old_pattern, 'unicode')
102 entry_key+old_uid, old_pattern, 'unicode')
103 Session().add(sett)
103 Session().add(sett)
104 Session().commit()
104 Session().commit()
105 post_url = route_path(
105 post_url = route_path(
106 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
106 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
107 post_data = {
107 post_data = {
108 'new_pattern_pattern_0': pattern,
108 'new_pattern_pattern_0': pattern,
109 'new_pattern_url_0': '/url',
109 'new_pattern_url_0': '/url',
110 'new_pattern_prefix_0': 'prefix',
110 'new_pattern_prefix_0': 'prefix',
111 'new_pattern_description_0': 'description',
111 'new_pattern_description_0': 'description',
112 'uid': old_uid,
112 'uid': old_uid,
113 'csrf_token': csrf_token
113 'csrf_token': csrf_token
114 }
114 }
115 self.app.post(post_url, post_data, status=302)
115 self.app.post(post_url, post_data, status=302)
116 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
116 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
117 settings = self.settings_model.get_repo_settings()
117 settings = self.settings_model.get_repo_settings()
118 self.uid = md5(pattern)
118 self.uid = md5(pattern)
119 assert settings[self.uid]['pat'] == pattern
119 assert settings[self.uid]['pat'] == pattern
120 with pytest.raises(KeyError):
120 with pytest.raises(KeyError):
121 key = settings[old_uid]
121 key = settings[old_uid]
122
122
123 @request.addfinalizer
123 @request.addfinalizer
124 def cleanup():
124 def cleanup():
125 self.settings_model.delete_entries(self.uid)
125 self.settings_model.delete_entries(self.uid)
126
126
127 def test_delete_issuetracker_pattern(
127 def test_delete_issuetracker_pattern(
128 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
128 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
129 repo = backend.create_repo()
129 repo = backend.create_repo()
130 repo_name = repo.repo_name
130 repo_name = repo.repo_name
131 entry_key = 'issuetracker_pat_'
131 entry_key = 'issuetracker_pat_'
132 pattern = 'issuetracker_pat3'
132 pattern = 'issuetracker_pat3'
133 uid = md5(pattern)
133 uid = md5(pattern)
134 settings_util.create_repo_rhodecode_setting(
134 settings_util.create_repo_rhodecode_setting(
135 repo=backend.repo, name=entry_key+uid,
135 repo=backend.repo, name=entry_key+uid,
136 value=entry_key, type_='unicode', cleanup=False)
136 value=entry_key, type_='unicode', cleanup=False)
137
137
138 self.app.post(
138 self.app.post(
139 route_path(
139 route_path(
140 'edit_repo_issuetracker_delete',
140 'edit_repo_issuetracker_delete',
141 repo_name=backend.repo.repo_name),
141 repo_name=backend.repo.repo_name),
142 {
142 {
143 'uid': uid,
143 'uid': uid,
144 'csrf_token': csrf_token,
144 'csrf_token': csrf_token,
145 '': ''
145 '': ''
146 }, extra_environ=xhr_header, status=200)
146 }, extra_environ=xhr_header, status=200)
147 settings = IssueTrackerSettingsModel(
147 settings = IssueTrackerSettingsModel(
148 repo=Repository.get_by_repo_name(repo_name)).get_repo_settings()
148 repo=Repository.get_by_repo_name(repo_name)).get_repo_settings()
149 assert 'rhodecode_%s%s' % (entry_key, uid) not in settings
149 assert 'rhodecode_%s%s' % (entry_key, uid) not in settings
@@ -1,74 +1,74 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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
23
24 from rhodecode.lib.utils2 import str2bool
24 from rhodecode.lib.utils2 import str2bool
25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
26 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.tests import (
28 from rhodecode.tests import (
29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
30 from rhodecode.tests.fixture import Fixture
30 from rhodecode.tests.fixture import Fixture
31
31
32 fixture = Fixture()
32 fixture = Fixture()
33
33
34
34
35 def route_path(name, params=None, **kwargs):
35 def route_path(name, params=None, **kwargs):
36 import urllib
36 import urllib.request, urllib.parse, urllib.error
37
37
38 base_url = {
38 base_url = {
39 'edit_repo_maintenance': '/{repo_name}/settings/maintenance',
39 'edit_repo_maintenance': '/{repo_name}/settings/maintenance',
40 'edit_repo_maintenance_execute': '/{repo_name}/settings/maintenance/execute',
40 'edit_repo_maintenance_execute': '/{repo_name}/settings/maintenance/execute',
41
41
42 }[name].format(**kwargs)
42 }[name].format(**kwargs)
43
43
44 if params:
44 if params:
45 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
45 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 return base_url
46 return base_url
47
47
48
48
49 def _get_permission_for_user(user, repo):
49 def _get_permission_for_user(user, repo):
50 perm = UserRepoToPerm.query()\
50 perm = UserRepoToPerm.query()\
51 .filter(UserRepoToPerm.repository ==
51 .filter(UserRepoToPerm.repository ==
52 Repository.get_by_repo_name(repo))\
52 Repository.get_by_repo_name(repo))\
53 .filter(UserRepoToPerm.user == User.get_by_username(user))\
53 .filter(UserRepoToPerm.user == User.get_by_username(user))\
54 .all()
54 .all()
55 return perm
55 return perm
56
56
57
57
58 @pytest.mark.usefixtures('autologin_user', 'app')
58 @pytest.mark.usefixtures('autologin_user', 'app')
59 class TestAdminRepoMaintenance(object):
59 class TestAdminRepoMaintenance(object):
60 @pytest.mark.parametrize('urlname', [
60 @pytest.mark.parametrize('urlname', [
61 'edit_repo_maintenance',
61 'edit_repo_maintenance',
62 ])
62 ])
63 def test_show_page(self, urlname, app, backend):
63 def test_show_page(self, urlname, app, backend):
64 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
64 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
65
65
66 def test_execute_maintenance_for_repo_hg(self, app, backend_hg, autologin_user, xhr_header):
66 def test_execute_maintenance_for_repo_hg(self, app, backend_hg, autologin_user, xhr_header):
67 repo_name = backend_hg.repo_name
67 repo_name = backend_hg.repo_name
68
68
69 response = app.get(
69 response = app.get(
70 route_path('edit_repo_maintenance_execute',
70 route_path('edit_repo_maintenance_execute',
71 repo_name=repo_name,),
71 repo_name=repo_name,),
72 extra_environ=xhr_header)
72 extra_environ=xhr_header)
73
73
74 assert "HG Verify repo" in ''.join(response.json)
74 assert "HG Verify repo" in ''.join(response.json)
@@ -1,77 +1,77 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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.utils import permission_update_data_generator
23 from rhodecode.tests.utils import permission_update_data_generator
24
24
25
25
26 def route_path(name, params=None, **kwargs):
26 def route_path(name, params=None, **kwargs):
27 import urllib
27 import urllib.request, urllib.parse, urllib.error
28
28
29 base_url = {
29 base_url = {
30 'edit_repo_perms': '/{repo_name}/settings/permissions'
30 'edit_repo_perms': '/{repo_name}/settings/permissions'
31 # update is the same url
31 # update is the same url
32 }[name].format(**kwargs)
32 }[name].format(**kwargs)
33
33
34 if params:
34 if params:
35 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
36 return base_url
36 return base_url
37
37
38
38
39 @pytest.mark.usefixtures("app")
39 @pytest.mark.usefixtures("app")
40 class TestRepoPermissionsView(object):
40 class TestRepoPermissionsView(object):
41
41
42 def test_edit_perms_view(self, user_util, autologin_user):
42 def test_edit_perms_view(self, user_util, autologin_user):
43 repo = user_util.create_repo()
43 repo = user_util.create_repo()
44 self.app.get(
44 self.app.get(
45 route_path('edit_repo_perms',
45 route_path('edit_repo_perms',
46 repo_name=repo.repo_name), status=200)
46 repo_name=repo.repo_name), status=200)
47
47
48 def test_update_permissions(self, csrf_token, user_util):
48 def test_update_permissions(self, csrf_token, user_util):
49 repo = user_util.create_repo()
49 repo = user_util.create_repo()
50 repo_name = repo.repo_name
50 repo_name = repo.repo_name
51 user = user_util.create_user()
51 user = user_util.create_user()
52 user_id = user.user_id
52 user_id = user.user_id
53 username = user.username
53 username = user.username
54
54
55 # grant new
55 # grant new
56 form_data = permission_update_data_generator(
56 form_data = permission_update_data_generator(
57 csrf_token,
57 csrf_token,
58 default='repository.write',
58 default='repository.write',
59 grant=[(user_id, 'repository.write', username, 'user')])
59 grant=[(user_id, 'repository.write', username, 'user')])
60
60
61 response = self.app.post(
61 response = self.app.post(
62 route_path('edit_repo_perms',
62 route_path('edit_repo_perms',
63 repo_name=repo_name), form_data).follow()
63 repo_name=repo_name), form_data).follow()
64
64
65 assert 'Repository access permissions updated' in response
65 assert 'Repository access permissions updated' in response
66
66
67 # revoke given
67 # revoke given
68 form_data = permission_update_data_generator(
68 form_data = permission_update_data_generator(
69 csrf_token,
69 csrf_token,
70 default='repository.read',
70 default='repository.read',
71 revoke=[(user_id, 'user')])
71 revoke=[(user_id, 'user')])
72
72
73 response = self.app.post(
73 response = self.app.post(
74 route_path('edit_repo_perms',
74 route_path('edit_repo_perms',
75 repo_name=repo_name), form_data).follow()
75 repo_name=repo_name), form_data).follow()
76
76
77 assert 'Repository access permissions updated' in response
77 assert 'Repository access permissions updated' in response
@@ -1,1680 +1,1680 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
25 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
26 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib.vcs.nodes import FileNode
27 from rhodecode.lib.ext_json import json
27 from rhodecode.lib.ext_json import json
28 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.changeset_status import ChangesetStatusModel
29 from rhodecode.model.db import (
29 from rhodecode.model.db import (
30 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
31 from rhodecode.model.meta import Session
31 from rhodecode.model.meta import Session
32 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.pull_request import PullRequestModel
33 from rhodecode.model.user import UserModel
33 from rhodecode.model.user import UserModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.tests import (
35 from rhodecode.tests import (
36 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
37
37
38
38
39 def route_path(name, params=None, **kwargs):
39 def route_path(name, params=None, **kwargs):
40 import urllib
40 import urllib.request, urllib.parse, urllib.error
41
41
42 base_url = {
42 base_url = {
43 'repo_changelog': '/{repo_name}/changelog',
43 'repo_changelog': '/{repo_name}/changelog',
44 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
45 'repo_commits': '/{repo_name}/commits',
45 'repo_commits': '/{repo_name}/commits',
46 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
46 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
47 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
47 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
48 'pullrequest_show_all': '/{repo_name}/pull-request',
48 'pullrequest_show_all': '/{repo_name}/pull-request',
49 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
49 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
50 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
50 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
51 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
51 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
52 'pullrequest_new': '/{repo_name}/pull-request/new',
52 'pullrequest_new': '/{repo_name}/pull-request/new',
53 'pullrequest_create': '/{repo_name}/pull-request/create',
53 'pullrequest_create': '/{repo_name}/pull-request/create',
54 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
54 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
55 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
55 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
56 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
56 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
57 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
57 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
58 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
58 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
59 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
59 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
60 }[name].format(**kwargs)
60 }[name].format(**kwargs)
61
61
62 if params:
62 if params:
63 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
63 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
64 return base_url
64 return base_url
65
65
66
66
67 @pytest.mark.usefixtures('app', 'autologin_user')
67 @pytest.mark.usefixtures('app', 'autologin_user')
68 @pytest.mark.backends("git", "hg")
68 @pytest.mark.backends("git", "hg")
69 class TestPullrequestsView(object):
69 class TestPullrequestsView(object):
70
70
71 def test_index(self, backend):
71 def test_index(self, backend):
72 self.app.get(route_path(
72 self.app.get(route_path(
73 'pullrequest_new',
73 'pullrequest_new',
74 repo_name=backend.repo_name))
74 repo_name=backend.repo_name))
75
75
76 def test_option_menu_create_pull_request_exists(self, backend):
76 def test_option_menu_create_pull_request_exists(self, backend):
77 repo_name = backend.repo_name
77 repo_name = backend.repo_name
78 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
78 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
79
79
80 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
80 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
81 'pullrequest_new', repo_name=repo_name)
81 'pullrequest_new', repo_name=repo_name)
82 response.mustcontain(create_pr_link)
82 response.mustcontain(create_pr_link)
83
83
84 def test_create_pr_form_with_raw_commit_id(self, backend):
84 def test_create_pr_form_with_raw_commit_id(self, backend):
85 repo = backend.repo
85 repo = backend.repo
86
86
87 self.app.get(
87 self.app.get(
88 route_path('pullrequest_new', repo_name=repo.repo_name,
88 route_path('pullrequest_new', repo_name=repo.repo_name,
89 commit=repo.get_commit().raw_id),
89 commit=repo.get_commit().raw_id),
90 status=200)
90 status=200)
91
91
92 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
92 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
93 @pytest.mark.parametrize('range_diff', ["0", "1"])
93 @pytest.mark.parametrize('range_diff', ["0", "1"])
94 def test_show(self, pr_util, pr_merge_enabled, range_diff):
94 def test_show(self, pr_util, pr_merge_enabled, range_diff):
95 pull_request = pr_util.create_pull_request(
95 pull_request = pr_util.create_pull_request(
96 mergeable=pr_merge_enabled, enable_notifications=False)
96 mergeable=pr_merge_enabled, enable_notifications=False)
97
97
98 response = self.app.get(route_path(
98 response = self.app.get(route_path(
99 'pullrequest_show',
99 'pullrequest_show',
100 repo_name=pull_request.target_repo.scm_instance().name,
100 repo_name=pull_request.target_repo.scm_instance().name,
101 pull_request_id=pull_request.pull_request_id,
101 pull_request_id=pull_request.pull_request_id,
102 params={'range-diff': range_diff}))
102 params={'range-diff': range_diff}))
103
103
104 for commit_id in pull_request.revisions:
104 for commit_id in pull_request.revisions:
105 response.mustcontain(commit_id)
105 response.mustcontain(commit_id)
106
106
107 response.mustcontain(pull_request.target_ref_parts.type)
107 response.mustcontain(pull_request.target_ref_parts.type)
108 response.mustcontain(pull_request.target_ref_parts.name)
108 response.mustcontain(pull_request.target_ref_parts.name)
109
109
110 response.mustcontain('class="pull-request-merge"')
110 response.mustcontain('class="pull-request-merge"')
111
111
112 if pr_merge_enabled:
112 if pr_merge_enabled:
113 response.mustcontain('Pull request reviewer approval is pending')
113 response.mustcontain('Pull request reviewer approval is pending')
114 else:
114 else:
115 response.mustcontain('Server-side pull request merging is disabled.')
115 response.mustcontain('Server-side pull request merging is disabled.')
116
116
117 if range_diff == "1":
117 if range_diff == "1":
118 response.mustcontain('Turn off: Show the diff as commit range')
118 response.mustcontain('Turn off: Show the diff as commit range')
119
119
120 def test_show_versions_of_pr(self, backend, csrf_token):
120 def test_show_versions_of_pr(self, backend, csrf_token):
121 commits = [
121 commits = [
122 {'message': 'initial-commit',
122 {'message': 'initial-commit',
123 'added': [FileNode('test-file.txt', 'LINE1\n')]},
123 'added': [FileNode('test-file.txt', 'LINE1\n')]},
124
124
125 {'message': 'commit-1',
125 {'message': 'commit-1',
126 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
126 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
127 # Above is the initial version of PR that changes a single line
127 # Above is the initial version of PR that changes a single line
128
128
129 # from now on we'll add 3x commit adding a nother line on each step
129 # from now on we'll add 3x commit adding a nother line on each step
130 {'message': 'commit-2',
130 {'message': 'commit-2',
131 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
131 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
132
132
133 {'message': 'commit-3',
133 {'message': 'commit-3',
134 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
134 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
135
135
136 {'message': 'commit-4',
136 {'message': 'commit-4',
137 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
137 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
138 ]
138 ]
139
139
140 commit_ids = backend.create_master_repo(commits)
140 commit_ids = backend.create_master_repo(commits)
141 target = backend.create_repo(heads=['initial-commit'])
141 target = backend.create_repo(heads=['initial-commit'])
142 source = backend.create_repo(heads=['commit-1'])
142 source = backend.create_repo(heads=['commit-1'])
143 source_repo_name = source.repo_name
143 source_repo_name = source.repo_name
144 target_repo_name = target.repo_name
144 target_repo_name = target.repo_name
145
145
146 target_ref = 'branch:{branch}:{commit_id}'.format(
146 target_ref = 'branch:{branch}:{commit_id}'.format(
147 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
147 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
148 source_ref = 'branch:{branch}:{commit_id}'.format(
148 source_ref = 'branch:{branch}:{commit_id}'.format(
149 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
149 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
150
150
151 response = self.app.post(
151 response = self.app.post(
152 route_path('pullrequest_create', repo_name=source.repo_name),
152 route_path('pullrequest_create', repo_name=source.repo_name),
153 [
153 [
154 ('source_repo', source_repo_name),
154 ('source_repo', source_repo_name),
155 ('source_ref', source_ref),
155 ('source_ref', source_ref),
156 ('target_repo', target_repo_name),
156 ('target_repo', target_repo_name),
157 ('target_ref', target_ref),
157 ('target_ref', target_ref),
158 ('common_ancestor', commit_ids['initial-commit']),
158 ('common_ancestor', commit_ids['initial-commit']),
159 ('pullrequest_title', 'Title'),
159 ('pullrequest_title', 'Title'),
160 ('pullrequest_desc', 'Description'),
160 ('pullrequest_desc', 'Description'),
161 ('description_renderer', 'markdown'),
161 ('description_renderer', 'markdown'),
162 ('__start__', 'review_members:sequence'),
162 ('__start__', 'review_members:sequence'),
163 ('__start__', 'reviewer:mapping'),
163 ('__start__', 'reviewer:mapping'),
164 ('user_id', '1'),
164 ('user_id', '1'),
165 ('__start__', 'reasons:sequence'),
165 ('__start__', 'reasons:sequence'),
166 ('reason', 'Some reason'),
166 ('reason', 'Some reason'),
167 ('__end__', 'reasons:sequence'),
167 ('__end__', 'reasons:sequence'),
168 ('__start__', 'rules:sequence'),
168 ('__start__', 'rules:sequence'),
169 ('__end__', 'rules:sequence'),
169 ('__end__', 'rules:sequence'),
170 ('mandatory', 'False'),
170 ('mandatory', 'False'),
171 ('__end__', 'reviewer:mapping'),
171 ('__end__', 'reviewer:mapping'),
172 ('__end__', 'review_members:sequence'),
172 ('__end__', 'review_members:sequence'),
173 ('__start__', 'revisions:sequence'),
173 ('__start__', 'revisions:sequence'),
174 ('revisions', commit_ids['commit-1']),
174 ('revisions', commit_ids['commit-1']),
175 ('__end__', 'revisions:sequence'),
175 ('__end__', 'revisions:sequence'),
176 ('user', ''),
176 ('user', ''),
177 ('csrf_token', csrf_token),
177 ('csrf_token', csrf_token),
178 ],
178 ],
179 status=302)
179 status=302)
180
180
181 location = response.headers['Location']
181 location = response.headers['Location']
182
182
183 pull_request_id = location.rsplit('/', 1)[1]
183 pull_request_id = location.rsplit('/', 1)[1]
184 assert pull_request_id != 'new'
184 assert pull_request_id != 'new'
185 pull_request = PullRequest.get(int(pull_request_id))
185 pull_request = PullRequest.get(int(pull_request_id))
186
186
187 pull_request_id = pull_request.pull_request_id
187 pull_request_id = pull_request.pull_request_id
188
188
189 # Show initial version of PR
189 # Show initial version of PR
190 response = self.app.get(
190 response = self.app.get(
191 route_path('pullrequest_show',
191 route_path('pullrequest_show',
192 repo_name=target_repo_name,
192 repo_name=target_repo_name,
193 pull_request_id=pull_request_id))
193 pull_request_id=pull_request_id))
194
194
195 response.mustcontain('commit-1')
195 response.mustcontain('commit-1')
196 response.mustcontain(no=['commit-2'])
196 response.mustcontain(no=['commit-2'])
197 response.mustcontain(no=['commit-3'])
197 response.mustcontain(no=['commit-3'])
198 response.mustcontain(no=['commit-4'])
198 response.mustcontain(no=['commit-4'])
199
199
200 response.mustcontain('cb-addition"></span><span>LINE2</span>')
200 response.mustcontain('cb-addition"></span><span>LINE2</span>')
201 response.mustcontain(no=['LINE3'])
201 response.mustcontain(no=['LINE3'])
202 response.mustcontain(no=['LINE4'])
202 response.mustcontain(no=['LINE4'])
203 response.mustcontain(no=['LINE5'])
203 response.mustcontain(no=['LINE5'])
204
204
205 # update PR #1
205 # update PR #1
206 source_repo = Repository.get_by_repo_name(source_repo_name)
206 source_repo = Repository.get_by_repo_name(source_repo_name)
207 backend.pull_heads(source_repo, heads=['commit-2'])
207 backend.pull_heads(source_repo, heads=['commit-2'])
208 response = self.app.post(
208 response = self.app.post(
209 route_path('pullrequest_update',
209 route_path('pullrequest_update',
210 repo_name=target_repo_name, pull_request_id=pull_request_id),
210 repo_name=target_repo_name, pull_request_id=pull_request_id),
211 params={'update_commits': 'true', 'csrf_token': csrf_token})
211 params={'update_commits': 'true', 'csrf_token': csrf_token})
212
212
213 # update PR #2
213 # update PR #2
214 source_repo = Repository.get_by_repo_name(source_repo_name)
214 source_repo = Repository.get_by_repo_name(source_repo_name)
215 backend.pull_heads(source_repo, heads=['commit-3'])
215 backend.pull_heads(source_repo, heads=['commit-3'])
216 response = self.app.post(
216 response = self.app.post(
217 route_path('pullrequest_update',
217 route_path('pullrequest_update',
218 repo_name=target_repo_name, pull_request_id=pull_request_id),
218 repo_name=target_repo_name, pull_request_id=pull_request_id),
219 params={'update_commits': 'true', 'csrf_token': csrf_token})
219 params={'update_commits': 'true', 'csrf_token': csrf_token})
220
220
221 # update PR #3
221 # update PR #3
222 source_repo = Repository.get_by_repo_name(source_repo_name)
222 source_repo = Repository.get_by_repo_name(source_repo_name)
223 backend.pull_heads(source_repo, heads=['commit-4'])
223 backend.pull_heads(source_repo, heads=['commit-4'])
224 response = self.app.post(
224 response = self.app.post(
225 route_path('pullrequest_update',
225 route_path('pullrequest_update',
226 repo_name=target_repo_name, pull_request_id=pull_request_id),
226 repo_name=target_repo_name, pull_request_id=pull_request_id),
227 params={'update_commits': 'true', 'csrf_token': csrf_token})
227 params={'update_commits': 'true', 'csrf_token': csrf_token})
228
228
229 # Show final version !
229 # Show final version !
230 response = self.app.get(
230 response = self.app.get(
231 route_path('pullrequest_show',
231 route_path('pullrequest_show',
232 repo_name=target_repo_name,
232 repo_name=target_repo_name,
233 pull_request_id=pull_request_id))
233 pull_request_id=pull_request_id))
234
234
235 # 3 updates, and the latest == 4
235 # 3 updates, and the latest == 4
236 response.mustcontain('4 versions available for this pull request')
236 response.mustcontain('4 versions available for this pull request')
237 response.mustcontain(no=['rhodecode diff rendering error'])
237 response.mustcontain(no=['rhodecode diff rendering error'])
238
238
239 # initial show must have 3 commits, and 3 adds
239 # initial show must have 3 commits, and 3 adds
240 response.mustcontain('commit-1')
240 response.mustcontain('commit-1')
241 response.mustcontain('commit-2')
241 response.mustcontain('commit-2')
242 response.mustcontain('commit-3')
242 response.mustcontain('commit-3')
243 response.mustcontain('commit-4')
243 response.mustcontain('commit-4')
244
244
245 response.mustcontain('cb-addition"></span><span>LINE2</span>')
245 response.mustcontain('cb-addition"></span><span>LINE2</span>')
246 response.mustcontain('cb-addition"></span><span>LINE3</span>')
246 response.mustcontain('cb-addition"></span><span>LINE3</span>')
247 response.mustcontain('cb-addition"></span><span>LINE4</span>')
247 response.mustcontain('cb-addition"></span><span>LINE4</span>')
248 response.mustcontain('cb-addition"></span><span>LINE5</span>')
248 response.mustcontain('cb-addition"></span><span>LINE5</span>')
249
249
250 # fetch versions
250 # fetch versions
251 pr = PullRequest.get(pull_request_id)
251 pr = PullRequest.get(pull_request_id)
252 versions = [x.pull_request_version_id for x in pr.versions.all()]
252 versions = [x.pull_request_version_id for x in pr.versions.all()]
253 assert len(versions) == 3
253 assert len(versions) == 3
254
254
255 # show v1,v2,v3,v4
255 # show v1,v2,v3,v4
256 def cb_line(text):
256 def cb_line(text):
257 return 'cb-addition"></span><span>{}</span>'.format(text)
257 return 'cb-addition"></span><span>{}</span>'.format(text)
258
258
259 def cb_context(text):
259 def cb_context(text):
260 return '<span class="cb-code"><span class="cb-action cb-context">' \
260 return '<span class="cb-code"><span class="cb-action cb-context">' \
261 '</span><span>{}</span></span>'.format(text)
261 '</span><span>{}</span></span>'.format(text)
262
262
263 commit_tests = {
263 commit_tests = {
264 # in response, not in response
264 # in response, not in response
265 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
265 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
266 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
266 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
267 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
267 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
268 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
268 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
269 }
269 }
270 diff_tests = {
270 diff_tests = {
271 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
271 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
272 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
272 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
273 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
273 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
274 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
274 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
275 }
275 }
276 for idx, ver in enumerate(versions, 1):
276 for idx, ver in enumerate(versions, 1):
277
277
278 response = self.app.get(
278 response = self.app.get(
279 route_path('pullrequest_show',
279 route_path('pullrequest_show',
280 repo_name=target_repo_name,
280 repo_name=target_repo_name,
281 pull_request_id=pull_request_id,
281 pull_request_id=pull_request_id,
282 params={'version': ver}))
282 params={'version': ver}))
283
283
284 response.mustcontain(no=['rhodecode diff rendering error'])
284 response.mustcontain(no=['rhodecode diff rendering error'])
285 response.mustcontain('Showing changes at v{}'.format(idx))
285 response.mustcontain('Showing changes at v{}'.format(idx))
286
286
287 yes, no = commit_tests[idx]
287 yes, no = commit_tests[idx]
288 for y in yes:
288 for y in yes:
289 response.mustcontain(y)
289 response.mustcontain(y)
290 for n in no:
290 for n in no:
291 response.mustcontain(no=n)
291 response.mustcontain(no=n)
292
292
293 yes, no = diff_tests[idx]
293 yes, no = diff_tests[idx]
294 for y in yes:
294 for y in yes:
295 response.mustcontain(cb_line(y))
295 response.mustcontain(cb_line(y))
296 for n in no:
296 for n in no:
297 response.mustcontain(no=n)
297 response.mustcontain(no=n)
298
298
299 # show diff between versions
299 # show diff between versions
300 diff_compare_tests = {
300 diff_compare_tests = {
301 1: (['LINE3'], ['LINE1', 'LINE2']),
301 1: (['LINE3'], ['LINE1', 'LINE2']),
302 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
302 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
303 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
303 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
304 }
304 }
305 for idx, ver in enumerate(versions, 1):
305 for idx, ver in enumerate(versions, 1):
306 adds, context = diff_compare_tests[idx]
306 adds, context = diff_compare_tests[idx]
307
307
308 to_ver = ver+1
308 to_ver = ver+1
309 if idx == 3:
309 if idx == 3:
310 to_ver = 'latest'
310 to_ver = 'latest'
311
311
312 response = self.app.get(
312 response = self.app.get(
313 route_path('pullrequest_show',
313 route_path('pullrequest_show',
314 repo_name=target_repo_name,
314 repo_name=target_repo_name,
315 pull_request_id=pull_request_id,
315 pull_request_id=pull_request_id,
316 params={'from_version': versions[0], 'version': to_ver}))
316 params={'from_version': versions[0], 'version': to_ver}))
317
317
318 response.mustcontain(no=['rhodecode diff rendering error'])
318 response.mustcontain(no=['rhodecode diff rendering error'])
319
319
320 for a in adds:
320 for a in adds:
321 response.mustcontain(cb_line(a))
321 response.mustcontain(cb_line(a))
322 for c in context:
322 for c in context:
323 response.mustcontain(cb_context(c))
323 response.mustcontain(cb_context(c))
324
324
325 # test version v2 -> v3
325 # test version v2 -> v3
326 response = self.app.get(
326 response = self.app.get(
327 route_path('pullrequest_show',
327 route_path('pullrequest_show',
328 repo_name=target_repo_name,
328 repo_name=target_repo_name,
329 pull_request_id=pull_request_id,
329 pull_request_id=pull_request_id,
330 params={'from_version': versions[1], 'version': versions[2]}))
330 params={'from_version': versions[1], 'version': versions[2]}))
331
331
332 response.mustcontain(cb_context('LINE1'))
332 response.mustcontain(cb_context('LINE1'))
333 response.mustcontain(cb_context('LINE2'))
333 response.mustcontain(cb_context('LINE2'))
334 response.mustcontain(cb_context('LINE3'))
334 response.mustcontain(cb_context('LINE3'))
335 response.mustcontain(cb_line('LINE4'))
335 response.mustcontain(cb_line('LINE4'))
336
336
337 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
337 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
338 # Logout
338 # Logout
339 response = self.app.post(
339 response = self.app.post(
340 h.route_path('logout'),
340 h.route_path('logout'),
341 params={'csrf_token': csrf_token})
341 params={'csrf_token': csrf_token})
342 # Login as regular user
342 # Login as regular user
343 response = self.app.post(h.route_path('login'),
343 response = self.app.post(h.route_path('login'),
344 {'username': TEST_USER_REGULAR_LOGIN,
344 {'username': TEST_USER_REGULAR_LOGIN,
345 'password': 'test12'})
345 'password': 'test12'})
346
346
347 pull_request = pr_util.create_pull_request(
347 pull_request = pr_util.create_pull_request(
348 author=TEST_USER_REGULAR_LOGIN)
348 author=TEST_USER_REGULAR_LOGIN)
349
349
350 response = self.app.get(route_path(
350 response = self.app.get(route_path(
351 'pullrequest_show',
351 'pullrequest_show',
352 repo_name=pull_request.target_repo.scm_instance().name,
352 repo_name=pull_request.target_repo.scm_instance().name,
353 pull_request_id=pull_request.pull_request_id))
353 pull_request_id=pull_request.pull_request_id))
354
354
355 response.mustcontain('Server-side pull request merging is disabled.')
355 response.mustcontain('Server-side pull request merging is disabled.')
356
356
357 assert_response = response.assert_response()
357 assert_response = response.assert_response()
358 # for regular user without a merge permissions, we don't see it
358 # for regular user without a merge permissions, we don't see it
359 assert_response.no_element_exists('#close-pull-request-action')
359 assert_response.no_element_exists('#close-pull-request-action')
360
360
361 user_util.grant_user_permission_to_repo(
361 user_util.grant_user_permission_to_repo(
362 pull_request.target_repo,
362 pull_request.target_repo,
363 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
363 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
364 'repository.write')
364 'repository.write')
365 response = self.app.get(route_path(
365 response = self.app.get(route_path(
366 'pullrequest_show',
366 'pullrequest_show',
367 repo_name=pull_request.target_repo.scm_instance().name,
367 repo_name=pull_request.target_repo.scm_instance().name,
368 pull_request_id=pull_request.pull_request_id))
368 pull_request_id=pull_request.pull_request_id))
369
369
370 response.mustcontain('Server-side pull request merging is disabled.')
370 response.mustcontain('Server-side pull request merging is disabled.')
371
371
372 assert_response = response.assert_response()
372 assert_response = response.assert_response()
373 # now regular user has a merge permissions, we have CLOSE button
373 # now regular user has a merge permissions, we have CLOSE button
374 assert_response.one_element_exists('#close-pull-request-action')
374 assert_response.one_element_exists('#close-pull-request-action')
375
375
376 def test_show_invalid_commit_id(self, pr_util):
376 def test_show_invalid_commit_id(self, pr_util):
377 # Simulating invalid revisions which will cause a lookup error
377 # Simulating invalid revisions which will cause a lookup error
378 pull_request = pr_util.create_pull_request()
378 pull_request = pr_util.create_pull_request()
379 pull_request.revisions = ['invalid']
379 pull_request.revisions = ['invalid']
380 Session().add(pull_request)
380 Session().add(pull_request)
381 Session().commit()
381 Session().commit()
382
382
383 response = self.app.get(route_path(
383 response = self.app.get(route_path(
384 'pullrequest_show',
384 'pullrequest_show',
385 repo_name=pull_request.target_repo.scm_instance().name,
385 repo_name=pull_request.target_repo.scm_instance().name,
386 pull_request_id=pull_request.pull_request_id))
386 pull_request_id=pull_request.pull_request_id))
387
387
388 for commit_id in pull_request.revisions:
388 for commit_id in pull_request.revisions:
389 response.mustcontain(commit_id)
389 response.mustcontain(commit_id)
390
390
391 def test_show_invalid_source_reference(self, pr_util):
391 def test_show_invalid_source_reference(self, pr_util):
392 pull_request = pr_util.create_pull_request()
392 pull_request = pr_util.create_pull_request()
393 pull_request.source_ref = 'branch:b:invalid'
393 pull_request.source_ref = 'branch:b:invalid'
394 Session().add(pull_request)
394 Session().add(pull_request)
395 Session().commit()
395 Session().commit()
396
396
397 self.app.get(route_path(
397 self.app.get(route_path(
398 'pullrequest_show',
398 'pullrequest_show',
399 repo_name=pull_request.target_repo.scm_instance().name,
399 repo_name=pull_request.target_repo.scm_instance().name,
400 pull_request_id=pull_request.pull_request_id))
400 pull_request_id=pull_request.pull_request_id))
401
401
402 def test_edit_title_description(self, pr_util, csrf_token):
402 def test_edit_title_description(self, pr_util, csrf_token):
403 pull_request = pr_util.create_pull_request()
403 pull_request = pr_util.create_pull_request()
404 pull_request_id = pull_request.pull_request_id
404 pull_request_id = pull_request.pull_request_id
405
405
406 response = self.app.post(
406 response = self.app.post(
407 route_path('pullrequest_update',
407 route_path('pullrequest_update',
408 repo_name=pull_request.target_repo.repo_name,
408 repo_name=pull_request.target_repo.repo_name,
409 pull_request_id=pull_request_id),
409 pull_request_id=pull_request_id),
410 params={
410 params={
411 'edit_pull_request': 'true',
411 'edit_pull_request': 'true',
412 'title': 'New title',
412 'title': 'New title',
413 'description': 'New description',
413 'description': 'New description',
414 'csrf_token': csrf_token})
414 'csrf_token': csrf_token})
415
415
416 assert_session_flash(
416 assert_session_flash(
417 response, u'Pull request title & description updated.',
417 response, u'Pull request title & description updated.',
418 category='success')
418 category='success')
419
419
420 pull_request = PullRequest.get(pull_request_id)
420 pull_request = PullRequest.get(pull_request_id)
421 assert pull_request.title == 'New title'
421 assert pull_request.title == 'New title'
422 assert pull_request.description == 'New description'
422 assert pull_request.description == 'New description'
423
423
424 def test_edit_title_description(self, pr_util, csrf_token):
424 def test_edit_title_description(self, pr_util, csrf_token):
425 pull_request = pr_util.create_pull_request()
425 pull_request = pr_util.create_pull_request()
426 pull_request_id = pull_request.pull_request_id
426 pull_request_id = pull_request.pull_request_id
427
427
428 response = self.app.post(
428 response = self.app.post(
429 route_path('pullrequest_update',
429 route_path('pullrequest_update',
430 repo_name=pull_request.target_repo.repo_name,
430 repo_name=pull_request.target_repo.repo_name,
431 pull_request_id=pull_request_id),
431 pull_request_id=pull_request_id),
432 params={
432 params={
433 'edit_pull_request': 'true',
433 'edit_pull_request': 'true',
434 'title': 'New title {} {2} {foo}',
434 'title': 'New title {} {2} {foo}',
435 'description': 'New description',
435 'description': 'New description',
436 'csrf_token': csrf_token})
436 'csrf_token': csrf_token})
437
437
438 assert_session_flash(
438 assert_session_flash(
439 response, u'Pull request title & description updated.',
439 response, u'Pull request title & description updated.',
440 category='success')
440 category='success')
441
441
442 pull_request = PullRequest.get(pull_request_id)
442 pull_request = PullRequest.get(pull_request_id)
443 assert pull_request.title_safe == 'New title {{}} {{2}} {{foo}}'
443 assert pull_request.title_safe == 'New title {{}} {{2}} {{foo}}'
444
444
445 def test_edit_title_description_closed(self, pr_util, csrf_token):
445 def test_edit_title_description_closed(self, pr_util, csrf_token):
446 pull_request = pr_util.create_pull_request()
446 pull_request = pr_util.create_pull_request()
447 pull_request_id = pull_request.pull_request_id
447 pull_request_id = pull_request.pull_request_id
448 repo_name = pull_request.target_repo.repo_name
448 repo_name = pull_request.target_repo.repo_name
449 pr_util.close()
449 pr_util.close()
450
450
451 response = self.app.post(
451 response = self.app.post(
452 route_path('pullrequest_update',
452 route_path('pullrequest_update',
453 repo_name=repo_name, pull_request_id=pull_request_id),
453 repo_name=repo_name, pull_request_id=pull_request_id),
454 params={
454 params={
455 'edit_pull_request': 'true',
455 'edit_pull_request': 'true',
456 'title': 'New title',
456 'title': 'New title',
457 'description': 'New description',
457 'description': 'New description',
458 'csrf_token': csrf_token}, status=200)
458 'csrf_token': csrf_token}, status=200)
459 assert_session_flash(
459 assert_session_flash(
460 response, u'Cannot update closed pull requests.',
460 response, u'Cannot update closed pull requests.',
461 category='error')
461 category='error')
462
462
463 def test_update_invalid_source_reference(self, pr_util, csrf_token):
463 def test_update_invalid_source_reference(self, pr_util, csrf_token):
464 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
464 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
465
465
466 pull_request = pr_util.create_pull_request()
466 pull_request = pr_util.create_pull_request()
467 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
467 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
468 Session().add(pull_request)
468 Session().add(pull_request)
469 Session().commit()
469 Session().commit()
470
470
471 pull_request_id = pull_request.pull_request_id
471 pull_request_id = pull_request.pull_request_id
472
472
473 response = self.app.post(
473 response = self.app.post(
474 route_path('pullrequest_update',
474 route_path('pullrequest_update',
475 repo_name=pull_request.target_repo.repo_name,
475 repo_name=pull_request.target_repo.repo_name,
476 pull_request_id=pull_request_id),
476 pull_request_id=pull_request_id),
477 params={'update_commits': 'true', 'csrf_token': csrf_token})
477 params={'update_commits': 'true', 'csrf_token': csrf_token})
478
478
479 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
479 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
480 UpdateFailureReason.MISSING_SOURCE_REF])
480 UpdateFailureReason.MISSING_SOURCE_REF])
481 assert_session_flash(response, expected_msg, category='error')
481 assert_session_flash(response, expected_msg, category='error')
482
482
483 def test_missing_target_reference(self, pr_util, csrf_token):
483 def test_missing_target_reference(self, pr_util, csrf_token):
484 from rhodecode.lib.vcs.backends.base import MergeFailureReason
484 from rhodecode.lib.vcs.backends.base import MergeFailureReason
485 pull_request = pr_util.create_pull_request(
485 pull_request = pr_util.create_pull_request(
486 approved=True, mergeable=True)
486 approved=True, mergeable=True)
487 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
487 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
488 pull_request.target_ref = unicode_reference
488 pull_request.target_ref = unicode_reference
489 Session().add(pull_request)
489 Session().add(pull_request)
490 Session().commit()
490 Session().commit()
491
491
492 pull_request_id = pull_request.pull_request_id
492 pull_request_id = pull_request.pull_request_id
493 pull_request_url = route_path(
493 pull_request_url = route_path(
494 'pullrequest_show',
494 'pullrequest_show',
495 repo_name=pull_request.target_repo.repo_name,
495 repo_name=pull_request.target_repo.repo_name,
496 pull_request_id=pull_request_id)
496 pull_request_id=pull_request_id)
497
497
498 response = self.app.get(pull_request_url)
498 response = self.app.get(pull_request_url)
499 target_ref_id = 'invalid-branch'
499 target_ref_id = 'invalid-branch'
500 merge_resp = MergeResponse(
500 merge_resp = MergeResponse(
501 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
501 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
502 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
502 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
503 response.assert_response().element_contains(
503 response.assert_response().element_contains(
504 'div[data-role="merge-message"]', merge_resp.merge_status_message)
504 'div[data-role="merge-message"]', merge_resp.merge_status_message)
505
505
506 def test_comment_and_close_pull_request_custom_message_approved(
506 def test_comment_and_close_pull_request_custom_message_approved(
507 self, pr_util, csrf_token, xhr_header):
507 self, pr_util, csrf_token, xhr_header):
508
508
509 pull_request = pr_util.create_pull_request(approved=True)
509 pull_request = pr_util.create_pull_request(approved=True)
510 pull_request_id = pull_request.pull_request_id
510 pull_request_id = pull_request.pull_request_id
511 author = pull_request.user_id
511 author = pull_request.user_id
512 repo = pull_request.target_repo.repo_id
512 repo = pull_request.target_repo.repo_id
513
513
514 self.app.post(
514 self.app.post(
515 route_path('pullrequest_comment_create',
515 route_path('pullrequest_comment_create',
516 repo_name=pull_request.target_repo.scm_instance().name,
516 repo_name=pull_request.target_repo.scm_instance().name,
517 pull_request_id=pull_request_id),
517 pull_request_id=pull_request_id),
518 params={
518 params={
519 'close_pull_request': '1',
519 'close_pull_request': '1',
520 'text': 'Closing a PR',
520 'text': 'Closing a PR',
521 'csrf_token': csrf_token},
521 'csrf_token': csrf_token},
522 extra_environ=xhr_header,)
522 extra_environ=xhr_header,)
523
523
524 journal = UserLog.query()\
524 journal = UserLog.query()\
525 .filter(UserLog.user_id == author)\
525 .filter(UserLog.user_id == author)\
526 .filter(UserLog.repository_id == repo) \
526 .filter(UserLog.repository_id == repo) \
527 .order_by(UserLog.user_log_id.asc()) \
527 .order_by(UserLog.user_log_id.asc()) \
528 .all()
528 .all()
529 assert journal[-1].action == 'repo.pull_request.close'
529 assert journal[-1].action == 'repo.pull_request.close'
530
530
531 pull_request = PullRequest.get(pull_request_id)
531 pull_request = PullRequest.get(pull_request_id)
532 assert pull_request.is_closed()
532 assert pull_request.is_closed()
533
533
534 status = ChangesetStatusModel().get_status(
534 status = ChangesetStatusModel().get_status(
535 pull_request.source_repo, pull_request=pull_request)
535 pull_request.source_repo, pull_request=pull_request)
536 assert status == ChangesetStatus.STATUS_APPROVED
536 assert status == ChangesetStatus.STATUS_APPROVED
537 comments = ChangesetComment().query() \
537 comments = ChangesetComment().query() \
538 .filter(ChangesetComment.pull_request == pull_request) \
538 .filter(ChangesetComment.pull_request == pull_request) \
539 .order_by(ChangesetComment.comment_id.asc())\
539 .order_by(ChangesetComment.comment_id.asc())\
540 .all()
540 .all()
541 assert comments[-1].text == 'Closing a PR'
541 assert comments[-1].text == 'Closing a PR'
542
542
543 def test_comment_force_close_pull_request_rejected(
543 def test_comment_force_close_pull_request_rejected(
544 self, pr_util, csrf_token, xhr_header):
544 self, pr_util, csrf_token, xhr_header):
545 pull_request = pr_util.create_pull_request()
545 pull_request = pr_util.create_pull_request()
546 pull_request_id = pull_request.pull_request_id
546 pull_request_id = pull_request.pull_request_id
547 PullRequestModel().update_reviewers(
547 PullRequestModel().update_reviewers(
548 pull_request_id, [
548 pull_request_id, [
549 (1, ['reason'], False, 'reviewer', []),
549 (1, ['reason'], False, 'reviewer', []),
550 (2, ['reason2'], False, 'reviewer', [])],
550 (2, ['reason2'], False, 'reviewer', [])],
551 pull_request.author)
551 pull_request.author)
552 author = pull_request.user_id
552 author = pull_request.user_id
553 repo = pull_request.target_repo.repo_id
553 repo = pull_request.target_repo.repo_id
554
554
555 self.app.post(
555 self.app.post(
556 route_path('pullrequest_comment_create',
556 route_path('pullrequest_comment_create',
557 repo_name=pull_request.target_repo.scm_instance().name,
557 repo_name=pull_request.target_repo.scm_instance().name,
558 pull_request_id=pull_request_id),
558 pull_request_id=pull_request_id),
559 params={
559 params={
560 'close_pull_request': '1',
560 'close_pull_request': '1',
561 'csrf_token': csrf_token},
561 'csrf_token': csrf_token},
562 extra_environ=xhr_header)
562 extra_environ=xhr_header)
563
563
564 pull_request = PullRequest.get(pull_request_id)
564 pull_request = PullRequest.get(pull_request_id)
565
565
566 journal = UserLog.query()\
566 journal = UserLog.query()\
567 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
567 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
568 .order_by(UserLog.user_log_id.asc()) \
568 .order_by(UserLog.user_log_id.asc()) \
569 .all()
569 .all()
570 assert journal[-1].action == 'repo.pull_request.close'
570 assert journal[-1].action == 'repo.pull_request.close'
571
571
572 # check only the latest status, not the review status
572 # check only the latest status, not the review status
573 status = ChangesetStatusModel().get_status(
573 status = ChangesetStatusModel().get_status(
574 pull_request.source_repo, pull_request=pull_request)
574 pull_request.source_repo, pull_request=pull_request)
575 assert status == ChangesetStatus.STATUS_REJECTED
575 assert status == ChangesetStatus.STATUS_REJECTED
576
576
577 def test_comment_and_close_pull_request(
577 def test_comment_and_close_pull_request(
578 self, pr_util, csrf_token, xhr_header):
578 self, pr_util, csrf_token, xhr_header):
579 pull_request = pr_util.create_pull_request()
579 pull_request = pr_util.create_pull_request()
580 pull_request_id = pull_request.pull_request_id
580 pull_request_id = pull_request.pull_request_id
581
581
582 response = self.app.post(
582 response = self.app.post(
583 route_path('pullrequest_comment_create',
583 route_path('pullrequest_comment_create',
584 repo_name=pull_request.target_repo.scm_instance().name,
584 repo_name=pull_request.target_repo.scm_instance().name,
585 pull_request_id=pull_request.pull_request_id),
585 pull_request_id=pull_request.pull_request_id),
586 params={
586 params={
587 'close_pull_request': 'true',
587 'close_pull_request': 'true',
588 'csrf_token': csrf_token},
588 'csrf_token': csrf_token},
589 extra_environ=xhr_header)
589 extra_environ=xhr_header)
590
590
591 assert response.json
591 assert response.json
592
592
593 pull_request = PullRequest.get(pull_request_id)
593 pull_request = PullRequest.get(pull_request_id)
594 assert pull_request.is_closed()
594 assert pull_request.is_closed()
595
595
596 # check only the latest status, not the review status
596 # check only the latest status, not the review status
597 status = ChangesetStatusModel().get_status(
597 status = ChangesetStatusModel().get_status(
598 pull_request.source_repo, pull_request=pull_request)
598 pull_request.source_repo, pull_request=pull_request)
599 assert status == ChangesetStatus.STATUS_REJECTED
599 assert status == ChangesetStatus.STATUS_REJECTED
600
600
601 def test_comment_and_close_pull_request_try_edit_comment(
601 def test_comment_and_close_pull_request_try_edit_comment(
602 self, pr_util, csrf_token, xhr_header
602 self, pr_util, csrf_token, xhr_header
603 ):
603 ):
604 pull_request = pr_util.create_pull_request()
604 pull_request = pr_util.create_pull_request()
605 pull_request_id = pull_request.pull_request_id
605 pull_request_id = pull_request.pull_request_id
606 target_scm = pull_request.target_repo.scm_instance()
606 target_scm = pull_request.target_repo.scm_instance()
607 target_scm_name = target_scm.name
607 target_scm_name = target_scm.name
608
608
609 response = self.app.post(
609 response = self.app.post(
610 route_path(
610 route_path(
611 'pullrequest_comment_create',
611 'pullrequest_comment_create',
612 repo_name=target_scm_name,
612 repo_name=target_scm_name,
613 pull_request_id=pull_request_id,
613 pull_request_id=pull_request_id,
614 ),
614 ),
615 params={
615 params={
616 'close_pull_request': 'true',
616 'close_pull_request': 'true',
617 'csrf_token': csrf_token,
617 'csrf_token': csrf_token,
618 },
618 },
619 extra_environ=xhr_header)
619 extra_environ=xhr_header)
620
620
621 assert response.json
621 assert response.json
622
622
623 pull_request = PullRequest.get(pull_request_id)
623 pull_request = PullRequest.get(pull_request_id)
624 target_scm = pull_request.target_repo.scm_instance()
624 target_scm = pull_request.target_repo.scm_instance()
625 target_scm_name = target_scm.name
625 target_scm_name = target_scm.name
626 assert pull_request.is_closed()
626 assert pull_request.is_closed()
627
627
628 # check only the latest status, not the review status
628 # check only the latest status, not the review status
629 status = ChangesetStatusModel().get_status(
629 status = ChangesetStatusModel().get_status(
630 pull_request.source_repo, pull_request=pull_request)
630 pull_request.source_repo, pull_request=pull_request)
631 assert status == ChangesetStatus.STATUS_REJECTED
631 assert status == ChangesetStatus.STATUS_REJECTED
632
632
633 for comment_id in response.json.keys():
633 for comment_id in response.json.keys():
634 test_text = 'test'
634 test_text = 'test'
635 response = self.app.post(
635 response = self.app.post(
636 route_path(
636 route_path(
637 'pullrequest_comment_edit',
637 'pullrequest_comment_edit',
638 repo_name=target_scm_name,
638 repo_name=target_scm_name,
639 pull_request_id=pull_request_id,
639 pull_request_id=pull_request_id,
640 comment_id=comment_id,
640 comment_id=comment_id,
641 ),
641 ),
642 extra_environ=xhr_header,
642 extra_environ=xhr_header,
643 params={
643 params={
644 'csrf_token': csrf_token,
644 'csrf_token': csrf_token,
645 'text': test_text,
645 'text': test_text,
646 },
646 },
647 status=403,
647 status=403,
648 )
648 )
649 assert response.status_int == 403
649 assert response.status_int == 403
650
650
651 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
651 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
652 pull_request = pr_util.create_pull_request()
652 pull_request = pr_util.create_pull_request()
653 target_scm = pull_request.target_repo.scm_instance()
653 target_scm = pull_request.target_repo.scm_instance()
654 target_scm_name = target_scm.name
654 target_scm_name = target_scm.name
655
655
656 response = self.app.post(
656 response = self.app.post(
657 route_path(
657 route_path(
658 'pullrequest_comment_create',
658 'pullrequest_comment_create',
659 repo_name=target_scm_name,
659 repo_name=target_scm_name,
660 pull_request_id=pull_request.pull_request_id),
660 pull_request_id=pull_request.pull_request_id),
661 params={
661 params={
662 'csrf_token': csrf_token,
662 'csrf_token': csrf_token,
663 'text': 'init',
663 'text': 'init',
664 },
664 },
665 extra_environ=xhr_header,
665 extra_environ=xhr_header,
666 )
666 )
667 assert response.json
667 assert response.json
668
668
669 for comment_id in response.json.keys():
669 for comment_id in response.json.keys():
670 assert comment_id
670 assert comment_id
671 test_text = 'test'
671 test_text = 'test'
672 self.app.post(
672 self.app.post(
673 route_path(
673 route_path(
674 'pullrequest_comment_edit',
674 'pullrequest_comment_edit',
675 repo_name=target_scm_name,
675 repo_name=target_scm_name,
676 pull_request_id=pull_request.pull_request_id,
676 pull_request_id=pull_request.pull_request_id,
677 comment_id=comment_id,
677 comment_id=comment_id,
678 ),
678 ),
679 extra_environ=xhr_header,
679 extra_environ=xhr_header,
680 params={
680 params={
681 'csrf_token': csrf_token,
681 'csrf_token': csrf_token,
682 'text': test_text,
682 'text': test_text,
683 'version': '0',
683 'version': '0',
684 },
684 },
685
685
686 )
686 )
687 text_form_db = ChangesetComment.query().filter(
687 text_form_db = ChangesetComment.query().filter(
688 ChangesetComment.comment_id == comment_id).first().text
688 ChangesetComment.comment_id == comment_id).first().text
689 assert test_text == text_form_db
689 assert test_text == text_form_db
690
690
691 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
691 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
692 pull_request = pr_util.create_pull_request()
692 pull_request = pr_util.create_pull_request()
693 target_scm = pull_request.target_repo.scm_instance()
693 target_scm = pull_request.target_repo.scm_instance()
694 target_scm_name = target_scm.name
694 target_scm_name = target_scm.name
695
695
696 response = self.app.post(
696 response = self.app.post(
697 route_path(
697 route_path(
698 'pullrequest_comment_create',
698 'pullrequest_comment_create',
699 repo_name=target_scm_name,
699 repo_name=target_scm_name,
700 pull_request_id=pull_request.pull_request_id),
700 pull_request_id=pull_request.pull_request_id),
701 params={
701 params={
702 'csrf_token': csrf_token,
702 'csrf_token': csrf_token,
703 'text': 'init',
703 'text': 'init',
704 },
704 },
705 extra_environ=xhr_header,
705 extra_environ=xhr_header,
706 )
706 )
707 assert response.json
707 assert response.json
708
708
709 for comment_id in response.json.keys():
709 for comment_id in response.json.keys():
710 test_text = 'init'
710 test_text = 'init'
711 response = self.app.post(
711 response = self.app.post(
712 route_path(
712 route_path(
713 'pullrequest_comment_edit',
713 'pullrequest_comment_edit',
714 repo_name=target_scm_name,
714 repo_name=target_scm_name,
715 pull_request_id=pull_request.pull_request_id,
715 pull_request_id=pull_request.pull_request_id,
716 comment_id=comment_id,
716 comment_id=comment_id,
717 ),
717 ),
718 extra_environ=xhr_header,
718 extra_environ=xhr_header,
719 params={
719 params={
720 'csrf_token': csrf_token,
720 'csrf_token': csrf_token,
721 'text': test_text,
721 'text': test_text,
722 'version': '0',
722 'version': '0',
723 },
723 },
724 status=404,
724 status=404,
725
725
726 )
726 )
727 assert response.status_int == 404
727 assert response.status_int == 404
728
728
729 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
729 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
730 pull_request = pr_util.create_pull_request()
730 pull_request = pr_util.create_pull_request()
731 target_scm = pull_request.target_repo.scm_instance()
731 target_scm = pull_request.target_repo.scm_instance()
732 target_scm_name = target_scm.name
732 target_scm_name = target_scm.name
733
733
734 response = self.app.post(
734 response = self.app.post(
735 route_path(
735 route_path(
736 'pullrequest_comment_create',
736 'pullrequest_comment_create',
737 repo_name=target_scm_name,
737 repo_name=target_scm_name,
738 pull_request_id=pull_request.pull_request_id),
738 pull_request_id=pull_request.pull_request_id),
739 params={
739 params={
740 'csrf_token': csrf_token,
740 'csrf_token': csrf_token,
741 'text': 'init',
741 'text': 'init',
742 },
742 },
743 extra_environ=xhr_header,
743 extra_environ=xhr_header,
744 )
744 )
745 assert response.json
745 assert response.json
746 for comment_id in response.json.keys():
746 for comment_id in response.json.keys():
747 test_text = 'test'
747 test_text = 'test'
748 self.app.post(
748 self.app.post(
749 route_path(
749 route_path(
750 'pullrequest_comment_edit',
750 'pullrequest_comment_edit',
751 repo_name=target_scm_name,
751 repo_name=target_scm_name,
752 pull_request_id=pull_request.pull_request_id,
752 pull_request_id=pull_request.pull_request_id,
753 comment_id=comment_id,
753 comment_id=comment_id,
754 ),
754 ),
755 extra_environ=xhr_header,
755 extra_environ=xhr_header,
756 params={
756 params={
757 'csrf_token': csrf_token,
757 'csrf_token': csrf_token,
758 'text': test_text,
758 'text': test_text,
759 'version': '0',
759 'version': '0',
760 },
760 },
761
761
762 )
762 )
763 test_text_v2 = 'test_v2'
763 test_text_v2 = 'test_v2'
764 response = self.app.post(
764 response = self.app.post(
765 route_path(
765 route_path(
766 'pullrequest_comment_edit',
766 'pullrequest_comment_edit',
767 repo_name=target_scm_name,
767 repo_name=target_scm_name,
768 pull_request_id=pull_request.pull_request_id,
768 pull_request_id=pull_request.pull_request_id,
769 comment_id=comment_id,
769 comment_id=comment_id,
770 ),
770 ),
771 extra_environ=xhr_header,
771 extra_environ=xhr_header,
772 params={
772 params={
773 'csrf_token': csrf_token,
773 'csrf_token': csrf_token,
774 'text': test_text_v2,
774 'text': test_text_v2,
775 'version': '0',
775 'version': '0',
776 },
776 },
777 status=409,
777 status=409,
778 )
778 )
779 assert response.status_int == 409
779 assert response.status_int == 409
780
780
781 text_form_db = ChangesetComment.query().filter(
781 text_form_db = ChangesetComment.query().filter(
782 ChangesetComment.comment_id == comment_id).first().text
782 ChangesetComment.comment_id == comment_id).first().text
783
783
784 assert test_text == text_form_db
784 assert test_text == text_form_db
785 assert test_text_v2 != text_form_db
785 assert test_text_v2 != text_form_db
786
786
787 def test_comment_and_comment_edit_permissions_forbidden(
787 def test_comment_and_comment_edit_permissions_forbidden(
788 self, autologin_regular_user, user_regular, user_admin, pr_util,
788 self, autologin_regular_user, user_regular, user_admin, pr_util,
789 csrf_token, xhr_header):
789 csrf_token, xhr_header):
790 pull_request = pr_util.create_pull_request(
790 pull_request = pr_util.create_pull_request(
791 author=user_admin.username, enable_notifications=False)
791 author=user_admin.username, enable_notifications=False)
792 comment = CommentsModel().create(
792 comment = CommentsModel().create(
793 text='test',
793 text='test',
794 repo=pull_request.target_repo.scm_instance().name,
794 repo=pull_request.target_repo.scm_instance().name,
795 user=user_admin,
795 user=user_admin,
796 pull_request=pull_request,
796 pull_request=pull_request,
797 )
797 )
798 response = self.app.post(
798 response = self.app.post(
799 route_path(
799 route_path(
800 'pullrequest_comment_edit',
800 'pullrequest_comment_edit',
801 repo_name=pull_request.target_repo.scm_instance().name,
801 repo_name=pull_request.target_repo.scm_instance().name,
802 pull_request_id=pull_request.pull_request_id,
802 pull_request_id=pull_request.pull_request_id,
803 comment_id=comment.comment_id,
803 comment_id=comment.comment_id,
804 ),
804 ),
805 extra_environ=xhr_header,
805 extra_environ=xhr_header,
806 params={
806 params={
807 'csrf_token': csrf_token,
807 'csrf_token': csrf_token,
808 'text': 'test_text',
808 'text': 'test_text',
809 },
809 },
810 status=403,
810 status=403,
811 )
811 )
812 assert response.status_int == 403
812 assert response.status_int == 403
813
813
814 def test_create_pull_request(self, backend, csrf_token):
814 def test_create_pull_request(self, backend, csrf_token):
815 commits = [
815 commits = [
816 {'message': 'ancestor'},
816 {'message': 'ancestor'},
817 {'message': 'change'},
817 {'message': 'change'},
818 {'message': 'change2'},
818 {'message': 'change2'},
819 ]
819 ]
820 commit_ids = backend.create_master_repo(commits)
820 commit_ids = backend.create_master_repo(commits)
821 target = backend.create_repo(heads=['ancestor'])
821 target = backend.create_repo(heads=['ancestor'])
822 source = backend.create_repo(heads=['change2'])
822 source = backend.create_repo(heads=['change2'])
823
823
824 response = self.app.post(
824 response = self.app.post(
825 route_path('pullrequest_create', repo_name=source.repo_name),
825 route_path('pullrequest_create', repo_name=source.repo_name),
826 [
826 [
827 ('source_repo', source.repo_name),
827 ('source_repo', source.repo_name),
828 ('source_ref', 'branch:default:' + commit_ids['change2']),
828 ('source_ref', 'branch:default:' + commit_ids['change2']),
829 ('target_repo', target.repo_name),
829 ('target_repo', target.repo_name),
830 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
830 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
831 ('common_ancestor', commit_ids['ancestor']),
831 ('common_ancestor', commit_ids['ancestor']),
832 ('pullrequest_title', 'Title'),
832 ('pullrequest_title', 'Title'),
833 ('pullrequest_desc', 'Description'),
833 ('pullrequest_desc', 'Description'),
834 ('description_renderer', 'markdown'),
834 ('description_renderer', 'markdown'),
835 ('__start__', 'review_members:sequence'),
835 ('__start__', 'review_members:sequence'),
836 ('__start__', 'reviewer:mapping'),
836 ('__start__', 'reviewer:mapping'),
837 ('user_id', '1'),
837 ('user_id', '1'),
838 ('__start__', 'reasons:sequence'),
838 ('__start__', 'reasons:sequence'),
839 ('reason', 'Some reason'),
839 ('reason', 'Some reason'),
840 ('__end__', 'reasons:sequence'),
840 ('__end__', 'reasons:sequence'),
841 ('__start__', 'rules:sequence'),
841 ('__start__', 'rules:sequence'),
842 ('__end__', 'rules:sequence'),
842 ('__end__', 'rules:sequence'),
843 ('mandatory', 'False'),
843 ('mandatory', 'False'),
844 ('__end__', 'reviewer:mapping'),
844 ('__end__', 'reviewer:mapping'),
845 ('__end__', 'review_members:sequence'),
845 ('__end__', 'review_members:sequence'),
846 ('__start__', 'revisions:sequence'),
846 ('__start__', 'revisions:sequence'),
847 ('revisions', commit_ids['change']),
847 ('revisions', commit_ids['change']),
848 ('revisions', commit_ids['change2']),
848 ('revisions', commit_ids['change2']),
849 ('__end__', 'revisions:sequence'),
849 ('__end__', 'revisions:sequence'),
850 ('user', ''),
850 ('user', ''),
851 ('csrf_token', csrf_token),
851 ('csrf_token', csrf_token),
852 ],
852 ],
853 status=302)
853 status=302)
854
854
855 location = response.headers['Location']
855 location = response.headers['Location']
856 pull_request_id = location.rsplit('/', 1)[1]
856 pull_request_id = location.rsplit('/', 1)[1]
857 assert pull_request_id != 'new'
857 assert pull_request_id != 'new'
858 pull_request = PullRequest.get(int(pull_request_id))
858 pull_request = PullRequest.get(int(pull_request_id))
859
859
860 # check that we have now both revisions
860 # check that we have now both revisions
861 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
861 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
862 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
862 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
863 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
863 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
864 assert pull_request.target_ref == expected_target_ref
864 assert pull_request.target_ref == expected_target_ref
865
865
866 def test_reviewer_notifications(self, backend, csrf_token):
866 def test_reviewer_notifications(self, backend, csrf_token):
867 # We have to use the app.post for this test so it will create the
867 # We have to use the app.post for this test so it will create the
868 # notifications properly with the new PR
868 # notifications properly with the new PR
869 commits = [
869 commits = [
870 {'message': 'ancestor',
870 {'message': 'ancestor',
871 'added': [FileNode('file_A', content='content_of_ancestor')]},
871 'added': [FileNode('file_A', content='content_of_ancestor')]},
872 {'message': 'change',
872 {'message': 'change',
873 'added': [FileNode('file_a', content='content_of_change')]},
873 'added': [FileNode('file_a', content='content_of_change')]},
874 {'message': 'change-child'},
874 {'message': 'change-child'},
875 {'message': 'ancestor-child', 'parents': ['ancestor'],
875 {'message': 'ancestor-child', 'parents': ['ancestor'],
876 'added': [
876 'added': [
877 FileNode('file_B', content='content_of_ancestor_child')]},
877 FileNode('file_B', content='content_of_ancestor_child')]},
878 {'message': 'ancestor-child-2'},
878 {'message': 'ancestor-child-2'},
879 ]
879 ]
880 commit_ids = backend.create_master_repo(commits)
880 commit_ids = backend.create_master_repo(commits)
881 target = backend.create_repo(heads=['ancestor-child'])
881 target = backend.create_repo(heads=['ancestor-child'])
882 source = backend.create_repo(heads=['change'])
882 source = backend.create_repo(heads=['change'])
883
883
884 response = self.app.post(
884 response = self.app.post(
885 route_path('pullrequest_create', repo_name=source.repo_name),
885 route_path('pullrequest_create', repo_name=source.repo_name),
886 [
886 [
887 ('source_repo', source.repo_name),
887 ('source_repo', source.repo_name),
888 ('source_ref', 'branch:default:' + commit_ids['change']),
888 ('source_ref', 'branch:default:' + commit_ids['change']),
889 ('target_repo', target.repo_name),
889 ('target_repo', target.repo_name),
890 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
890 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
891 ('common_ancestor', commit_ids['ancestor']),
891 ('common_ancestor', commit_ids['ancestor']),
892 ('pullrequest_title', 'Title'),
892 ('pullrequest_title', 'Title'),
893 ('pullrequest_desc', 'Description'),
893 ('pullrequest_desc', 'Description'),
894 ('description_renderer', 'markdown'),
894 ('description_renderer', 'markdown'),
895 ('__start__', 'review_members:sequence'),
895 ('__start__', 'review_members:sequence'),
896 ('__start__', 'reviewer:mapping'),
896 ('__start__', 'reviewer:mapping'),
897 ('user_id', '2'),
897 ('user_id', '2'),
898 ('__start__', 'reasons:sequence'),
898 ('__start__', 'reasons:sequence'),
899 ('reason', 'Some reason'),
899 ('reason', 'Some reason'),
900 ('__end__', 'reasons:sequence'),
900 ('__end__', 'reasons:sequence'),
901 ('__start__', 'rules:sequence'),
901 ('__start__', 'rules:sequence'),
902 ('__end__', 'rules:sequence'),
902 ('__end__', 'rules:sequence'),
903 ('mandatory', 'False'),
903 ('mandatory', 'False'),
904 ('__end__', 'reviewer:mapping'),
904 ('__end__', 'reviewer:mapping'),
905 ('__end__', 'review_members:sequence'),
905 ('__end__', 'review_members:sequence'),
906 ('__start__', 'revisions:sequence'),
906 ('__start__', 'revisions:sequence'),
907 ('revisions', commit_ids['change']),
907 ('revisions', commit_ids['change']),
908 ('__end__', 'revisions:sequence'),
908 ('__end__', 'revisions:sequence'),
909 ('user', ''),
909 ('user', ''),
910 ('csrf_token', csrf_token),
910 ('csrf_token', csrf_token),
911 ],
911 ],
912 status=302)
912 status=302)
913
913
914 location = response.headers['Location']
914 location = response.headers['Location']
915
915
916 pull_request_id = location.rsplit('/', 1)[1]
916 pull_request_id = location.rsplit('/', 1)[1]
917 assert pull_request_id != 'new'
917 assert pull_request_id != 'new'
918 pull_request = PullRequest.get(int(pull_request_id))
918 pull_request = PullRequest.get(int(pull_request_id))
919
919
920 # Check that a notification was made
920 # Check that a notification was made
921 notifications = Notification.query()\
921 notifications = Notification.query()\
922 .filter(Notification.created_by == pull_request.author.user_id,
922 .filter(Notification.created_by == pull_request.author.user_id,
923 Notification.type_ == Notification.TYPE_PULL_REQUEST,
923 Notification.type_ == Notification.TYPE_PULL_REQUEST,
924 Notification.subject.contains(
924 Notification.subject.contains(
925 "requested a pull request review. !%s" % pull_request_id))
925 "requested a pull request review. !%s" % pull_request_id))
926 assert len(notifications.all()) == 1
926 assert len(notifications.all()) == 1
927
927
928 # Change reviewers and check that a notification was made
928 # Change reviewers and check that a notification was made
929 PullRequestModel().update_reviewers(
929 PullRequestModel().update_reviewers(
930 pull_request.pull_request_id, [
930 pull_request.pull_request_id, [
931 (1, [], False, 'reviewer', [])
931 (1, [], False, 'reviewer', [])
932 ],
932 ],
933 pull_request.author)
933 pull_request.author)
934 assert len(notifications.all()) == 2
934 assert len(notifications.all()) == 2
935
935
936 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
936 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
937 commits = [
937 commits = [
938 {'message': 'ancestor',
938 {'message': 'ancestor',
939 'added': [FileNode('file_A', content='content_of_ancestor')]},
939 'added': [FileNode('file_A', content='content_of_ancestor')]},
940 {'message': 'change',
940 {'message': 'change',
941 'added': [FileNode('file_a', content='content_of_change')]},
941 'added': [FileNode('file_a', content='content_of_change')]},
942 {'message': 'change-child'},
942 {'message': 'change-child'},
943 {'message': 'ancestor-child', 'parents': ['ancestor'],
943 {'message': 'ancestor-child', 'parents': ['ancestor'],
944 'added': [
944 'added': [
945 FileNode('file_B', content='content_of_ancestor_child')]},
945 FileNode('file_B', content='content_of_ancestor_child')]},
946 {'message': 'ancestor-child-2'},
946 {'message': 'ancestor-child-2'},
947 ]
947 ]
948 commit_ids = backend.create_master_repo(commits)
948 commit_ids = backend.create_master_repo(commits)
949 target = backend.create_repo(heads=['ancestor-child'])
949 target = backend.create_repo(heads=['ancestor-child'])
950 source = backend.create_repo(heads=['change'])
950 source = backend.create_repo(heads=['change'])
951
951
952 response = self.app.post(
952 response = self.app.post(
953 route_path('pullrequest_create', repo_name=source.repo_name),
953 route_path('pullrequest_create', repo_name=source.repo_name),
954 [
954 [
955 ('source_repo', source.repo_name),
955 ('source_repo', source.repo_name),
956 ('source_ref', 'branch:default:' + commit_ids['change']),
956 ('source_ref', 'branch:default:' + commit_ids['change']),
957 ('target_repo', target.repo_name),
957 ('target_repo', target.repo_name),
958 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
958 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
959 ('common_ancestor', commit_ids['ancestor']),
959 ('common_ancestor', commit_ids['ancestor']),
960 ('pullrequest_title', 'Title'),
960 ('pullrequest_title', 'Title'),
961 ('pullrequest_desc', 'Description'),
961 ('pullrequest_desc', 'Description'),
962 ('description_renderer', 'markdown'),
962 ('description_renderer', 'markdown'),
963 ('__start__', 'review_members:sequence'),
963 ('__start__', 'review_members:sequence'),
964 ('__start__', 'reviewer:mapping'),
964 ('__start__', 'reviewer:mapping'),
965 ('user_id', '1'),
965 ('user_id', '1'),
966 ('__start__', 'reasons:sequence'),
966 ('__start__', 'reasons:sequence'),
967 ('reason', 'Some reason'),
967 ('reason', 'Some reason'),
968 ('__end__', 'reasons:sequence'),
968 ('__end__', 'reasons:sequence'),
969 ('__start__', 'rules:sequence'),
969 ('__start__', 'rules:sequence'),
970 ('__end__', 'rules:sequence'),
970 ('__end__', 'rules:sequence'),
971 ('mandatory', 'False'),
971 ('mandatory', 'False'),
972 ('__end__', 'reviewer:mapping'),
972 ('__end__', 'reviewer:mapping'),
973 ('__end__', 'review_members:sequence'),
973 ('__end__', 'review_members:sequence'),
974 ('__start__', 'revisions:sequence'),
974 ('__start__', 'revisions:sequence'),
975 ('revisions', commit_ids['change']),
975 ('revisions', commit_ids['change']),
976 ('__end__', 'revisions:sequence'),
976 ('__end__', 'revisions:sequence'),
977 ('user', ''),
977 ('user', ''),
978 ('csrf_token', csrf_token),
978 ('csrf_token', csrf_token),
979 ],
979 ],
980 status=302)
980 status=302)
981
981
982 location = response.headers['Location']
982 location = response.headers['Location']
983
983
984 pull_request_id = location.rsplit('/', 1)[1]
984 pull_request_id = location.rsplit('/', 1)[1]
985 assert pull_request_id != 'new'
985 assert pull_request_id != 'new'
986 pull_request = PullRequest.get(int(pull_request_id))
986 pull_request = PullRequest.get(int(pull_request_id))
987
987
988 # target_ref has to point to the ancestor's commit_id in order to
988 # target_ref has to point to the ancestor's commit_id in order to
989 # show the correct diff
989 # show the correct diff
990 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
990 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
991 assert pull_request.target_ref == expected_target_ref
991 assert pull_request.target_ref == expected_target_ref
992
992
993 # Check generated diff contents
993 # Check generated diff contents
994 response = response.follow()
994 response = response.follow()
995 response.mustcontain(no=['content_of_ancestor'])
995 response.mustcontain(no=['content_of_ancestor'])
996 response.mustcontain(no=['content_of_ancestor-child'])
996 response.mustcontain(no=['content_of_ancestor-child'])
997 response.mustcontain('content_of_change')
997 response.mustcontain('content_of_change')
998
998
999 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
999 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
1000 # Clear any previous calls to rcextensions
1000 # Clear any previous calls to rcextensions
1001 rhodecode.EXTENSIONS.calls.clear()
1001 rhodecode.EXTENSIONS.calls.clear()
1002
1002
1003 pull_request = pr_util.create_pull_request(
1003 pull_request = pr_util.create_pull_request(
1004 approved=True, mergeable=True)
1004 approved=True, mergeable=True)
1005 pull_request_id = pull_request.pull_request_id
1005 pull_request_id = pull_request.pull_request_id
1006 repo_name = pull_request.target_repo.scm_instance().name,
1006 repo_name = pull_request.target_repo.scm_instance().name,
1007
1007
1008 url = route_path('pullrequest_merge',
1008 url = route_path('pullrequest_merge',
1009 repo_name=str(repo_name[0]),
1009 repo_name=str(repo_name[0]),
1010 pull_request_id=pull_request_id)
1010 pull_request_id=pull_request_id)
1011 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
1011 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
1012
1012
1013 pull_request = PullRequest.get(pull_request_id)
1013 pull_request = PullRequest.get(pull_request_id)
1014
1014
1015 assert response.status_int == 200
1015 assert response.status_int == 200
1016 assert pull_request.is_closed()
1016 assert pull_request.is_closed()
1017 assert_pull_request_status(
1017 assert_pull_request_status(
1018 pull_request, ChangesetStatus.STATUS_APPROVED)
1018 pull_request, ChangesetStatus.STATUS_APPROVED)
1019
1019
1020 # Check the relevant log entries were added
1020 # Check the relevant log entries were added
1021 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1021 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1022 actions = [log.action for log in user_logs]
1022 actions = [log.action for log in user_logs]
1023 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1023 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1024 expected_actions = [
1024 expected_actions = [
1025 u'repo.pull_request.close',
1025 u'repo.pull_request.close',
1026 u'repo.pull_request.merge',
1026 u'repo.pull_request.merge',
1027 u'repo.pull_request.comment.create'
1027 u'repo.pull_request.comment.create'
1028 ]
1028 ]
1029 assert actions == expected_actions
1029 assert actions == expected_actions
1030
1030
1031 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1031 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1032 actions = [log for log in user_logs]
1032 actions = [log for log in user_logs]
1033 assert actions[-1].action == 'user.push'
1033 assert actions[-1].action == 'user.push'
1034 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1034 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1035
1035
1036 # Check post_push rcextension was really executed
1036 # Check post_push rcextension was really executed
1037 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1037 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1038 assert len(push_calls) == 1
1038 assert len(push_calls) == 1
1039 unused_last_call_args, last_call_kwargs = push_calls[0]
1039 unused_last_call_args, last_call_kwargs = push_calls[0]
1040 assert last_call_kwargs['action'] == 'push'
1040 assert last_call_kwargs['action'] == 'push'
1041 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1041 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1042
1042
1043 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1043 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1044 pull_request = pr_util.create_pull_request(mergeable=False)
1044 pull_request = pr_util.create_pull_request(mergeable=False)
1045 pull_request_id = pull_request.pull_request_id
1045 pull_request_id = pull_request.pull_request_id
1046 pull_request = PullRequest.get(pull_request_id)
1046 pull_request = PullRequest.get(pull_request_id)
1047
1047
1048 response = self.app.post(
1048 response = self.app.post(
1049 route_path('pullrequest_merge',
1049 route_path('pullrequest_merge',
1050 repo_name=pull_request.target_repo.scm_instance().name,
1050 repo_name=pull_request.target_repo.scm_instance().name,
1051 pull_request_id=pull_request.pull_request_id),
1051 pull_request_id=pull_request.pull_request_id),
1052 params={'csrf_token': csrf_token}).follow()
1052 params={'csrf_token': csrf_token}).follow()
1053
1053
1054 assert response.status_int == 200
1054 assert response.status_int == 200
1055 response.mustcontain(
1055 response.mustcontain(
1056 'Merge is not currently possible because of below failed checks.')
1056 'Merge is not currently possible because of below failed checks.')
1057 response.mustcontain('Server-side pull request merging is disabled.')
1057 response.mustcontain('Server-side pull request merging is disabled.')
1058
1058
1059 @pytest.mark.skip_backends('svn')
1059 @pytest.mark.skip_backends('svn')
1060 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1060 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1061 pull_request = pr_util.create_pull_request(mergeable=True)
1061 pull_request = pr_util.create_pull_request(mergeable=True)
1062 pull_request_id = pull_request.pull_request_id
1062 pull_request_id = pull_request.pull_request_id
1063 repo_name = pull_request.target_repo.scm_instance().name
1063 repo_name = pull_request.target_repo.scm_instance().name
1064
1064
1065 response = self.app.post(
1065 response = self.app.post(
1066 route_path('pullrequest_merge',
1066 route_path('pullrequest_merge',
1067 repo_name=repo_name, pull_request_id=pull_request_id),
1067 repo_name=repo_name, pull_request_id=pull_request_id),
1068 params={'csrf_token': csrf_token}).follow()
1068 params={'csrf_token': csrf_token}).follow()
1069
1069
1070 assert response.status_int == 200
1070 assert response.status_int == 200
1071
1071
1072 response.mustcontain(
1072 response.mustcontain(
1073 'Merge is not currently possible because of below failed checks.')
1073 'Merge is not currently possible because of below failed checks.')
1074 response.mustcontain('Pull request reviewer approval is pending.')
1074 response.mustcontain('Pull request reviewer approval is pending.')
1075
1075
1076 def test_merge_pull_request_renders_failure_reason(
1076 def test_merge_pull_request_renders_failure_reason(
1077 self, user_regular, csrf_token, pr_util):
1077 self, user_regular, csrf_token, pr_util):
1078 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1078 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1079 pull_request_id = pull_request.pull_request_id
1079 pull_request_id = pull_request.pull_request_id
1080 repo_name = pull_request.target_repo.scm_instance().name
1080 repo_name = pull_request.target_repo.scm_instance().name
1081
1081
1082 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1082 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1083 MergeFailureReason.PUSH_FAILED,
1083 MergeFailureReason.PUSH_FAILED,
1084 metadata={'target': 'shadow repo',
1084 metadata={'target': 'shadow repo',
1085 'merge_commit': 'xxx'})
1085 'merge_commit': 'xxx'})
1086 model_patcher = mock.patch.multiple(
1086 model_patcher = mock.patch.multiple(
1087 PullRequestModel,
1087 PullRequestModel,
1088 merge_repo=mock.Mock(return_value=merge_resp),
1088 merge_repo=mock.Mock(return_value=merge_resp),
1089 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1089 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1090
1090
1091 with model_patcher:
1091 with model_patcher:
1092 response = self.app.post(
1092 response = self.app.post(
1093 route_path('pullrequest_merge',
1093 route_path('pullrequest_merge',
1094 repo_name=repo_name,
1094 repo_name=repo_name,
1095 pull_request_id=pull_request_id),
1095 pull_request_id=pull_request_id),
1096 params={'csrf_token': csrf_token}, status=302)
1096 params={'csrf_token': csrf_token}, status=302)
1097
1097
1098 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1098 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1099 metadata={'target': 'shadow repo',
1099 metadata={'target': 'shadow repo',
1100 'merge_commit': 'xxx'})
1100 'merge_commit': 'xxx'})
1101 assert_session_flash(response, merge_resp.merge_status_message)
1101 assert_session_flash(response, merge_resp.merge_status_message)
1102
1102
1103 def test_update_source_revision(self, backend, csrf_token):
1103 def test_update_source_revision(self, backend, csrf_token):
1104 commits = [
1104 commits = [
1105 {'message': 'ancestor'},
1105 {'message': 'ancestor'},
1106 {'message': 'change'},
1106 {'message': 'change'},
1107 {'message': 'change-2'},
1107 {'message': 'change-2'},
1108 ]
1108 ]
1109 commit_ids = backend.create_master_repo(commits)
1109 commit_ids = backend.create_master_repo(commits)
1110 target = backend.create_repo(heads=['ancestor'])
1110 target = backend.create_repo(heads=['ancestor'])
1111 source = backend.create_repo(heads=['change'])
1111 source = backend.create_repo(heads=['change'])
1112
1112
1113 # create pr from a in source to A in target
1113 # create pr from a in source to A in target
1114 pull_request = PullRequest()
1114 pull_request = PullRequest()
1115
1115
1116 pull_request.source_repo = source
1116 pull_request.source_repo = source
1117 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1117 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1118 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1118 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1119
1119
1120 pull_request.target_repo = target
1120 pull_request.target_repo = target
1121 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1121 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1122 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1122 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1123
1123
1124 pull_request.revisions = [commit_ids['change']]
1124 pull_request.revisions = [commit_ids['change']]
1125 pull_request.title = u"Test"
1125 pull_request.title = u"Test"
1126 pull_request.description = u"Description"
1126 pull_request.description = u"Description"
1127 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1127 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1128 pull_request.pull_request_state = PullRequest.STATE_CREATED
1128 pull_request.pull_request_state = PullRequest.STATE_CREATED
1129 Session().add(pull_request)
1129 Session().add(pull_request)
1130 Session().commit()
1130 Session().commit()
1131 pull_request_id = pull_request.pull_request_id
1131 pull_request_id = pull_request.pull_request_id
1132
1132
1133 # source has ancestor - change - change-2
1133 # source has ancestor - change - change-2
1134 backend.pull_heads(source, heads=['change-2'])
1134 backend.pull_heads(source, heads=['change-2'])
1135 target_repo_name = target.repo_name
1135 target_repo_name = target.repo_name
1136
1136
1137 # update PR
1137 # update PR
1138 self.app.post(
1138 self.app.post(
1139 route_path('pullrequest_update',
1139 route_path('pullrequest_update',
1140 repo_name=target_repo_name, pull_request_id=pull_request_id),
1140 repo_name=target_repo_name, pull_request_id=pull_request_id),
1141 params={'update_commits': 'true', 'csrf_token': csrf_token})
1141 params={'update_commits': 'true', 'csrf_token': csrf_token})
1142
1142
1143 response = self.app.get(
1143 response = self.app.get(
1144 route_path('pullrequest_show',
1144 route_path('pullrequest_show',
1145 repo_name=target_repo_name,
1145 repo_name=target_repo_name,
1146 pull_request_id=pull_request.pull_request_id))
1146 pull_request_id=pull_request.pull_request_id))
1147
1147
1148 assert response.status_int == 200
1148 assert response.status_int == 200
1149 response.mustcontain('Pull request updated to')
1149 response.mustcontain('Pull request updated to')
1150 response.mustcontain('with 1 added, 0 removed commits.')
1150 response.mustcontain('with 1 added, 0 removed commits.')
1151
1151
1152 # check that we have now both revisions
1152 # check that we have now both revisions
1153 pull_request = PullRequest.get(pull_request_id)
1153 pull_request = PullRequest.get(pull_request_id)
1154 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1154 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1155
1155
1156 def test_update_target_revision(self, backend, csrf_token):
1156 def test_update_target_revision(self, backend, csrf_token):
1157 commits = [
1157 commits = [
1158 {'message': 'ancestor'},
1158 {'message': 'ancestor'},
1159 {'message': 'change'},
1159 {'message': 'change'},
1160 {'message': 'ancestor-new', 'parents': ['ancestor']},
1160 {'message': 'ancestor-new', 'parents': ['ancestor']},
1161 {'message': 'change-rebased'},
1161 {'message': 'change-rebased'},
1162 ]
1162 ]
1163 commit_ids = backend.create_master_repo(commits)
1163 commit_ids = backend.create_master_repo(commits)
1164 target = backend.create_repo(heads=['ancestor'])
1164 target = backend.create_repo(heads=['ancestor'])
1165 source = backend.create_repo(heads=['change'])
1165 source = backend.create_repo(heads=['change'])
1166
1166
1167 # create pr from a in source to A in target
1167 # create pr from a in source to A in target
1168 pull_request = PullRequest()
1168 pull_request = PullRequest()
1169
1169
1170 pull_request.source_repo = source
1170 pull_request.source_repo = source
1171 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1171 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1172 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1172 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1173
1173
1174 pull_request.target_repo = target
1174 pull_request.target_repo = target
1175 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1175 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1176 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1176 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1177
1177
1178 pull_request.revisions = [commit_ids['change']]
1178 pull_request.revisions = [commit_ids['change']]
1179 pull_request.title = u"Test"
1179 pull_request.title = u"Test"
1180 pull_request.description = u"Description"
1180 pull_request.description = u"Description"
1181 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1181 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1182 pull_request.pull_request_state = PullRequest.STATE_CREATED
1182 pull_request.pull_request_state = PullRequest.STATE_CREATED
1183
1183
1184 Session().add(pull_request)
1184 Session().add(pull_request)
1185 Session().commit()
1185 Session().commit()
1186 pull_request_id = pull_request.pull_request_id
1186 pull_request_id = pull_request.pull_request_id
1187
1187
1188 # target has ancestor - ancestor-new
1188 # target has ancestor - ancestor-new
1189 # source has ancestor - ancestor-new - change-rebased
1189 # source has ancestor - ancestor-new - change-rebased
1190 backend.pull_heads(target, heads=['ancestor-new'])
1190 backend.pull_heads(target, heads=['ancestor-new'])
1191 backend.pull_heads(source, heads=['change-rebased'])
1191 backend.pull_heads(source, heads=['change-rebased'])
1192 target_repo_name = target.repo_name
1192 target_repo_name = target.repo_name
1193
1193
1194 # update PR
1194 # update PR
1195 url = route_path('pullrequest_update',
1195 url = route_path('pullrequest_update',
1196 repo_name=target_repo_name,
1196 repo_name=target_repo_name,
1197 pull_request_id=pull_request_id)
1197 pull_request_id=pull_request_id)
1198 self.app.post(url,
1198 self.app.post(url,
1199 params={'update_commits': 'true', 'csrf_token': csrf_token},
1199 params={'update_commits': 'true', 'csrf_token': csrf_token},
1200 status=200)
1200 status=200)
1201
1201
1202 # check that we have now both revisions
1202 # check that we have now both revisions
1203 pull_request = PullRequest.get(pull_request_id)
1203 pull_request = PullRequest.get(pull_request_id)
1204 assert pull_request.revisions == [commit_ids['change-rebased']]
1204 assert pull_request.revisions == [commit_ids['change-rebased']]
1205 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1205 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1206 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1206 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1207
1207
1208 response = self.app.get(
1208 response = self.app.get(
1209 route_path('pullrequest_show',
1209 route_path('pullrequest_show',
1210 repo_name=target_repo_name,
1210 repo_name=target_repo_name,
1211 pull_request_id=pull_request.pull_request_id))
1211 pull_request_id=pull_request.pull_request_id))
1212 assert response.status_int == 200
1212 assert response.status_int == 200
1213 response.mustcontain('Pull request updated to')
1213 response.mustcontain('Pull request updated to')
1214 response.mustcontain('with 1 added, 1 removed commits.')
1214 response.mustcontain('with 1 added, 1 removed commits.')
1215
1215
1216 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1216 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1217 backend = backend_git
1217 backend = backend_git
1218 commits = [
1218 commits = [
1219 {'message': 'master-commit-1'},
1219 {'message': 'master-commit-1'},
1220 {'message': 'master-commit-2-change-1'},
1220 {'message': 'master-commit-2-change-1'},
1221 {'message': 'master-commit-3-change-2'},
1221 {'message': 'master-commit-3-change-2'},
1222
1222
1223 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1223 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1224 {'message': 'feat-commit-2'},
1224 {'message': 'feat-commit-2'},
1225 ]
1225 ]
1226 commit_ids = backend.create_master_repo(commits)
1226 commit_ids = backend.create_master_repo(commits)
1227 target = backend.create_repo(heads=['master-commit-3-change-2'])
1227 target = backend.create_repo(heads=['master-commit-3-change-2'])
1228 source = backend.create_repo(heads=['feat-commit-2'])
1228 source = backend.create_repo(heads=['feat-commit-2'])
1229
1229
1230 # create pr from a in source to A in target
1230 # create pr from a in source to A in target
1231 pull_request = PullRequest()
1231 pull_request = PullRequest()
1232 pull_request.source_repo = source
1232 pull_request.source_repo = source
1233
1233
1234 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1234 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1235 branch=backend.default_branch_name,
1235 branch=backend.default_branch_name,
1236 commit_id=commit_ids['master-commit-3-change-2'])
1236 commit_id=commit_ids['master-commit-3-change-2'])
1237
1237
1238 pull_request.target_repo = target
1238 pull_request.target_repo = target
1239 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1239 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1240 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1240 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1241
1241
1242 pull_request.revisions = [
1242 pull_request.revisions = [
1243 commit_ids['feat-commit-1'],
1243 commit_ids['feat-commit-1'],
1244 commit_ids['feat-commit-2']
1244 commit_ids['feat-commit-2']
1245 ]
1245 ]
1246 pull_request.title = u"Test"
1246 pull_request.title = u"Test"
1247 pull_request.description = u"Description"
1247 pull_request.description = u"Description"
1248 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1248 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1249 pull_request.pull_request_state = PullRequest.STATE_CREATED
1249 pull_request.pull_request_state = PullRequest.STATE_CREATED
1250 Session().add(pull_request)
1250 Session().add(pull_request)
1251 Session().commit()
1251 Session().commit()
1252 pull_request_id = pull_request.pull_request_id
1252 pull_request_id = pull_request.pull_request_id
1253
1253
1254 # PR is created, now we simulate a force-push into target,
1254 # PR is created, now we simulate a force-push into target,
1255 # that drops a 2 last commits
1255 # that drops a 2 last commits
1256 vcsrepo = target.scm_instance()
1256 vcsrepo = target.scm_instance()
1257 vcsrepo.config.clear_section('hooks')
1257 vcsrepo.config.clear_section('hooks')
1258 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1258 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1259 target_repo_name = target.repo_name
1259 target_repo_name = target.repo_name
1260
1260
1261 # update PR
1261 # update PR
1262 url = route_path('pullrequest_update',
1262 url = route_path('pullrequest_update',
1263 repo_name=target_repo_name,
1263 repo_name=target_repo_name,
1264 pull_request_id=pull_request_id)
1264 pull_request_id=pull_request_id)
1265 self.app.post(url,
1265 self.app.post(url,
1266 params={'update_commits': 'true', 'csrf_token': csrf_token},
1266 params={'update_commits': 'true', 'csrf_token': csrf_token},
1267 status=200)
1267 status=200)
1268
1268
1269 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1269 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1270 assert response.status_int == 200
1270 assert response.status_int == 200
1271 response.mustcontain('Pull request updated to')
1271 response.mustcontain('Pull request updated to')
1272 response.mustcontain('with 0 added, 0 removed commits.')
1272 response.mustcontain('with 0 added, 0 removed commits.')
1273
1273
1274 def test_update_of_ancestor_reference(self, backend, csrf_token):
1274 def test_update_of_ancestor_reference(self, backend, csrf_token):
1275 commits = [
1275 commits = [
1276 {'message': 'ancestor'},
1276 {'message': 'ancestor'},
1277 {'message': 'change'},
1277 {'message': 'change'},
1278 {'message': 'change-2'},
1278 {'message': 'change-2'},
1279 {'message': 'ancestor-new', 'parents': ['ancestor']},
1279 {'message': 'ancestor-new', 'parents': ['ancestor']},
1280 {'message': 'change-rebased'},
1280 {'message': 'change-rebased'},
1281 ]
1281 ]
1282 commit_ids = backend.create_master_repo(commits)
1282 commit_ids = backend.create_master_repo(commits)
1283 target = backend.create_repo(heads=['ancestor'])
1283 target = backend.create_repo(heads=['ancestor'])
1284 source = backend.create_repo(heads=['change'])
1284 source = backend.create_repo(heads=['change'])
1285
1285
1286 # create pr from a in source to A in target
1286 # create pr from a in source to A in target
1287 pull_request = PullRequest()
1287 pull_request = PullRequest()
1288 pull_request.source_repo = source
1288 pull_request.source_repo = source
1289
1289
1290 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1290 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1291 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1291 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1292 pull_request.target_repo = target
1292 pull_request.target_repo = target
1293 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1293 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1294 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1294 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1295 pull_request.revisions = [commit_ids['change']]
1295 pull_request.revisions = [commit_ids['change']]
1296 pull_request.title = u"Test"
1296 pull_request.title = u"Test"
1297 pull_request.description = u"Description"
1297 pull_request.description = u"Description"
1298 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1298 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1299 pull_request.pull_request_state = PullRequest.STATE_CREATED
1299 pull_request.pull_request_state = PullRequest.STATE_CREATED
1300 Session().add(pull_request)
1300 Session().add(pull_request)
1301 Session().commit()
1301 Session().commit()
1302 pull_request_id = pull_request.pull_request_id
1302 pull_request_id = pull_request.pull_request_id
1303
1303
1304 # target has ancestor - ancestor-new
1304 # target has ancestor - ancestor-new
1305 # source has ancestor - ancestor-new - change-rebased
1305 # source has ancestor - ancestor-new - change-rebased
1306 backend.pull_heads(target, heads=['ancestor-new'])
1306 backend.pull_heads(target, heads=['ancestor-new'])
1307 backend.pull_heads(source, heads=['change-rebased'])
1307 backend.pull_heads(source, heads=['change-rebased'])
1308 target_repo_name = target.repo_name
1308 target_repo_name = target.repo_name
1309
1309
1310 # update PR
1310 # update PR
1311 self.app.post(
1311 self.app.post(
1312 route_path('pullrequest_update',
1312 route_path('pullrequest_update',
1313 repo_name=target_repo_name, pull_request_id=pull_request_id),
1313 repo_name=target_repo_name, pull_request_id=pull_request_id),
1314 params={'update_commits': 'true', 'csrf_token': csrf_token},
1314 params={'update_commits': 'true', 'csrf_token': csrf_token},
1315 status=200)
1315 status=200)
1316
1316
1317 # Expect the target reference to be updated correctly
1317 # Expect the target reference to be updated correctly
1318 pull_request = PullRequest.get(pull_request_id)
1318 pull_request = PullRequest.get(pull_request_id)
1319 assert pull_request.revisions == [commit_ids['change-rebased']]
1319 assert pull_request.revisions == [commit_ids['change-rebased']]
1320 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1320 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1321 branch=backend.default_branch_name,
1321 branch=backend.default_branch_name,
1322 commit_id=commit_ids['ancestor-new'])
1322 commit_id=commit_ids['ancestor-new'])
1323 assert pull_request.target_ref == expected_target_ref
1323 assert pull_request.target_ref == expected_target_ref
1324
1324
1325 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1325 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1326 branch_name = 'development'
1326 branch_name = 'development'
1327 commits = [
1327 commits = [
1328 {'message': 'initial-commit'},
1328 {'message': 'initial-commit'},
1329 {'message': 'old-feature'},
1329 {'message': 'old-feature'},
1330 {'message': 'new-feature', 'branch': branch_name},
1330 {'message': 'new-feature', 'branch': branch_name},
1331 ]
1331 ]
1332 repo = backend_git.create_repo(commits)
1332 repo = backend_git.create_repo(commits)
1333 repo_name = repo.repo_name
1333 repo_name = repo.repo_name
1334 commit_ids = backend_git.commit_ids
1334 commit_ids = backend_git.commit_ids
1335
1335
1336 pull_request = PullRequest()
1336 pull_request = PullRequest()
1337 pull_request.source_repo = repo
1337 pull_request.source_repo = repo
1338 pull_request.target_repo = repo
1338 pull_request.target_repo = repo
1339 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1339 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1340 branch=branch_name, commit_id=commit_ids['new-feature'])
1340 branch=branch_name, commit_id=commit_ids['new-feature'])
1341 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1341 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1342 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1342 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1343 pull_request.revisions = [commit_ids['new-feature']]
1343 pull_request.revisions = [commit_ids['new-feature']]
1344 pull_request.title = u"Test"
1344 pull_request.title = u"Test"
1345 pull_request.description = u"Description"
1345 pull_request.description = u"Description"
1346 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1346 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1347 pull_request.pull_request_state = PullRequest.STATE_CREATED
1347 pull_request.pull_request_state = PullRequest.STATE_CREATED
1348 Session().add(pull_request)
1348 Session().add(pull_request)
1349 Session().commit()
1349 Session().commit()
1350
1350
1351 pull_request_id = pull_request.pull_request_id
1351 pull_request_id = pull_request.pull_request_id
1352
1352
1353 vcs = repo.scm_instance()
1353 vcs = repo.scm_instance()
1354 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1354 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1355 # NOTE(marcink): run GC to ensure the commits are gone
1355 # NOTE(marcink): run GC to ensure the commits are gone
1356 vcs.run_gc()
1356 vcs.run_gc()
1357
1357
1358 response = self.app.get(route_path(
1358 response = self.app.get(route_path(
1359 'pullrequest_show',
1359 'pullrequest_show',
1360 repo_name=repo_name,
1360 repo_name=repo_name,
1361 pull_request_id=pull_request_id))
1361 pull_request_id=pull_request_id))
1362
1362
1363 assert response.status_int == 200
1363 assert response.status_int == 200
1364
1364
1365 response.assert_response().element_contains(
1365 response.assert_response().element_contains(
1366 '#changeset_compare_view_content .alert strong',
1366 '#changeset_compare_view_content .alert strong',
1367 'Missing commits')
1367 'Missing commits')
1368 response.assert_response().element_contains(
1368 response.assert_response().element_contains(
1369 '#changeset_compare_view_content .alert',
1369 '#changeset_compare_view_content .alert',
1370 'This pull request cannot be displayed, because one or more'
1370 'This pull request cannot be displayed, because one or more'
1371 ' commits no longer exist in the source repository.')
1371 ' commits no longer exist in the source repository.')
1372
1372
1373 def test_strip_commits_from_pull_request(
1373 def test_strip_commits_from_pull_request(
1374 self, backend, pr_util, csrf_token):
1374 self, backend, pr_util, csrf_token):
1375 commits = [
1375 commits = [
1376 {'message': 'initial-commit'},
1376 {'message': 'initial-commit'},
1377 {'message': 'old-feature'},
1377 {'message': 'old-feature'},
1378 {'message': 'new-feature', 'parents': ['initial-commit']},
1378 {'message': 'new-feature', 'parents': ['initial-commit']},
1379 ]
1379 ]
1380 pull_request = pr_util.create_pull_request(
1380 pull_request = pr_util.create_pull_request(
1381 commits, target_head='initial-commit', source_head='new-feature',
1381 commits, target_head='initial-commit', source_head='new-feature',
1382 revisions=['new-feature'])
1382 revisions=['new-feature'])
1383
1383
1384 vcs = pr_util.source_repository.scm_instance()
1384 vcs = pr_util.source_repository.scm_instance()
1385 if backend.alias == 'git':
1385 if backend.alias == 'git':
1386 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1386 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1387 else:
1387 else:
1388 vcs.strip(pr_util.commit_ids['new-feature'])
1388 vcs.strip(pr_util.commit_ids['new-feature'])
1389
1389
1390 response = self.app.get(route_path(
1390 response = self.app.get(route_path(
1391 'pullrequest_show',
1391 'pullrequest_show',
1392 repo_name=pr_util.target_repository.repo_name,
1392 repo_name=pr_util.target_repository.repo_name,
1393 pull_request_id=pull_request.pull_request_id))
1393 pull_request_id=pull_request.pull_request_id))
1394
1394
1395 assert response.status_int == 200
1395 assert response.status_int == 200
1396
1396
1397 response.assert_response().element_contains(
1397 response.assert_response().element_contains(
1398 '#changeset_compare_view_content .alert strong',
1398 '#changeset_compare_view_content .alert strong',
1399 'Missing commits')
1399 'Missing commits')
1400 response.assert_response().element_contains(
1400 response.assert_response().element_contains(
1401 '#changeset_compare_view_content .alert',
1401 '#changeset_compare_view_content .alert',
1402 'This pull request cannot be displayed, because one or more'
1402 'This pull request cannot be displayed, because one or more'
1403 ' commits no longer exist in the source repository.')
1403 ' commits no longer exist in the source repository.')
1404 response.assert_response().element_contains(
1404 response.assert_response().element_contains(
1405 '#update_commits',
1405 '#update_commits',
1406 'Update commits')
1406 'Update commits')
1407
1407
1408 def test_strip_commits_and_update(
1408 def test_strip_commits_and_update(
1409 self, backend, pr_util, csrf_token):
1409 self, backend, pr_util, csrf_token):
1410 commits = [
1410 commits = [
1411 {'message': 'initial-commit'},
1411 {'message': 'initial-commit'},
1412 {'message': 'old-feature'},
1412 {'message': 'old-feature'},
1413 {'message': 'new-feature', 'parents': ['old-feature']},
1413 {'message': 'new-feature', 'parents': ['old-feature']},
1414 ]
1414 ]
1415 pull_request = pr_util.create_pull_request(
1415 pull_request = pr_util.create_pull_request(
1416 commits, target_head='old-feature', source_head='new-feature',
1416 commits, target_head='old-feature', source_head='new-feature',
1417 revisions=['new-feature'], mergeable=True)
1417 revisions=['new-feature'], mergeable=True)
1418 pr_id = pull_request.pull_request_id
1418 pr_id = pull_request.pull_request_id
1419 target_repo_name = pull_request.target_repo.repo_name
1419 target_repo_name = pull_request.target_repo.repo_name
1420
1420
1421 vcs = pr_util.source_repository.scm_instance()
1421 vcs = pr_util.source_repository.scm_instance()
1422 if backend.alias == 'git':
1422 if backend.alias == 'git':
1423 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1423 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1424 else:
1424 else:
1425 vcs.strip(pr_util.commit_ids['new-feature'])
1425 vcs.strip(pr_util.commit_ids['new-feature'])
1426
1426
1427 url = route_path('pullrequest_update',
1427 url = route_path('pullrequest_update',
1428 repo_name=target_repo_name,
1428 repo_name=target_repo_name,
1429 pull_request_id=pr_id)
1429 pull_request_id=pr_id)
1430 response = self.app.post(url,
1430 response = self.app.post(url,
1431 params={'update_commits': 'true',
1431 params={'update_commits': 'true',
1432 'csrf_token': csrf_token})
1432 'csrf_token': csrf_token})
1433
1433
1434 assert response.status_int == 200
1434 assert response.status_int == 200
1435 assert json.loads(response.body) == json.loads('{"response": true, "redirect_url": null}')
1435 assert json.loads(response.body) == json.loads('{"response": true, "redirect_url": null}')
1436
1436
1437 # Make sure that after update, it won't raise 500 errors
1437 # Make sure that after update, it won't raise 500 errors
1438 response = self.app.get(route_path(
1438 response = self.app.get(route_path(
1439 'pullrequest_show',
1439 'pullrequest_show',
1440 repo_name=target_repo_name,
1440 repo_name=target_repo_name,
1441 pull_request_id=pr_id))
1441 pull_request_id=pr_id))
1442
1442
1443 assert response.status_int == 200
1443 assert response.status_int == 200
1444 response.assert_response().element_contains(
1444 response.assert_response().element_contains(
1445 '#changeset_compare_view_content .alert strong',
1445 '#changeset_compare_view_content .alert strong',
1446 'Missing commits')
1446 'Missing commits')
1447
1447
1448 def test_branch_is_a_link(self, pr_util):
1448 def test_branch_is_a_link(self, pr_util):
1449 pull_request = pr_util.create_pull_request()
1449 pull_request = pr_util.create_pull_request()
1450 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1450 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1451 pull_request.target_ref = 'branch:target:abcdef1234567890'
1451 pull_request.target_ref = 'branch:target:abcdef1234567890'
1452 Session().add(pull_request)
1452 Session().add(pull_request)
1453 Session().commit()
1453 Session().commit()
1454
1454
1455 response = self.app.get(route_path(
1455 response = self.app.get(route_path(
1456 'pullrequest_show',
1456 'pullrequest_show',
1457 repo_name=pull_request.target_repo.scm_instance().name,
1457 repo_name=pull_request.target_repo.scm_instance().name,
1458 pull_request_id=pull_request.pull_request_id))
1458 pull_request_id=pull_request.pull_request_id))
1459 assert response.status_int == 200
1459 assert response.status_int == 200
1460
1460
1461 source = response.assert_response().get_element('.pr-source-info')
1461 source = response.assert_response().get_element('.pr-source-info')
1462 source_parent = source.getparent()
1462 source_parent = source.getparent()
1463 assert len(source_parent) == 1
1463 assert len(source_parent) == 1
1464
1464
1465 target = response.assert_response().get_element('.pr-target-info')
1465 target = response.assert_response().get_element('.pr-target-info')
1466 target_parent = target.getparent()
1466 target_parent = target.getparent()
1467 assert len(target_parent) == 1
1467 assert len(target_parent) == 1
1468
1468
1469 expected_origin_link = route_path(
1469 expected_origin_link = route_path(
1470 'repo_commits',
1470 'repo_commits',
1471 repo_name=pull_request.source_repo.scm_instance().name,
1471 repo_name=pull_request.source_repo.scm_instance().name,
1472 params=dict(branch='origin'))
1472 params=dict(branch='origin'))
1473 expected_target_link = route_path(
1473 expected_target_link = route_path(
1474 'repo_commits',
1474 'repo_commits',
1475 repo_name=pull_request.target_repo.scm_instance().name,
1475 repo_name=pull_request.target_repo.scm_instance().name,
1476 params=dict(branch='target'))
1476 params=dict(branch='target'))
1477 assert source_parent.attrib['href'] == expected_origin_link
1477 assert source_parent.attrib['href'] == expected_origin_link
1478 assert target_parent.attrib['href'] == expected_target_link
1478 assert target_parent.attrib['href'] == expected_target_link
1479
1479
1480 def test_bookmark_is_not_a_link(self, pr_util):
1480 def test_bookmark_is_not_a_link(self, pr_util):
1481 pull_request = pr_util.create_pull_request()
1481 pull_request = pr_util.create_pull_request()
1482 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1482 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1483 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1483 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1484 Session().add(pull_request)
1484 Session().add(pull_request)
1485 Session().commit()
1485 Session().commit()
1486
1486
1487 response = self.app.get(route_path(
1487 response = self.app.get(route_path(
1488 'pullrequest_show',
1488 'pullrequest_show',
1489 repo_name=pull_request.target_repo.scm_instance().name,
1489 repo_name=pull_request.target_repo.scm_instance().name,
1490 pull_request_id=pull_request.pull_request_id))
1490 pull_request_id=pull_request.pull_request_id))
1491 assert response.status_int == 200
1491 assert response.status_int == 200
1492
1492
1493 source = response.assert_response().get_element('.pr-source-info')
1493 source = response.assert_response().get_element('.pr-source-info')
1494 assert source.text.strip() == 'bookmark:origin'
1494 assert source.text.strip() == 'bookmark:origin'
1495 assert source.getparent().attrib.get('href') is None
1495 assert source.getparent().attrib.get('href') is None
1496
1496
1497 target = response.assert_response().get_element('.pr-target-info')
1497 target = response.assert_response().get_element('.pr-target-info')
1498 assert target.text.strip() == 'bookmark:target'
1498 assert target.text.strip() == 'bookmark:target'
1499 assert target.getparent().attrib.get('href') is None
1499 assert target.getparent().attrib.get('href') is None
1500
1500
1501 def test_tag_is_not_a_link(self, pr_util):
1501 def test_tag_is_not_a_link(self, pr_util):
1502 pull_request = pr_util.create_pull_request()
1502 pull_request = pr_util.create_pull_request()
1503 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1503 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1504 pull_request.target_ref = 'tag:target:abcdef1234567890'
1504 pull_request.target_ref = 'tag:target:abcdef1234567890'
1505 Session().add(pull_request)
1505 Session().add(pull_request)
1506 Session().commit()
1506 Session().commit()
1507
1507
1508 response = self.app.get(route_path(
1508 response = self.app.get(route_path(
1509 'pullrequest_show',
1509 'pullrequest_show',
1510 repo_name=pull_request.target_repo.scm_instance().name,
1510 repo_name=pull_request.target_repo.scm_instance().name,
1511 pull_request_id=pull_request.pull_request_id))
1511 pull_request_id=pull_request.pull_request_id))
1512 assert response.status_int == 200
1512 assert response.status_int == 200
1513
1513
1514 source = response.assert_response().get_element('.pr-source-info')
1514 source = response.assert_response().get_element('.pr-source-info')
1515 assert source.text.strip() == 'tag:origin'
1515 assert source.text.strip() == 'tag:origin'
1516 assert source.getparent().attrib.get('href') is None
1516 assert source.getparent().attrib.get('href') is None
1517
1517
1518 target = response.assert_response().get_element('.pr-target-info')
1518 target = response.assert_response().get_element('.pr-target-info')
1519 assert target.text.strip() == 'tag:target'
1519 assert target.text.strip() == 'tag:target'
1520 assert target.getparent().attrib.get('href') is None
1520 assert target.getparent().attrib.get('href') is None
1521
1521
1522 @pytest.mark.parametrize('mergeable', [True, False])
1522 @pytest.mark.parametrize('mergeable', [True, False])
1523 def test_shadow_repository_link(
1523 def test_shadow_repository_link(
1524 self, mergeable, pr_util, http_host_only_stub):
1524 self, mergeable, pr_util, http_host_only_stub):
1525 """
1525 """
1526 Check that the pull request summary page displays a link to the shadow
1526 Check that the pull request summary page displays a link to the shadow
1527 repository if the pull request is mergeable. If it is not mergeable
1527 repository if the pull request is mergeable. If it is not mergeable
1528 the link should not be displayed.
1528 the link should not be displayed.
1529 """
1529 """
1530 pull_request = pr_util.create_pull_request(
1530 pull_request = pr_util.create_pull_request(
1531 mergeable=mergeable, enable_notifications=False)
1531 mergeable=mergeable, enable_notifications=False)
1532 target_repo = pull_request.target_repo.scm_instance()
1532 target_repo = pull_request.target_repo.scm_instance()
1533 pr_id = pull_request.pull_request_id
1533 pr_id = pull_request.pull_request_id
1534 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1534 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1535 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1535 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1536
1536
1537 response = self.app.get(route_path(
1537 response = self.app.get(route_path(
1538 'pullrequest_show',
1538 'pullrequest_show',
1539 repo_name=target_repo.name,
1539 repo_name=target_repo.name,
1540 pull_request_id=pr_id))
1540 pull_request_id=pr_id))
1541
1541
1542 if mergeable:
1542 if mergeable:
1543 response.assert_response().element_value_contains(
1543 response.assert_response().element_value_contains(
1544 'input.pr-mergeinfo', shadow_url)
1544 'input.pr-mergeinfo', shadow_url)
1545 response.assert_response().element_value_contains(
1545 response.assert_response().element_value_contains(
1546 'input.pr-mergeinfo ', 'pr-merge')
1546 'input.pr-mergeinfo ', 'pr-merge')
1547 else:
1547 else:
1548 response.assert_response().no_element_exists('.pr-mergeinfo')
1548 response.assert_response().no_element_exists('.pr-mergeinfo')
1549
1549
1550
1550
1551 @pytest.mark.usefixtures('app')
1551 @pytest.mark.usefixtures('app')
1552 @pytest.mark.backends("git", "hg")
1552 @pytest.mark.backends("git", "hg")
1553 class TestPullrequestsControllerDelete(object):
1553 class TestPullrequestsControllerDelete(object):
1554 def test_pull_request_delete_button_permissions_admin(
1554 def test_pull_request_delete_button_permissions_admin(
1555 self, autologin_user, user_admin, pr_util):
1555 self, autologin_user, user_admin, pr_util):
1556 pull_request = pr_util.create_pull_request(
1556 pull_request = pr_util.create_pull_request(
1557 author=user_admin.username, enable_notifications=False)
1557 author=user_admin.username, enable_notifications=False)
1558
1558
1559 response = self.app.get(route_path(
1559 response = self.app.get(route_path(
1560 'pullrequest_show',
1560 'pullrequest_show',
1561 repo_name=pull_request.target_repo.scm_instance().name,
1561 repo_name=pull_request.target_repo.scm_instance().name,
1562 pull_request_id=pull_request.pull_request_id))
1562 pull_request_id=pull_request.pull_request_id))
1563
1563
1564 response.mustcontain('id="delete_pullrequest"')
1564 response.mustcontain('id="delete_pullrequest"')
1565 response.mustcontain('Confirm to delete this pull request')
1565 response.mustcontain('Confirm to delete this pull request')
1566
1566
1567 def test_pull_request_delete_button_permissions_owner(
1567 def test_pull_request_delete_button_permissions_owner(
1568 self, autologin_regular_user, user_regular, pr_util):
1568 self, autologin_regular_user, user_regular, pr_util):
1569 pull_request = pr_util.create_pull_request(
1569 pull_request = pr_util.create_pull_request(
1570 author=user_regular.username, enable_notifications=False)
1570 author=user_regular.username, enable_notifications=False)
1571
1571
1572 response = self.app.get(route_path(
1572 response = self.app.get(route_path(
1573 'pullrequest_show',
1573 'pullrequest_show',
1574 repo_name=pull_request.target_repo.scm_instance().name,
1574 repo_name=pull_request.target_repo.scm_instance().name,
1575 pull_request_id=pull_request.pull_request_id))
1575 pull_request_id=pull_request.pull_request_id))
1576
1576
1577 response.mustcontain('id="delete_pullrequest"')
1577 response.mustcontain('id="delete_pullrequest"')
1578 response.mustcontain('Confirm to delete this pull request')
1578 response.mustcontain('Confirm to delete this pull request')
1579
1579
1580 def test_pull_request_delete_button_permissions_forbidden(
1580 def test_pull_request_delete_button_permissions_forbidden(
1581 self, autologin_regular_user, user_regular, user_admin, pr_util):
1581 self, autologin_regular_user, user_regular, user_admin, pr_util):
1582 pull_request = pr_util.create_pull_request(
1582 pull_request = pr_util.create_pull_request(
1583 author=user_admin.username, enable_notifications=False)
1583 author=user_admin.username, enable_notifications=False)
1584
1584
1585 response = self.app.get(route_path(
1585 response = self.app.get(route_path(
1586 'pullrequest_show',
1586 'pullrequest_show',
1587 repo_name=pull_request.target_repo.scm_instance().name,
1587 repo_name=pull_request.target_repo.scm_instance().name,
1588 pull_request_id=pull_request.pull_request_id))
1588 pull_request_id=pull_request.pull_request_id))
1589 response.mustcontain(no=['id="delete_pullrequest"'])
1589 response.mustcontain(no=['id="delete_pullrequest"'])
1590 response.mustcontain(no=['Confirm to delete this pull request'])
1590 response.mustcontain(no=['Confirm to delete this pull request'])
1591
1591
1592 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1592 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1593 self, autologin_regular_user, user_regular, user_admin, pr_util,
1593 self, autologin_regular_user, user_regular, user_admin, pr_util,
1594 user_util):
1594 user_util):
1595
1595
1596 pull_request = pr_util.create_pull_request(
1596 pull_request = pr_util.create_pull_request(
1597 author=user_admin.username, enable_notifications=False)
1597 author=user_admin.username, enable_notifications=False)
1598
1598
1599 user_util.grant_user_permission_to_repo(
1599 user_util.grant_user_permission_to_repo(
1600 pull_request.target_repo, user_regular,
1600 pull_request.target_repo, user_regular,
1601 'repository.write')
1601 'repository.write')
1602
1602
1603 response = self.app.get(route_path(
1603 response = self.app.get(route_path(
1604 'pullrequest_show',
1604 'pullrequest_show',
1605 repo_name=pull_request.target_repo.scm_instance().name,
1605 repo_name=pull_request.target_repo.scm_instance().name,
1606 pull_request_id=pull_request.pull_request_id))
1606 pull_request_id=pull_request.pull_request_id))
1607
1607
1608 response.mustcontain('id="open_edit_pullrequest"')
1608 response.mustcontain('id="open_edit_pullrequest"')
1609 response.mustcontain('id="delete_pullrequest"')
1609 response.mustcontain('id="delete_pullrequest"')
1610 response.mustcontain(no=['Confirm to delete this pull request'])
1610 response.mustcontain(no=['Confirm to delete this pull request'])
1611
1611
1612 def test_delete_comment_returns_404_if_comment_does_not_exist(
1612 def test_delete_comment_returns_404_if_comment_does_not_exist(
1613 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1613 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1614
1614
1615 pull_request = pr_util.create_pull_request(
1615 pull_request = pr_util.create_pull_request(
1616 author=user_admin.username, enable_notifications=False)
1616 author=user_admin.username, enable_notifications=False)
1617
1617
1618 self.app.post(
1618 self.app.post(
1619 route_path(
1619 route_path(
1620 'pullrequest_comment_delete',
1620 'pullrequest_comment_delete',
1621 repo_name=pull_request.target_repo.scm_instance().name,
1621 repo_name=pull_request.target_repo.scm_instance().name,
1622 pull_request_id=pull_request.pull_request_id,
1622 pull_request_id=pull_request.pull_request_id,
1623 comment_id=1024404),
1623 comment_id=1024404),
1624 extra_environ=xhr_header,
1624 extra_environ=xhr_header,
1625 params={'csrf_token': csrf_token},
1625 params={'csrf_token': csrf_token},
1626 status=404
1626 status=404
1627 )
1627 )
1628
1628
1629 def test_delete_comment(
1629 def test_delete_comment(
1630 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1630 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1631
1631
1632 pull_request = pr_util.create_pull_request(
1632 pull_request = pr_util.create_pull_request(
1633 author=user_admin.username, enable_notifications=False)
1633 author=user_admin.username, enable_notifications=False)
1634 comment = pr_util.create_comment()
1634 comment = pr_util.create_comment()
1635 comment_id = comment.comment_id
1635 comment_id = comment.comment_id
1636
1636
1637 response = self.app.post(
1637 response = self.app.post(
1638 route_path(
1638 route_path(
1639 'pullrequest_comment_delete',
1639 'pullrequest_comment_delete',
1640 repo_name=pull_request.target_repo.scm_instance().name,
1640 repo_name=pull_request.target_repo.scm_instance().name,
1641 pull_request_id=pull_request.pull_request_id,
1641 pull_request_id=pull_request.pull_request_id,
1642 comment_id=comment_id),
1642 comment_id=comment_id),
1643 extra_environ=xhr_header,
1643 extra_environ=xhr_header,
1644 params={'csrf_token': csrf_token},
1644 params={'csrf_token': csrf_token},
1645 status=200
1645 status=200
1646 )
1646 )
1647 assert response.body == 'true'
1647 assert response.body == 'true'
1648
1648
1649 @pytest.mark.parametrize('url_type', [
1649 @pytest.mark.parametrize('url_type', [
1650 'pullrequest_new',
1650 'pullrequest_new',
1651 'pullrequest_create',
1651 'pullrequest_create',
1652 'pullrequest_update',
1652 'pullrequest_update',
1653 'pullrequest_merge',
1653 'pullrequest_merge',
1654 ])
1654 ])
1655 def test_pull_request_is_forbidden_on_archived_repo(
1655 def test_pull_request_is_forbidden_on_archived_repo(
1656 self, autologin_user, backend, xhr_header, user_util, url_type):
1656 self, autologin_user, backend, xhr_header, user_util, url_type):
1657
1657
1658 # create a temporary repo
1658 # create a temporary repo
1659 source = user_util.create_repo(repo_type=backend.alias)
1659 source = user_util.create_repo(repo_type=backend.alias)
1660 repo_name = source.repo_name
1660 repo_name = source.repo_name
1661 repo = Repository.get_by_repo_name(repo_name)
1661 repo = Repository.get_by_repo_name(repo_name)
1662 repo.archived = True
1662 repo.archived = True
1663 Session().commit()
1663 Session().commit()
1664
1664
1665 response = self.app.get(
1665 response = self.app.get(
1666 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1666 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1667
1667
1668 msg = 'Action not supported for archived repository.'
1668 msg = 'Action not supported for archived repository.'
1669 assert_session_flash(response, msg)
1669 assert_session_flash(response, msg)
1670
1670
1671
1671
1672 def assert_pull_request_status(pull_request, expected_status):
1672 def assert_pull_request_status(pull_request, expected_status):
1673 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1673 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1674 assert status == expected_status
1674 assert status == expected_status
1675
1675
1676
1676
1677 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1677 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1678 @pytest.mark.usefixtures("autologin_user")
1678 @pytest.mark.usefixtures("autologin_user")
1679 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1679 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1680 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
1680 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,232 +1,232 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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
23
24 from rhodecode.lib.utils2 import str2bool
24 from rhodecode.lib.utils2 import str2bool
25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
26 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.tests import (
28 from rhodecode.tests import (
29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
30 from rhodecode.tests.fixture import Fixture
30 from rhodecode.tests.fixture import Fixture
31
31
32 fixture = Fixture()
32 fixture = Fixture()
33
33
34
34
35 def route_path(name, params=None, **kwargs):
35 def route_path(name, params=None, **kwargs):
36 import urllib
36 import urllib.request, urllib.parse, urllib.error
37
37
38 base_url = {
38 base_url = {
39 'edit_repo': '/{repo_name}/settings',
39 'edit_repo': '/{repo_name}/settings',
40 'edit_repo_advanced': '/{repo_name}/settings/advanced',
40 'edit_repo_advanced': '/{repo_name}/settings/advanced',
41 'edit_repo_caches': '/{repo_name}/settings/caches',
41 'edit_repo_caches': '/{repo_name}/settings/caches',
42 'edit_repo_perms': '/{repo_name}/settings/permissions',
42 'edit_repo_perms': '/{repo_name}/settings/permissions',
43 'edit_repo_vcs': '/{repo_name}/settings/vcs',
43 'edit_repo_vcs': '/{repo_name}/settings/vcs',
44 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
44 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
45 'edit_repo_fields': '/{repo_name}/settings/fields',
45 'edit_repo_fields': '/{repo_name}/settings/fields',
46 'edit_repo_remote': '/{repo_name}/settings/remote',
46 'edit_repo_remote': '/{repo_name}/settings/remote',
47 'edit_repo_statistics': '/{repo_name}/settings/statistics',
47 'edit_repo_statistics': '/{repo_name}/settings/statistics',
48 }[name].format(**kwargs)
48 }[name].format(**kwargs)
49
49
50 if params:
50 if params:
51 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
51 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
52 return base_url
52 return base_url
53
53
54
54
55 def _get_permission_for_user(user, repo):
55 def _get_permission_for_user(user, repo):
56 perm = UserRepoToPerm.query()\
56 perm = UserRepoToPerm.query()\
57 .filter(UserRepoToPerm.repository ==
57 .filter(UserRepoToPerm.repository ==
58 Repository.get_by_repo_name(repo))\
58 Repository.get_by_repo_name(repo))\
59 .filter(UserRepoToPerm.user == User.get_by_username(user))\
59 .filter(UserRepoToPerm.user == User.get_by_username(user))\
60 .all()
60 .all()
61 return perm
61 return perm
62
62
63
63
64 @pytest.mark.usefixtures('autologin_user', 'app')
64 @pytest.mark.usefixtures('autologin_user', 'app')
65 class TestAdminRepoSettings(object):
65 class TestAdminRepoSettings(object):
66 @pytest.mark.parametrize('urlname', [
66 @pytest.mark.parametrize('urlname', [
67 'edit_repo',
67 'edit_repo',
68 'edit_repo_caches',
68 'edit_repo_caches',
69 'edit_repo_perms',
69 'edit_repo_perms',
70 'edit_repo_advanced',
70 'edit_repo_advanced',
71 'edit_repo_vcs',
71 'edit_repo_vcs',
72 'edit_repo_issuetracker',
72 'edit_repo_issuetracker',
73 'edit_repo_fields',
73 'edit_repo_fields',
74 'edit_repo_remote',
74 'edit_repo_remote',
75 'edit_repo_statistics',
75 'edit_repo_statistics',
76 ])
76 ])
77 def test_show_page(self, urlname, app, backend):
77 def test_show_page(self, urlname, app, backend):
78 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
78 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
79
79
80 def test_edit_accessible_when_missing_requirements(
80 def test_edit_accessible_when_missing_requirements(
81 self, backend_hg, autologin_user):
81 self, backend_hg, autologin_user):
82 scm_patcher = mock.patch.object(
82 scm_patcher = mock.patch.object(
83 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
83 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
84 with scm_patcher:
84 with scm_patcher:
85 self.app.get(route_path('edit_repo', repo_name=backend_hg.repo_name))
85 self.app.get(route_path('edit_repo', repo_name=backend_hg.repo_name))
86
86
87 @pytest.mark.parametrize('update_settings', [
87 @pytest.mark.parametrize('update_settings', [
88 {'repo_description': 'alter-desc'},
88 {'repo_description': 'alter-desc'},
89 {'repo_owner': TEST_USER_REGULAR_LOGIN},
89 {'repo_owner': TEST_USER_REGULAR_LOGIN},
90 {'repo_private': 'true'},
90 {'repo_private': 'true'},
91 {'repo_enable_locking': 'true'},
91 {'repo_enable_locking': 'true'},
92 {'repo_enable_downloads': 'true'},
92 {'repo_enable_downloads': 'true'},
93 ])
93 ])
94 def test_update_repo_settings(self, update_settings, csrf_token, backend, user_util):
94 def test_update_repo_settings(self, update_settings, csrf_token, backend, user_util):
95 repo = user_util.create_repo(repo_type=backend.alias)
95 repo = user_util.create_repo(repo_type=backend.alias)
96 repo_name = repo.repo_name
96 repo_name = repo.repo_name
97
97
98 params = fixture._get_repo_create_params(
98 params = fixture._get_repo_create_params(
99 csrf_token=csrf_token,
99 csrf_token=csrf_token,
100 repo_name=repo_name,
100 repo_name=repo_name,
101 repo_type=backend.alias,
101 repo_type=backend.alias,
102 repo_owner=TEST_USER_ADMIN_LOGIN,
102 repo_owner=TEST_USER_ADMIN_LOGIN,
103 repo_description='DESC',
103 repo_description='DESC',
104
104
105 repo_private='false',
105 repo_private='false',
106 repo_enable_locking='false',
106 repo_enable_locking='false',
107 repo_enable_downloads='false')
107 repo_enable_downloads='false')
108 params.update(update_settings)
108 params.update(update_settings)
109 self.app.post(
109 self.app.post(
110 route_path('edit_repo', repo_name=repo_name),
110 route_path('edit_repo', repo_name=repo_name),
111 params=params, status=302)
111 params=params, status=302)
112
112
113 repo = Repository.get_by_repo_name(repo_name)
113 repo = Repository.get_by_repo_name(repo_name)
114 assert repo.user.username == \
114 assert repo.user.username == \
115 update_settings.get('repo_owner', repo.user.username)
115 update_settings.get('repo_owner', repo.user.username)
116
116
117 assert repo.description == \
117 assert repo.description == \
118 update_settings.get('repo_description', repo.description)
118 update_settings.get('repo_description', repo.description)
119
119
120 assert repo.private == \
120 assert repo.private == \
121 str2bool(update_settings.get(
121 str2bool(update_settings.get(
122 'repo_private', repo.private))
122 'repo_private', repo.private))
123
123
124 assert repo.enable_locking == \
124 assert repo.enable_locking == \
125 str2bool(update_settings.get(
125 str2bool(update_settings.get(
126 'repo_enable_locking', repo.enable_locking))
126 'repo_enable_locking', repo.enable_locking))
127
127
128 assert repo.enable_downloads == \
128 assert repo.enable_downloads == \
129 str2bool(update_settings.get(
129 str2bool(update_settings.get(
130 'repo_enable_downloads', repo.enable_downloads))
130 'repo_enable_downloads', repo.enable_downloads))
131
131
132 def test_update_repo_name_via_settings(self, csrf_token, user_util, backend):
132 def test_update_repo_name_via_settings(self, csrf_token, user_util, backend):
133 repo = user_util.create_repo(repo_type=backend.alias)
133 repo = user_util.create_repo(repo_type=backend.alias)
134 repo_name = repo.repo_name
134 repo_name = repo.repo_name
135
135
136 repo_group = user_util.create_repo_group()
136 repo_group = user_util.create_repo_group()
137 repo_group_name = repo_group.group_name
137 repo_group_name = repo_group.group_name
138 new_name = repo_group_name + '_' + repo_name
138 new_name = repo_group_name + '_' + repo_name
139
139
140 params = fixture._get_repo_create_params(
140 params = fixture._get_repo_create_params(
141 csrf_token=csrf_token,
141 csrf_token=csrf_token,
142 repo_name=new_name,
142 repo_name=new_name,
143 repo_type=backend.alias,
143 repo_type=backend.alias,
144 repo_owner=TEST_USER_ADMIN_LOGIN,
144 repo_owner=TEST_USER_ADMIN_LOGIN,
145 repo_description='DESC',
145 repo_description='DESC',
146 repo_private='false',
146 repo_private='false',
147 repo_enable_locking='false',
147 repo_enable_locking='false',
148 repo_enable_downloads='false')
148 repo_enable_downloads='false')
149 self.app.post(
149 self.app.post(
150 route_path('edit_repo', repo_name=repo_name),
150 route_path('edit_repo', repo_name=repo_name),
151 params=params, status=302)
151 params=params, status=302)
152 repo = Repository.get_by_repo_name(new_name)
152 repo = Repository.get_by_repo_name(new_name)
153 assert repo.repo_name == new_name
153 assert repo.repo_name == new_name
154
154
155 def test_update_repo_group_via_settings(self, csrf_token, user_util, backend):
155 def test_update_repo_group_via_settings(self, csrf_token, user_util, backend):
156 repo = user_util.create_repo(repo_type=backend.alias)
156 repo = user_util.create_repo(repo_type=backend.alias)
157 repo_name = repo.repo_name
157 repo_name = repo.repo_name
158
158
159 repo_group = user_util.create_repo_group()
159 repo_group = user_util.create_repo_group()
160 repo_group_name = repo_group.group_name
160 repo_group_name = repo_group.group_name
161 repo_group_id = repo_group.group_id
161 repo_group_id = repo_group.group_id
162
162
163 new_name = repo_group_name + '/' + repo_name
163 new_name = repo_group_name + '/' + repo_name
164 params = fixture._get_repo_create_params(
164 params = fixture._get_repo_create_params(
165 csrf_token=csrf_token,
165 csrf_token=csrf_token,
166 repo_name=repo_name,
166 repo_name=repo_name,
167 repo_type=backend.alias,
167 repo_type=backend.alias,
168 repo_owner=TEST_USER_ADMIN_LOGIN,
168 repo_owner=TEST_USER_ADMIN_LOGIN,
169 repo_description='DESC',
169 repo_description='DESC',
170 repo_group=repo_group_id,
170 repo_group=repo_group_id,
171 repo_private='false',
171 repo_private='false',
172 repo_enable_locking='false',
172 repo_enable_locking='false',
173 repo_enable_downloads='false')
173 repo_enable_downloads='false')
174 self.app.post(
174 self.app.post(
175 route_path('edit_repo', repo_name=repo_name),
175 route_path('edit_repo', repo_name=repo_name),
176 params=params, status=302)
176 params=params, status=302)
177 repo = Repository.get_by_repo_name(new_name)
177 repo = Repository.get_by_repo_name(new_name)
178 assert repo.repo_name == new_name
178 assert repo.repo_name == new_name
179
179
180 def test_set_private_flag_sets_default_user_permissions_to_none(
180 def test_set_private_flag_sets_default_user_permissions_to_none(
181 self, autologin_user, backend, csrf_token):
181 self, autologin_user, backend, csrf_token):
182
182
183 # initially repository perm should be read
183 # initially repository perm should be read
184 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
184 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
185 assert len(perm) == 1
185 assert len(perm) == 1
186 assert perm[0].permission.permission_name == 'repository.read'
186 assert perm[0].permission.permission_name == 'repository.read'
187 assert not backend.repo.private
187 assert not backend.repo.private
188
188
189 response = self.app.post(
189 response = self.app.post(
190 route_path('edit_repo', repo_name=backend.repo_name),
190 route_path('edit_repo', repo_name=backend.repo_name),
191 params=fixture._get_repo_create_params(
191 params=fixture._get_repo_create_params(
192 repo_private='true',
192 repo_private='true',
193 repo_name=backend.repo_name,
193 repo_name=backend.repo_name,
194 repo_type=backend.alias,
194 repo_type=backend.alias,
195 repo_owner=TEST_USER_ADMIN_LOGIN,
195 repo_owner=TEST_USER_ADMIN_LOGIN,
196 csrf_token=csrf_token), status=302)
196 csrf_token=csrf_token), status=302)
197
197
198 assert_session_flash(
198 assert_session_flash(
199 response,
199 response,
200 msg='Repository `%s` updated successfully' % (backend.repo_name))
200 msg='Repository `%s` updated successfully' % (backend.repo_name))
201
201
202 repo = Repository.get_by_repo_name(backend.repo_name)
202 repo = Repository.get_by_repo_name(backend.repo_name)
203 assert repo.private is True
203 assert repo.private is True
204
204
205 # now the repo default permission should be None
205 # now the repo default permission should be None
206 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
206 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
207 assert len(perm) == 1
207 assert len(perm) == 1
208 assert perm[0].permission.permission_name == 'repository.none'
208 assert perm[0].permission.permission_name == 'repository.none'
209
209
210 response = self.app.post(
210 response = self.app.post(
211 route_path('edit_repo', repo_name=backend.repo_name),
211 route_path('edit_repo', repo_name=backend.repo_name),
212 params=fixture._get_repo_create_params(
212 params=fixture._get_repo_create_params(
213 repo_private='false',
213 repo_private='false',
214 repo_name=backend.repo_name,
214 repo_name=backend.repo_name,
215 repo_type=backend.alias,
215 repo_type=backend.alias,
216 repo_owner=TEST_USER_ADMIN_LOGIN,
216 repo_owner=TEST_USER_ADMIN_LOGIN,
217 csrf_token=csrf_token), status=302)
217 csrf_token=csrf_token), status=302)
218
218
219 assert_session_flash(
219 assert_session_flash(
220 response,
220 response,
221 msg='Repository `%s` updated successfully' % (backend.repo_name))
221 msg='Repository `%s` updated successfully' % (backend.repo_name))
222 assert backend.repo.private is False
222 assert backend.repo.private is False
223
223
224 # we turn off private now the repo default permission should stay None
224 # we turn off private now the repo default permission should stay None
225 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
225 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
226 assert len(perm) == 1
226 assert len(perm) == 1
227 assert perm[0].permission.permission_name == 'repository.none'
227 assert perm[0].permission.permission_name == 'repository.none'
228
228
229 # update this permission back
229 # update this permission back
230 perm[0].permission = Permission.get_by_key('repository.read')
230 perm[0].permission = Permission.get_by_key('repository.read')
231 Session().add(perm[0])
231 Session().add(perm[0])
232 Session().commit()
232 Session().commit()
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now