##// END OF EJS Templates
tests: fixed all tests for python3 BIG changes
super-admin -
r5087:0a27bd22 default
parent child Browse files
Show More

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

1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,201 +1,58 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # 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
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # 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/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import pytest
20 import pytest # noqa
22 from rhodecode.lib import ext_json
21
22 # keep the imports to have a toplevel conftest.py but still importable from EE edition
23 from rhodecode.tests.conftest_common import ( # noqa
24 pytest_generate_tests,
25 pytest_runtest_makereport,
26 pytest_addoption
27 )
23
28
24
29
25 pytest_plugins = [
30 pytest_plugins = [
26 "rhodecode.tests.fixture_mods.fixture_pyramid",
31 "rhodecode.tests.fixture_mods.fixture_pyramid",
27 "rhodecode.tests.fixture_mods.fixture_utils",
32 "rhodecode.tests.fixture_mods.fixture_utils",
28 ]
33 ]
29
34
30
35
31 def pytest_configure(config):
36 def pytest_configure(config):
32 from rhodecode.config import patches
37 from rhodecode.config import patches # noqa
33
34
35 def pytest_addoption(parser):
36
37 def _parse_json(value):
38 return ext_json.str_json(value) if value else None
39
40 def _split_comma(value):
41 return value.split(',')
42
43 parser.addoption(
44 '--keep-tmp-path', action='store_true',
45 help="Keep the test temporary directories")
46
47 parser.addoption(
48 '--backends', action='store', type=_split_comma,
49 default=['git', 'hg', 'svn'],
50 help="Select which backends to test for backend specific tests.")
51 parser.addoption(
52 '--dbs', action='store', type=_split_comma,
53 default=['sqlite'],
54 help="Select which database to test for database specific tests. "
55 "Possible options are sqlite,postgres,mysql")
56 parser.addoption(
57 '--appenlight', '--ae', action='store_true',
58 help="Track statistics in appenlight.")
59 parser.addoption(
60 '--appenlight-api-key', '--ae-key',
61 help="API key for Appenlight.")
62 parser.addoption(
63 '--appenlight-url', '--ae-url',
64 default="https://ae.rhodecode.com",
65 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
66 parser.addoption(
67 '--sqlite-connection-string', action='store',
68 default='', help="Connection string for the dbs tests with SQLite")
69 parser.addoption(
70 '--postgres-connection-string', action='store',
71 default='', help="Connection string for the dbs tests with Postgres")
72 parser.addoption(
73 '--mysql-connection-string', action='store',
74 default='', help="Connection string for the dbs tests with MySQL")
75 parser.addoption(
76 '--repeat', type=int, default=100,
77 help="Number of repetitions in performance tests.")
78
79 parser.addoption(
80 '--test-loglevel', dest='test_loglevel',
81 help="Set default Logging level for tests, critical(default), error, warn , info, debug")
82 group = parser.getgroup('pylons')
83 group.addoption(
84 '--with-pylons', dest='pyramid_config',
85 help="Set up a Pylons environment with the specified config file.")
86 group.addoption(
87 '--ini-config-override', action='store', type=_parse_json,
88 default=None, dest='pyramid_config_override', help=(
89 "Overrides the .ini file settings. Should be specified in JSON"
90 " format, e.g. '{\"section\": {\"parameter\": \"value\", ...}}'"
91 )
92 )
93 parser.addini(
94 'pyramid_config',
95 "Set up a Pyramid environment with the specified config file.")
96
97 vcsgroup = parser.getgroup('vcs')
98 vcsgroup.addoption(
99 '--without-vcsserver', dest='with_vcsserver', action='store_false',
100 help="Do not start the VCSServer in a background process.")
101 vcsgroup.addoption(
102 '--with-vcsserver-http', dest='vcsserver_config_http',
103 help="Start the HTTP VCSServer with the specified config file.")
104 vcsgroup.addoption(
105 '--vcsserver-protocol', dest='vcsserver_protocol',
106 help="Start the VCSServer with HTTP protocol support.")
107 vcsgroup.addoption(
108 '--vcsserver-config-override', action='store', type=_parse_json,
109 default=None, dest='vcsserver_config_override', help=(
110 "Overrides the .ini file settings for the VCSServer. "
111 "Should be specified in JSON "
112 "format, e.g. '{\"section\": {\"parameter\": \"value\", ...}}'"
113 )
114 )
115 vcsgroup.addoption(
116 '--vcsserver-port', action='store', type=int,
117 default=None, help=(
118 "Allows to set the port of the vcsserver. Useful when testing "
119 "against an already running server and random ports cause "
120 "trouble."))
121 parser.addini(
122 'vcsserver_config_http',
123 "Start the HTTP VCSServer with the specified config file.")
124 parser.addini(
125 'vcsserver_protocol',
126 "Start the VCSServer with HTTP protocol support.")
127
128
129 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
130 def pytest_runtest_makereport(item, call):
131 """
132 Adding the remote traceback if the exception has this information.
133
134 VCSServer attaches this information as the attribute `_vcs_server_traceback`
135 to the exception instance.
136 """
137 outcome = yield
138 report = outcome.get_result()
139
140 if call.excinfo:
141 exc = call.excinfo.value
142 vcsserver_traceback = getattr(exc, '_vcs_server_traceback', None)
143
144 if vcsserver_traceback and report.outcome == 'failed':
145 section = f'VCSServer remote traceback {report.when}'
146 report.sections.append((section, vcsserver_traceback))
147
38
148
39
149 def pytest_collection_modifyitems(session, config, items):
40 def pytest_collection_modifyitems(session, config, items):
150 # nottest marked, compare nose, used for transition from nose to pytest
41 # nottest marked, compare nose, used for transition from nose to pytest
151 remaining = [
42 remaining = [
152 i for i in items if getattr(i.obj, '__test__', True)]
43 i for i in items if getattr(i.obj, '__test__', True)]
153 items[:] = remaining
44 items[:] = remaining
154
45
155 # NOTE(marcink): custom test ordering, db tests and vcstests are slowes and should
46 # NOTE(marcink): custom test ordering, db tests and vcstests are slowest and should
156 # be executed at the end for faster test feedback
47 # be executed at the end for faster test feedback
157 def sorter(item):
48 def sorter(item):
158 pos = 0
49 pos = 0
159 key = item._nodeid
50 key = item._nodeid
160 if key.startswith('rhodecode/tests/database'):
51 if key.startswith('rhodecode/tests/database'):
161 pos = 1
52 pos = 1
162 elif key.startswith('rhodecode/tests/vcs_operations'):
53 elif key.startswith('rhodecode/tests/vcs_operations'):
163 pos = 2
54 pos = 2
164
55
165 return pos
56 return pos
166
57
167 items.sort(key=sorter)
58 items.sort(key=sorter)
168
169
170 def get_backends_from_metafunc(metafunc):
171 requested_backends = set(metafunc.config.getoption('--backends'))
172 backend_mark = metafunc.definition.get_closest_marker('backends')
173 if backend_mark:
174 # Supported backends by this test function, created from
175 # pytest.mark.backends
176 backends = backend_mark.args
177 elif hasattr(metafunc.cls, 'backend_alias'):
178 # Support class attribute "backend_alias", this is mainly
179 # for legacy reasons for tests not yet using pytest.mark.backends
180 backends = [metafunc.cls.backend_alias]
181 else:
182 backends = metafunc.config.getoption('--backends')
183 return requested_backends.intersection(backends)
184
185
186 def pytest_generate_tests(metafunc):
187
188 # Support test generation based on --backend parameter
189 if 'backend_alias' in metafunc.fixturenames:
190 backends = get_backends_from_metafunc(metafunc)
191 scope = None
192 if not backends:
193 pytest.skip("Not enabled for any of selected backends")
194
195 metafunc.parametrize('backend_alias', backends, scope=scope)
196
197 backend_mark = metafunc.definition.get_closest_marker('backends')
198 if backend_mark:
199 backends = get_backends_from_metafunc(metafunc)
200 if not backends:
201 pytest.skip("Not enabled for any of selected backends")
@@ -1,21 +1,23 b''
1 [pytest]
1 [pytest]
2 testpaths = rhodecode
2 testpaths = rhodecode
3 norecursedirs = rhodecode/public rhodecode/templates tests/scripts
3 norecursedirs = rhodecode/public rhodecode/templates tests/scripts
4 cache_dir = /tmp/.pytest_cache
4 cache_dir = /tmp/.pytest_cache
5
5
6 pyramid_config = rhodecode/tests/rhodecode.ini
6 pyramid_config = rhodecode/tests/rhodecode.ini
7 vcsserver_protocol = http
7 vcsserver_protocol = http
8 vcsserver_config_http = rhodecode/tests/vcsserver_http.ini
8 vcsserver_config_http = rhodecode/tests/vcsserver_http.ini
9
9
10 addopts =
10 addopts =
11 --pdbcls=IPython.terminal.debugger:TerminalPdb
11 --pdbcls=IPython.terminal.debugger:TerminalPdb
12 --strict-markers
12 --strict-markers
13 --capture=no
13 --capture=no
14 --show-capture=all
14 --show-capture=all
15
15
16 # --test-loglevel=INFO, show log-level during execution
17
16 markers =
18 markers =
17 vcs_operations: Mark tests depending on a running RhodeCode instance.
19 vcs_operations: Mark tests depending on a running RhodeCode instance.
18 xfail_backends: Mark tests as xfail for given backends.
20 xfail_backends: Mark tests as xfail for given backends.
19 skip_backends: Mark tests as skipped for given backends.
21 skip_backends: Mark tests as skipped for given backends.
20 backends: Mark backends
22 backends: Mark backends
21 dbs: database markers for running tests for given DB
23 dbs: database markers for running tests for given DB
@@ -1,170 +1,172 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import os
20 import os
21 import csv
21 import csv
22 import datetime
22 import datetime
23
23
24 import pytest
24 import pytest
25
25
26 from rhodecode.lib.str_utils import safe_str
26 from rhodecode.tests import *
27 from rhodecode.tests import *
27 from rhodecode.tests.fixture import FIXTURES
28 from rhodecode.tests.fixture import FIXTURES
28 from rhodecode.model.db import UserLog
29 from rhodecode.model.db import UserLog
29 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
30 from rhodecode.lib.utils2 import safe_unicode
31
31
32
32
33 def route_path(name, params=None, **kwargs):
33 def route_path(name, params=None, **kwargs):
34 import urllib.request, urllib.parse, urllib.error
34 import urllib.request
35 import urllib.parse
36 import urllib.error
35 from rhodecode.apps._base import ADMIN_PREFIX
37 from rhodecode.apps._base import ADMIN_PREFIX
36
38
37 base_url = {
39 base_url = {
38 'admin_home': ADMIN_PREFIX,
40 'admin_home': ADMIN_PREFIX,
39 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs',
41 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs',
40
42
41 }[name].format(**kwargs)
43 }[name].format(**kwargs)
42
44
43 if params:
45 if params:
44 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
45 return base_url
47 return base_url
46
48
47
49
48 @pytest.mark.usefixtures('app')
50 @pytest.mark.usefixtures('app')
49 class TestAdminController(object):
51 class TestAdminController(object):
50
52
51 @pytest.fixture(scope='class', autouse=True)
53 @pytest.fixture(scope='class', autouse=True)
52 def prepare(self, request, baseapp):
54 def prepare(self, request, baseapp):
53 UserLog.query().delete()
55 UserLog.query().delete()
54 Session().commit()
56 Session().commit()
55
57
56 def strptime(val):
58 def strptime(val):
57 fmt = '%Y-%m-%d %H:%M:%S'
59 fmt = '%Y-%m-%d %H:%M:%S'
58 if '.' not in val:
60 if '.' not in val:
59 return datetime.datetime.strptime(val, fmt)
61 return datetime.datetime.strptime(val, fmt)
60
62
61 nofrag, frag = val.split(".")
63 nofrag, frag = val.split(".")
62 date = datetime.datetime.strptime(nofrag, fmt)
64 date = datetime.datetime.strptime(nofrag, fmt)
63
65
64 frag = frag[:6] # truncate to microseconds
66 frag = frag[:6] # truncate to microseconds
65 frag += (6 - len(frag)) * '0' # add 0s
67 frag += (6 - len(frag)) * '0' # add 0s
66 return date.replace(microsecond=int(frag))
68 return date.replace(microsecond=int(frag))
67
69
68 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
70 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
69 for row in csv.DictReader(f):
71 for row in csv.DictReader(f):
70 ul = UserLog()
72 ul = UserLog()
71 for k, v in row.items():
73 for k, v in row.items():
72 v = safe_unicode(v)
74 v = safe_str(v)
73 if k == 'action_date':
75 if k == 'action_date':
74 v = strptime(v)
76 v = strptime(v)
75 if k in ['user_id', 'repository_id']:
77 if k in ['user_id', 'repository_id']:
76 # nullable due to FK problems
78 # nullable due to FK problems
77 v = None
79 v = None
78 setattr(ul, k, v)
80 setattr(ul, k, v)
79 Session().add(ul)
81 Session().add(ul)
80 Session().commit()
82 Session().commit()
81
83
82 @request.addfinalizer
84 @request.addfinalizer
83 def cleanup():
85 def cleanup():
84 UserLog.query().delete()
86 UserLog.query().delete()
85 Session().commit()
87 Session().commit()
86
88
87 def test_index(self, autologin_user):
89 def test_index(self, autologin_user):
88 response = self.app.get(route_path('admin_audit_logs'))
90 response = self.app.get(route_path('admin_audit_logs'))
89 response.mustcontain('Admin audit logs')
91 response.mustcontain('Admin audit logs')
90
92
91 def test_filter_all_entries(self, autologin_user):
93 def test_filter_all_entries(self, autologin_user):
92 response = self.app.get(route_path('admin_audit_logs'))
94 response = self.app.get(route_path('admin_audit_logs'))
93 all_count = UserLog.query().count()
95 all_count = UserLog.query().count()
94 response.mustcontain('%s entries' % all_count)
96 response.mustcontain('%s entries' % all_count)
95
97
96 def test_filter_journal_filter_exact_match_on_repository(self, autologin_user):
98 def test_filter_journal_filter_exact_match_on_repository(self, autologin_user):
97 response = self.app.get(route_path('admin_audit_logs',
99 response = self.app.get(route_path('admin_audit_logs',
98 params=dict(filter='repository:rhodecode')))
100 params=dict(filter='repository:rhodecode')))
99 response.mustcontain('3 entries')
101 response.mustcontain('3 entries')
100
102
101 def test_filter_journal_filter_exact_match_on_repository_CamelCase(self, autologin_user):
103 def test_filter_journal_filter_exact_match_on_repository_CamelCase(self, autologin_user):
102 response = self.app.get(route_path('admin_audit_logs',
104 response = self.app.get(route_path('admin_audit_logs',
103 params=dict(filter='repository:RhodeCode')))
105 params=dict(filter='repository:RhodeCode')))
104 response.mustcontain('3 entries')
106 response.mustcontain('3 entries')
105
107
106 def test_filter_journal_filter_wildcard_on_repository(self, autologin_user):
108 def test_filter_journal_filter_wildcard_on_repository(self, autologin_user):
107 response = self.app.get(route_path('admin_audit_logs',
109 response = self.app.get(route_path('admin_audit_logs',
108 params=dict(filter='repository:*test*')))
110 params=dict(filter='repository:*test*')))
109 response.mustcontain('862 entries')
111 response.mustcontain('862 entries')
110
112
111 def test_filter_journal_filter_prefix_on_repository(self, autologin_user):
113 def test_filter_journal_filter_prefix_on_repository(self, autologin_user):
112 response = self.app.get(route_path('admin_audit_logs',
114 response = self.app.get(route_path('admin_audit_logs',
113 params=dict(filter='repository:test*')))
115 params=dict(filter='repository:test*')))
114 response.mustcontain('257 entries')
116 response.mustcontain('257 entries')
115
117
116 def test_filter_journal_filter_prefix_on_repository_CamelCase(self, autologin_user):
118 def test_filter_journal_filter_prefix_on_repository_CamelCase(self, autologin_user):
117 response = self.app.get(route_path('admin_audit_logs',
119 response = self.app.get(route_path('admin_audit_logs',
118 params=dict(filter='repository:Test*')))
120 params=dict(filter='repository:Test*')))
119 response.mustcontain('257 entries')
121 response.mustcontain('257 entries')
120
122
121 def test_filter_journal_filter_prefix_on_repository_and_user(self, autologin_user):
123 def test_filter_journal_filter_prefix_on_repository_and_user(self, autologin_user):
122 response = self.app.get(route_path('admin_audit_logs',
124 response = self.app.get(route_path('admin_audit_logs',
123 params=dict(filter='repository:test* AND username:demo')))
125 params=dict(filter='repository:test* AND username:demo')))
124 response.mustcontain('130 entries')
126 response.mustcontain('130 entries')
125
127
126 def test_filter_journal_filter_prefix_on_repository_or_target_repo(self, autologin_user):
128 def test_filter_journal_filter_prefix_on_repository_or_target_repo(self, autologin_user):
127 response = self.app.get(route_path('admin_audit_logs',
129 response = self.app.get(route_path('admin_audit_logs',
128 params=dict(filter='repository:test* OR repository:rhodecode')))
130 params=dict(filter='repository:test* OR repository:rhodecode')))
129 response.mustcontain('260 entries') # 257 + 3
131 response.mustcontain('260 entries') # 257 + 3
130
132
131 def test_filter_journal_filter_exact_match_on_username(self, autologin_user):
133 def test_filter_journal_filter_exact_match_on_username(self, autologin_user):
132 response = self.app.get(route_path('admin_audit_logs',
134 response = self.app.get(route_path('admin_audit_logs',
133 params=dict(filter='username:demo')))
135 params=dict(filter='username:demo')))
134 response.mustcontain('1087 entries')
136 response.mustcontain('1087 entries')
135
137
136 def test_filter_journal_filter_exact_match_on_username_camelCase(self, autologin_user):
138 def test_filter_journal_filter_exact_match_on_username_camelCase(self, autologin_user):
137 response = self.app.get(route_path('admin_audit_logs',
139 response = self.app.get(route_path('admin_audit_logs',
138 params=dict(filter='username:DemO')))
140 params=dict(filter='username:DemO')))
139 response.mustcontain('1087 entries')
141 response.mustcontain('1087 entries')
140
142
141 def test_filter_journal_filter_wildcard_on_username(self, autologin_user):
143 def test_filter_journal_filter_wildcard_on_username(self, autologin_user):
142 response = self.app.get(route_path('admin_audit_logs',
144 response = self.app.get(route_path('admin_audit_logs',
143 params=dict(filter='username:*test*')))
145 params=dict(filter='username:*test*')))
144 entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
146 entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
145 response.mustcontain('{} entries'.format(entries_count))
147 response.mustcontain('{} entries'.format(entries_count))
146
148
147 def test_filter_journal_filter_prefix_on_username(self, autologin_user):
149 def test_filter_journal_filter_prefix_on_username(self, autologin_user):
148 response = self.app.get(route_path('admin_audit_logs',
150 response = self.app.get(route_path('admin_audit_logs',
149 params=dict(filter='username:demo*')))
151 params=dict(filter='username:demo*')))
150 response.mustcontain('1101 entries')
152 response.mustcontain('1101 entries')
151
153
152 def test_filter_journal_filter_prefix_on_user_or_other_user(self, autologin_user):
154 def test_filter_journal_filter_prefix_on_user_or_other_user(self, autologin_user):
153 response = self.app.get(route_path('admin_audit_logs',
155 response = self.app.get(route_path('admin_audit_logs',
154 params=dict(filter='username:demo OR username:volcan')))
156 params=dict(filter='username:demo OR username:volcan')))
155 response.mustcontain('1095 entries') # 1087 + 8
157 response.mustcontain('1095 entries') # 1087 + 8
156
158
157 def test_filter_journal_filter_wildcard_on_action(self, autologin_user):
159 def test_filter_journal_filter_wildcard_on_action(self, autologin_user):
158 response = self.app.get(route_path('admin_audit_logs',
160 response = self.app.get(route_path('admin_audit_logs',
159 params=dict(filter='action:*pull_request*')))
161 params=dict(filter='action:*pull_request*')))
160 response.mustcontain('187 entries')
162 response.mustcontain('187 entries')
161
163
162 def test_filter_journal_filter_on_date(self, autologin_user):
164 def test_filter_journal_filter_on_date(self, autologin_user):
163 response = self.app.get(route_path('admin_audit_logs',
165 response = self.app.get(route_path('admin_audit_logs',
164 params=dict(filter='date:20121010')))
166 params=dict(filter='date:20121010')))
165 response.mustcontain('47 entries')
167 response.mustcontain('47 entries')
166
168
167 def test_filter_journal_filter_on_date_2(self, autologin_user):
169 def test_filter_journal_filter_on_date_2(self, autologin_user):
168 response = self.app.get(route_path('admin_audit_logs',
170 response = self.app.get(route_path('admin_audit_logs',
169 params=dict(filter='date:20121020')))
171 params=dict(filter='date:20121020')))
170 response.mustcontain('17 entries')
172 response.mustcontain('17 entries')
@@ -1,84 +1,86 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.tests import assert_session_flash
22 from rhodecode.tests import assert_session_flash
23 from rhodecode.model.settings import SettingsModel
23 from rhodecode.model.settings import SettingsModel
24
24
25
25
26 def route_path(name, params=None, **kwargs):
26 def route_path(name, params=None, **kwargs):
27 import urllib.request, urllib.parse, urllib.error
27 import urllib.request
28 import urllib.parse
29 import urllib.error
28 from rhodecode.apps._base import ADMIN_PREFIX
30 from rhodecode.apps._base import ADMIN_PREFIX
29
31
30 base_url = {
32 base_url = {
31 'admin_defaults_repositories':
33 'admin_defaults_repositories':
32 ADMIN_PREFIX + '/defaults/repositories',
34 ADMIN_PREFIX + '/defaults/repositories',
33 'admin_defaults_repositories_update':
35 'admin_defaults_repositories_update':
34 ADMIN_PREFIX + '/defaults/repositories/update',
36 ADMIN_PREFIX + '/defaults/repositories/update',
35 }[name].format(**kwargs)
37 }[name].format(**kwargs)
36
38
37 if params:
39 if params:
38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
39 return base_url
41 return base_url
40
42
41
43
42 @pytest.mark.usefixtures("app")
44 @pytest.mark.usefixtures("app")
43 class TestDefaultsView(object):
45 class TestDefaultsView(object):
44
46
45 def test_index(self, autologin_user):
47 def test_index(self, autologin_user):
46 response = self.app.get(route_path('admin_defaults_repositories'))
48 response = self.app.get(route_path('admin_defaults_repositories'))
47 response.mustcontain('default_repo_private')
49 response.mustcontain('default_repo_private')
48 response.mustcontain('default_repo_enable_statistics')
50 response.mustcontain('default_repo_enable_statistics')
49 response.mustcontain('default_repo_enable_downloads')
51 response.mustcontain('default_repo_enable_downloads')
50 response.mustcontain('default_repo_enable_locking')
52 response.mustcontain('default_repo_enable_locking')
51
53
52 def test_update_params_true_hg(self, autologin_user, csrf_token):
54 def test_update_params_true_hg(self, autologin_user, csrf_token):
53 params = {
55 params = {
54 'default_repo_enable_locking': True,
56 'default_repo_enable_locking': True,
55 'default_repo_enable_downloads': True,
57 'default_repo_enable_downloads': True,
56 'default_repo_enable_statistics': True,
58 'default_repo_enable_statistics': True,
57 'default_repo_private': True,
59 'default_repo_private': True,
58 'default_repo_type': 'hg',
60 'default_repo_type': 'hg',
59 'csrf_token': csrf_token,
61 'csrf_token': csrf_token,
60 }
62 }
61 response = self.app.post(
63 response = self.app.post(
62 route_path('admin_defaults_repositories_update'), params=params)
64 route_path('admin_defaults_repositories_update'), params=params)
63 assert_session_flash(response, 'Default settings updated successfully')
65 assert_session_flash(response, 'Default settings updated successfully')
64
66
65 defs = SettingsModel().get_default_repo_settings()
67 defs = SettingsModel().get_default_repo_settings()
66 del params['csrf_token']
68 del params['csrf_token']
67 assert params == defs
69 assert params == defs
68
70
69 def test_update_params_false_git(self, autologin_user, csrf_token):
71 def test_update_params_false_git(self, autologin_user, csrf_token):
70 params = {
72 params = {
71 'default_repo_enable_locking': False,
73 'default_repo_enable_locking': False,
72 'default_repo_enable_downloads': False,
74 'default_repo_enable_downloads': False,
73 'default_repo_enable_statistics': False,
75 'default_repo_enable_statistics': False,
74 'default_repo_private': False,
76 'default_repo_private': False,
75 'default_repo_type': 'git',
77 'default_repo_type': 'git',
76 'csrf_token': csrf_token,
78 'csrf_token': csrf_token,
77 }
79 }
78 response = self.app.post(
80 response = self.app.post(
79 route_path('admin_defaults_repositories_update'), params=params)
81 route_path('admin_defaults_repositories_update'), params=params)
80 assert_session_flash(response, 'Default settings updated successfully')
82 assert_session_flash(response, 'Default settings updated successfully')
81
83
82 defs = SettingsModel().get_default_repo_settings()
84 defs = SettingsModel().get_default_repo_settings()
83 del params['csrf_token']
85 del params['csrf_token']
84 assert params == defs
86 assert params == defs
@@ -1,81 +1,86 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.tests import TestController
22 from rhodecode.tests import TestController
23 from rhodecode.tests.fixture import Fixture
23 from rhodecode.tests.fixture import Fixture
24
24
25 fixture = Fixture()
25 fixture = Fixture()
26
26
27
27
28 def route_path(name, params=None, **kwargs):
28 def route_path(name, params=None, **kwargs):
29 import urllib.request, urllib.parse, urllib.error
29 import urllib.request
30 import urllib.parse
31 import urllib.error
30 from rhodecode.apps._base import ADMIN_PREFIX
32 from rhodecode.apps._base import ADMIN_PREFIX
31
33
32 base_url = {
34 base_url = {
33 'admin_home': ADMIN_PREFIX,
35 'admin_home': ADMIN_PREFIX,
34 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
36 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
35 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}',
37 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}',
36 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}',
38 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}',
37 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}',
39 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}',
38
40
39 }[name].format(**kwargs)
41 }[name].format(**kwargs)
40
42
41 if params:
43 if params:
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
44 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 return base_url
45 return base_url
44
46
45
47
46 class TestAdminMainView(TestController):
48 class TestAdminMainView(TestController):
47
49
48 def test_access_admin_home(self):
50 def test_access_admin_home(self):
49 self.log_user()
51 self.log_user()
50 response = self.app.get(route_path('admin_home'), status=200)
52 response = self.app.get(route_path('admin_home'), status=200)
51 response.mustcontain("Administration area")
53 response.mustcontain("Administration area")
52
54
53 def test_redirect_pull_request_view(self, view):
55 @pytest.mark.parametrize('view', [
56 'pull_requests_global',
57 ])
58 def test_redirect_pull_request_view_global(self, view):
54 self.log_user()
59 self.log_user()
55 self.app.get(
60 self.app.get(
56 route_path(view, pull_request_id='xxxx'),
61 route_path(view, pull_request_id='xxxx'),
57 status=404)
62 status=404)
58
63
59 @pytest.mark.backends("git", "hg")
64 @pytest.mark.backends("git", "hg")
60 @pytest.mark.parametrize('view', [
65 @pytest.mark.parametrize('view', [
61 'pull_requests_global',
66 'pull_requests_global',
62 'pull_requests_global_0',
67 'pull_requests_global_0',
63 'pull_requests_global_1',
68 'pull_requests_global_1',
64 ])
69 ])
65 def test_redirect_pull_request_view(self, view, pr_util):
70 def test_redirect_pull_request_view(self, view, pr_util):
66 self.log_user()
71 self.log_user()
67 pull_request = pr_util.create_pull_request()
72 pull_request = pr_util.create_pull_request()
68 pull_request_id = pull_request.pull_request_id
73 pull_request_id = pull_request.pull_request_id
69 repo_name = pull_request.target_repo.repo_name
74 repo_name = pull_request.target_repo.repo_name
70
75
71 response = self.app.get(
76 response = self.app.get(
72 route_path(view, pull_request_id=pull_request_id),
77 route_path(view, pull_request_id=pull_request_id),
73 status=302)
78 status=302)
74 assert response.location.endswith(
79 assert response.location.endswith(
75 'pull-request/{}'.format(pull_request_id))
80 'pull-request/{}'.format(pull_request_id))
76
81
77 redirect_url = route_path(
82 redirect_url = route_path(
78 'pullrequest_show', repo_name=repo_name,
83 'pullrequest_show', repo_name=repo_name,
79 pull_request_id=pull_request_id)
84 pull_request_id=pull_request_id)
80
85
81 assert redirect_url in response.location
86 assert redirect_url in response.location
@@ -1,298 +1,300 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import mock
20 import mock
21 import pytest
21 import pytest
22 from rhodecode.model.db import User, UserIpMap
22 from rhodecode.model.db import User, UserIpMap
23 from rhodecode.model.meta import Session
23 from rhodecode.model.meta import Session
24 from rhodecode.model.permission import PermissionModel
24 from rhodecode.model.permission import PermissionModel
25 from rhodecode.model.ssh_key import SshKeyModel
25 from rhodecode.model.ssh_key import SshKeyModel
26 from rhodecode.tests import (
26 from rhodecode.tests import (
27 TestController, clear_cache_regions, assert_session_flash)
27 TestController, clear_cache_regions, assert_session_flash)
28
28
29
29
30 def route_path(name, params=None, **kwargs):
30 def route_path(name, params=None, **kwargs):
31 import urllib.request, urllib.parse, urllib.error
31 import urllib.request
32 import urllib.parse
33 import urllib.error
32 from rhodecode.apps._base import ADMIN_PREFIX
34 from rhodecode.apps._base import ADMIN_PREFIX
33
35
34 base_url = {
36 base_url = {
35 'edit_user_ips':
37 'edit_user_ips':
36 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
38 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
37 'edit_user_ips_add':
39 'edit_user_ips_add':
38 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
40 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
39 'edit_user_ips_delete':
41 'edit_user_ips_delete':
40 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
42 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
41
43
42 'admin_permissions_application':
44 'admin_permissions_application':
43 ADMIN_PREFIX + '/permissions/application',
45 ADMIN_PREFIX + '/permissions/application',
44 'admin_permissions_application_update':
46 'admin_permissions_application_update':
45 ADMIN_PREFIX + '/permissions/application/update',
47 ADMIN_PREFIX + '/permissions/application/update',
46
48
47 'admin_permissions_global':
49 'admin_permissions_global':
48 ADMIN_PREFIX + '/permissions/global',
50 ADMIN_PREFIX + '/permissions/global',
49 'admin_permissions_global_update':
51 'admin_permissions_global_update':
50 ADMIN_PREFIX + '/permissions/global/update',
52 ADMIN_PREFIX + '/permissions/global/update',
51
53
52 'admin_permissions_object':
54 'admin_permissions_object':
53 ADMIN_PREFIX + '/permissions/object',
55 ADMIN_PREFIX + '/permissions/object',
54 'admin_permissions_object_update':
56 'admin_permissions_object_update':
55 ADMIN_PREFIX + '/permissions/object/update',
57 ADMIN_PREFIX + '/permissions/object/update',
56
58
57 'admin_permissions_ips':
59 'admin_permissions_ips':
58 ADMIN_PREFIX + '/permissions/ips',
60 ADMIN_PREFIX + '/permissions/ips',
59 'admin_permissions_overview':
61 'admin_permissions_overview':
60 ADMIN_PREFIX + '/permissions/overview',
62 ADMIN_PREFIX + '/permissions/overview',
61
63
62 'admin_permissions_ssh_keys':
64 'admin_permissions_ssh_keys':
63 ADMIN_PREFIX + '/permissions/ssh_keys',
65 ADMIN_PREFIX + '/permissions/ssh_keys',
64 'admin_permissions_ssh_keys_data':
66 'admin_permissions_ssh_keys_data':
65 ADMIN_PREFIX + '/permissions/ssh_keys/data',
67 ADMIN_PREFIX + '/permissions/ssh_keys/data',
66 'admin_permissions_ssh_keys_update':
68 'admin_permissions_ssh_keys_update':
67 ADMIN_PREFIX + '/permissions/ssh_keys/update'
69 ADMIN_PREFIX + '/permissions/ssh_keys/update'
68
70
69 }[name].format(**kwargs)
71 }[name].format(**kwargs)
70
72
71 if params:
73 if params:
72 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
74 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
73 return base_url
75 return base_url
74
76
75
77
76 class TestAdminPermissionsController(TestController):
78 class TestAdminPermissionsController(TestController):
77
79
78 @pytest.fixture(scope='class', autouse=True)
80 @pytest.fixture(scope='class', autouse=True)
79 def prepare(self, request):
81 def prepare(self, request):
80 # cleanup and reset to default permissions after
82 # cleanup and reset to default permissions after
81 @request.addfinalizer
83 @request.addfinalizer
82 def cleanup():
84 def cleanup():
83 PermissionModel().create_default_user_permissions(
85 PermissionModel().create_default_user_permissions(
84 User.get_default_user(), force=True)
86 User.get_default_user(), force=True)
85
87
86 def test_index_application(self):
88 def test_index_application(self):
87 self.log_user()
89 self.log_user()
88 self.app.get(route_path('admin_permissions_application'))
90 self.app.get(route_path('admin_permissions_application'))
89
91
90 @pytest.mark.parametrize(
92 @pytest.mark.parametrize(
91 'anonymous, default_register, default_register_message, default_password_reset,'
93 'anonymous, default_register, default_register_message, default_password_reset,'
92 'default_extern_activate, expect_error, expect_form_error', [
94 'default_extern_activate, expect_error, expect_form_error', [
93 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
95 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
94 False, False),
96 False, False),
95 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
97 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
96 False, False),
98 False, False),
97 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
99 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
98 False, False),
100 False, False),
99 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
101 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
100 False, False),
102 False, False),
101 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
103 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
102 False, True),
104 False, True),
103 (True, '', '', 'hg.password_reset.enabled', '', True, False),
105 (True, '', '', 'hg.password_reset.enabled', '', True, False),
104 ])
106 ])
105 def test_update_application_permissions(
107 def test_update_application_permissions(
106 self, anonymous, default_register, default_register_message, default_password_reset,
108 self, anonymous, default_register, default_register_message, default_password_reset,
107 default_extern_activate, expect_error, expect_form_error):
109 default_extern_activate, expect_error, expect_form_error):
108
110
109 self.log_user()
111 self.log_user()
110
112
111 # TODO: anonymous access set here to False, breaks some other tests
113 # TODO: anonymous access set here to False, breaks some other tests
112 params = {
114 params = {
113 'csrf_token': self.csrf_token,
115 'csrf_token': self.csrf_token,
114 'anonymous': anonymous,
116 'anonymous': anonymous,
115 'default_register': default_register,
117 'default_register': default_register,
116 'default_register_message': default_register_message,
118 'default_register_message': default_register_message,
117 'default_password_reset': default_password_reset,
119 'default_password_reset': default_password_reset,
118 'default_extern_activate': default_extern_activate,
120 'default_extern_activate': default_extern_activate,
119 }
121 }
120 response = self.app.post(route_path('admin_permissions_application_update'),
122 response = self.app.post(route_path('admin_permissions_application_update'),
121 params=params)
123 params=params)
122 if expect_form_error:
124 if expect_form_error:
123 assert response.status_int == 200
125 assert response.status_int == 200
124 response.mustcontain('Value must be one of')
126 response.mustcontain('Value must be one of')
125 else:
127 else:
126 if expect_error:
128 if expect_error:
127 msg = 'Error occurred during update of permissions'
129 msg = 'Error occurred during update of permissions'
128 else:
130 else:
129 msg = 'Application permissions updated successfully'
131 msg = 'Application permissions updated successfully'
130 assert_session_flash(response, msg)
132 assert_session_flash(response, msg)
131
133
132 def test_index_object(self):
134 def test_index_object(self):
133 self.log_user()
135 self.log_user()
134 self.app.get(route_path('admin_permissions_object'))
136 self.app.get(route_path('admin_permissions_object'))
135
137
136 @pytest.mark.parametrize(
138 @pytest.mark.parametrize(
137 'repo, repo_group, user_group, expect_error, expect_form_error', [
139 'repo, repo_group, user_group, expect_error, expect_form_error', [
138 ('repository.none', 'group.none', 'usergroup.none', False, False),
140 ('repository.none', 'group.none', 'usergroup.none', False, False),
139 ('repository.read', 'group.read', 'usergroup.read', False, False),
141 ('repository.read', 'group.read', 'usergroup.read', False, False),
140 ('repository.write', 'group.write', 'usergroup.write',
142 ('repository.write', 'group.write', 'usergroup.write',
141 False, False),
143 False, False),
142 ('repository.admin', 'group.admin', 'usergroup.admin',
144 ('repository.admin', 'group.admin', 'usergroup.admin',
143 False, False),
145 False, False),
144 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
146 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
145 ('', '', '', True, False),
147 ('', '', '', True, False),
146 ])
148 ])
147 def test_update_object_permissions(self, repo, repo_group, user_group,
149 def test_update_object_permissions(self, repo, repo_group, user_group,
148 expect_error, expect_form_error):
150 expect_error, expect_form_error):
149 self.log_user()
151 self.log_user()
150
152
151 params = {
153 params = {
152 'csrf_token': self.csrf_token,
154 'csrf_token': self.csrf_token,
153 'default_repo_perm': repo,
155 'default_repo_perm': repo,
154 'overwrite_default_repo': False,
156 'overwrite_default_repo': False,
155 'default_group_perm': repo_group,
157 'default_group_perm': repo_group,
156 'overwrite_default_group': False,
158 'overwrite_default_group': False,
157 'default_user_group_perm': user_group,
159 'default_user_group_perm': user_group,
158 'overwrite_default_user_group': False,
160 'overwrite_default_user_group': False,
159 }
161 }
160 response = self.app.post(route_path('admin_permissions_object_update'),
162 response = self.app.post(route_path('admin_permissions_object_update'),
161 params=params)
163 params=params)
162 if expect_form_error:
164 if expect_form_error:
163 assert response.status_int == 200
165 assert response.status_int == 200
164 response.mustcontain('Value must be one of')
166 response.mustcontain('Value must be one of')
165 else:
167 else:
166 if expect_error:
168 if expect_error:
167 msg = 'Error occurred during update of permissions'
169 msg = 'Error occurred during update of permissions'
168 else:
170 else:
169 msg = 'Object permissions updated successfully'
171 msg = 'Object permissions updated successfully'
170 assert_session_flash(response, msg)
172 assert_session_flash(response, msg)
171
173
172 def test_index_global(self):
174 def test_index_global(self):
173 self.log_user()
175 self.log_user()
174 self.app.get(route_path('admin_permissions_global'))
176 self.app.get(route_path('admin_permissions_global'))
175
177
176 @pytest.mark.parametrize(
178 @pytest.mark.parametrize(
177 'repo_create, repo_create_write, user_group_create, repo_group_create,'
179 'repo_create, repo_create_write, user_group_create, repo_group_create,'
178 'fork_create, inherit_default_permissions, expect_error,'
180 'fork_create, inherit_default_permissions, expect_error,'
179 'expect_form_error', [
181 'expect_form_error', [
180 ('hg.create.none', 'hg.create.write_on_repogroup.false',
182 ('hg.create.none', 'hg.create.write_on_repogroup.false',
181 'hg.usergroup.create.false', 'hg.repogroup.create.false',
183 'hg.usergroup.create.false', 'hg.repogroup.create.false',
182 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
184 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
183 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
185 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
184 'hg.usergroup.create.true', 'hg.repogroup.create.true',
186 'hg.usergroup.create.true', 'hg.repogroup.create.true',
185 'hg.fork.repository', 'hg.inherit_default_perms.false',
187 'hg.fork.repository', 'hg.inherit_default_perms.false',
186 False, False),
188 False, False),
187 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
189 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
188 'hg.usergroup.create.true', 'hg.repogroup.create.true',
190 'hg.usergroup.create.true', 'hg.repogroup.create.true',
189 'hg.fork.repository', 'hg.inherit_default_perms.false',
191 'hg.fork.repository', 'hg.inherit_default_perms.false',
190 False, True),
192 False, True),
191 ('', '', '', '', '', '', True, False),
193 ('', '', '', '', '', '', True, False),
192 ])
194 ])
193 def test_update_global_permissions(
195 def test_update_global_permissions(
194 self, repo_create, repo_create_write, user_group_create,
196 self, repo_create, repo_create_write, user_group_create,
195 repo_group_create, fork_create, inherit_default_permissions,
197 repo_group_create, fork_create, inherit_default_permissions,
196 expect_error, expect_form_error):
198 expect_error, expect_form_error):
197 self.log_user()
199 self.log_user()
198
200
199 params = {
201 params = {
200 'csrf_token': self.csrf_token,
202 'csrf_token': self.csrf_token,
201 'default_repo_create': repo_create,
203 'default_repo_create': repo_create,
202 'default_repo_create_on_write': repo_create_write,
204 'default_repo_create_on_write': repo_create_write,
203 'default_user_group_create': user_group_create,
205 'default_user_group_create': user_group_create,
204 'default_repo_group_create': repo_group_create,
206 'default_repo_group_create': repo_group_create,
205 'default_fork_create': fork_create,
207 'default_fork_create': fork_create,
206 'default_inherit_default_permissions': inherit_default_permissions
208 'default_inherit_default_permissions': inherit_default_permissions
207 }
209 }
208 response = self.app.post(route_path('admin_permissions_global_update'),
210 response = self.app.post(route_path('admin_permissions_global_update'),
209 params=params)
211 params=params)
210 if expect_form_error:
212 if expect_form_error:
211 assert response.status_int == 200
213 assert response.status_int == 200
212 response.mustcontain('Value must be one of')
214 response.mustcontain('Value must be one of')
213 else:
215 else:
214 if expect_error:
216 if expect_error:
215 msg = 'Error occurred during update of permissions'
217 msg = 'Error occurred during update of permissions'
216 else:
218 else:
217 msg = 'Global permissions updated successfully'
219 msg = 'Global permissions updated successfully'
218 assert_session_flash(response, msg)
220 assert_session_flash(response, msg)
219
221
220 def test_index_ips(self):
222 def test_index_ips(self):
221 self.log_user()
223 self.log_user()
222 response = self.app.get(route_path('admin_permissions_ips'))
224 response = self.app.get(route_path('admin_permissions_ips'))
223 response.mustcontain('All IP addresses are allowed')
225 response.mustcontain('All IP addresses are allowed')
224
226
225 def test_add_delete_ips(self):
227 def test_add_delete_ips(self):
226 clear_cache_regions(['sql_cache_short'])
228 clear_cache_regions(['sql_cache_short'])
227 self.log_user()
229 self.log_user()
228
230
229 # ADD
231 # ADD
230 default_user_id = User.get_default_user_id()
232 default_user_id = User.get_default_user_id()
231 self.app.post(
233 self.app.post(
232 route_path('edit_user_ips_add', user_id=default_user_id),
234 route_path('edit_user_ips_add', user_id=default_user_id),
233 params={'new_ip': '0.0.0.0/24', 'csrf_token': self.csrf_token})
235 params={'new_ip': '0.0.0.0/24', 'csrf_token': self.csrf_token})
234
236
235 response = self.app.get(route_path('admin_permissions_ips'))
237 response = self.app.get(route_path('admin_permissions_ips'))
236 response.mustcontain('0.0.0.0/24')
238 response.mustcontain('0.0.0.0/24')
237 response.mustcontain('0.0.0.0 - 0.0.0.255')
239 response.mustcontain('0.0.0.0 - 0.0.0.255')
238
240
239 # DELETE
241 # DELETE
240 default_user_id = User.get_default_user_id()
242 default_user_id = User.get_default_user_id()
241 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
243 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
242 default_user_id).first().ip_id
244 default_user_id).first().ip_id
243
245
244 response = self.app.post(
246 response = self.app.post(
245 route_path('edit_user_ips_delete', user_id=default_user_id),
247 route_path('edit_user_ips_delete', user_id=default_user_id),
246 params={'del_ip_id': del_ip_id, 'csrf_token': self.csrf_token})
248 params={'del_ip_id': del_ip_id, 'csrf_token': self.csrf_token})
247
249
248 assert_session_flash(response, 'Removed ip address from user whitelist')
250 assert_session_flash(response, 'Removed ip address from user whitelist')
249
251
250 clear_cache_regions(['sql_cache_short'])
252 clear_cache_regions(['sql_cache_short'])
251 response = self.app.get(route_path('admin_permissions_ips'))
253 response = self.app.get(route_path('admin_permissions_ips'))
252 response.mustcontain('All IP addresses are allowed')
254 response.mustcontain('All IP addresses are allowed')
253 response.mustcontain(no=['0.0.0.0/24'])
255 response.mustcontain(no=['0.0.0.0/24'])
254 response.mustcontain(no=['0.0.0.0 - 0.0.0.255'])
256 response.mustcontain(no=['0.0.0.0 - 0.0.0.255'])
255
257
256 def test_index_overview(self):
258 def test_index_overview(self):
257 self.log_user()
259 self.log_user()
258 self.app.get(route_path('admin_permissions_overview'))
260 self.app.get(route_path('admin_permissions_overview'))
259
261
260 def test_ssh_keys(self):
262 def test_ssh_keys(self):
261 self.log_user()
263 self.log_user()
262 self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
264 self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
263
265
264 def test_ssh_keys_data(self, user_util, xhr_header):
266 def test_ssh_keys_data(self, user_util, xhr_header):
265 self.log_user()
267 self.log_user()
266 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
268 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
267 extra_environ=xhr_header)
269 extra_environ=xhr_header)
268 assert response.json == {u'data': [], u'draw': None,
270 assert response.json == {u'data': [], u'draw': None,
269 u'recordsFiltered': 0, u'recordsTotal': 0}
271 u'recordsFiltered': 0, u'recordsTotal': 0}
270
272
271 dummy_user = user_util.create_user()
273 dummy_user = user_util.create_user()
272 SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
274 SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
273 Session().commit()
275 Session().commit()
274 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
276 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
275 extra_environ=xhr_header)
277 extra_environ=xhr_header)
276 assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
278 assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
277
279
278 def test_ssh_keys_update(self):
280 def test_ssh_keys_update(self):
279 self.log_user()
281 self.log_user()
280 response = self.app.post(
282 response = self.app.post(
281 route_path('admin_permissions_ssh_keys_update'),
283 route_path('admin_permissions_ssh_keys_update'),
282 dict(csrf_token=self.csrf_token), status=302)
284 dict(csrf_token=self.csrf_token), status=302)
283
285
284 assert_session_flash(
286 assert_session_flash(
285 response, 'Updated SSH keys file')
287 response, 'Updated SSH keys file')
286
288
287 def test_ssh_keys_update_disabled(self):
289 def test_ssh_keys_update_disabled(self):
288 self.log_user()
290 self.log_user()
289
291
290 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
292 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
291 with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
293 with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
292 return_value=False):
294 return_value=False):
293 response = self.app.post(
295 response = self.app.post(
294 route_path('admin_permissions_ssh_keys_update'),
296 route_path('admin_permissions_ssh_keys_update'),
295 dict(csrf_token=self.csrf_token), status=302)
297 dict(csrf_token=self.csrf_token), status=302)
296
298
297 assert_session_flash(
299 assert_session_flash(
298 response, 'SSH key support is disabled in .ini file') No newline at end of file
300 response, 'SSH key support is disabled in .ini file')
@@ -1,511 +1,515 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import urllib.request, urllib.parse, urllib.error
20 import urllib.request
21 import urllib.parse
22 import urllib.error
21
23
22 import mock
24 import mock
23 import pytest
25 import pytest
24
26
25 from rhodecode.apps._base import ADMIN_PREFIX
27 from rhodecode.apps._base import ADMIN_PREFIX
26 from rhodecode.lib import auth
28 from rhodecode.lib import auth
27 from rhodecode.lib.utils2 import safe_str
29 from rhodecode.lib.utils2 import safe_str
28 from rhodecode.lib import helpers as h
30 from rhodecode.lib import helpers as h
29 from rhodecode.model.db import (
31 from rhodecode.model.db import (
30 Repository, RepoGroup, UserRepoToPerm, User, Permission)
32 Repository, RepoGroup, UserRepoToPerm, User, Permission)
31 from rhodecode.model.meta import Session
33 from rhodecode.model.meta import Session
32 from rhodecode.model.repo import RepoModel
34 from rhodecode.model.repo import RepoModel
33 from rhodecode.model.repo_group import RepoGroupModel
35 from rhodecode.model.repo_group import RepoGroupModel
34 from rhodecode.model.user import UserModel
36 from rhodecode.model.user import UserModel
35 from rhodecode.tests import (
37 from rhodecode.tests import (
36 login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
38 login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
37 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
39 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
38 from rhodecode.tests.fixture import Fixture, error_function
40 from rhodecode.tests.fixture import Fixture, error_function
39 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
41 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
40
42
41 fixture = Fixture()
43 fixture = Fixture()
42
44
43
45
44 def route_path(name, params=None, **kwargs):
46 def route_path(name, params=None, **kwargs):
45 import urllib.request, urllib.parse, urllib.error
47 import urllib.request
48 import urllib.parse
49 import urllib.error
46
50
47 base_url = {
51 base_url = {
48 'repos': ADMIN_PREFIX + '/repos',
52 'repos': ADMIN_PREFIX + '/repos',
49 'repos_data': ADMIN_PREFIX + '/repos_data',
53 'repos_data': ADMIN_PREFIX + '/repos_data',
50 'repo_new': ADMIN_PREFIX + '/repos/new',
54 'repo_new': ADMIN_PREFIX + '/repos/new',
51 'repo_create': ADMIN_PREFIX + '/repos/create',
55 'repo_create': ADMIN_PREFIX + '/repos/create',
52
56
53 'repo_creating_check': '/{repo_name}/repo_creating_check',
57 'repo_creating_check': '/{repo_name}/repo_creating_check',
54 }[name].format(**kwargs)
58 }[name].format(**kwargs)
55
59
56 if params:
60 if params:
57 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
61 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
58 return base_url
62 return base_url
59
63
60
64
61 def _get_permission_for_user(user, repo):
65 def _get_permission_for_user(user, repo):
62 perm = UserRepoToPerm.query()\
66 perm = UserRepoToPerm.query()\
63 .filter(UserRepoToPerm.repository ==
67 .filter(UserRepoToPerm.repository ==
64 Repository.get_by_repo_name(repo))\
68 Repository.get_by_repo_name(repo))\
65 .filter(UserRepoToPerm.user == User.get_by_username(user))\
69 .filter(UserRepoToPerm.user == User.get_by_username(user))\
66 .all()
70 .all()
67 return perm
71 return perm
68
72
69
73
70 @pytest.mark.usefixtures("app")
74 @pytest.mark.usefixtures("app")
71 class TestAdminRepos(object):
75 class TestAdminRepos(object):
72
76
73 def test_repo_list(self, autologin_user, user_util, xhr_header):
77 def test_repo_list(self, autologin_user, user_util, xhr_header):
74 repo = user_util.create_repo()
78 repo = user_util.create_repo()
75 repo_name = repo.repo_name
79 repo_name = repo.repo_name
76 response = self.app.get(
80 response = self.app.get(
77 route_path('repos_data'), status=200,
81 route_path('repos_data'), status=200,
78 extra_environ=xhr_header)
82 extra_environ=xhr_header)
79
83
80 response.mustcontain(repo_name)
84 response.mustcontain(repo_name)
81
85
82 def test_create_page_restricted_to_single_backend(self, autologin_user, backend):
86 def test_create_page_restricted_to_single_backend(self, autologin_user, backend):
83 with mock.patch('rhodecode.BACKENDS', {'git': 'git'}):
87 with mock.patch('rhodecode.BACKENDS', {'git': 'git'}):
84 response = self.app.get(route_path('repo_new'), status=200)
88 response = self.app.get(route_path('repo_new'), status=200)
85 assert_response = response.assert_response()
89 assert_response = response.assert_response()
86 element = assert_response.get_element('[name=repo_type]')
90 element = assert_response.get_element('[name=repo_type]')
87 assert element.get('value') == 'git'
91 assert element.get('value') == 'git'
88
92
89 def test_create_page_non_restricted_backends(self, autologin_user, backend):
93 def test_create_page_non_restricted_backends(self, autologin_user, backend):
90 response = self.app.get(route_path('repo_new'), status=200)
94 response = self.app.get(route_path('repo_new'), status=200)
91 assert_response = response.assert_response()
95 assert_response = response.assert_response()
92 assert ['hg', 'git', 'svn'] == [x.get('value') for x in assert_response.get_elements('[name=repo_type]')]
96 assert ['hg', 'git', 'svn'] == [x.get('value') for x in assert_response.get_elements('[name=repo_type]')]
93
97
94 @pytest.mark.parametrize(
98 @pytest.mark.parametrize(
95 "suffix", [u'', u'xxa'], ids=['', 'non-ascii'])
99 "suffix", ['', 'xxa'], ids=['', 'non-ascii'])
96 def test_create(self, autologin_user, backend, suffix, csrf_token):
100 def test_create(self, autologin_user, backend, suffix, csrf_token):
97 repo_name_unicode = backend.new_repo_name(suffix=suffix)
101 repo_name_unicode = backend.new_repo_name(suffix=suffix)
98 repo_name = repo_name_unicode.encode('utf8')
102 repo_name = repo_name_unicode
99 description_unicode = u'description for newly created repo' + suffix
103
100 description = description_unicode.encode('utf8')
104 description_unicode = 'description for newly created repo' + suffix
105 description = description_unicode
106
101 response = self.app.post(
107 response = self.app.post(
102 route_path('repo_create'),
108 route_path('repo_create'),
103 fixture._get_repo_create_params(
109 fixture._get_repo_create_params(
104 repo_private=False,
110 repo_private=False,
105 repo_name=repo_name,
111 repo_name=repo_name,
106 repo_type=backend.alias,
112 repo_type=backend.alias,
107 repo_description=description,
113 repo_description=description,
108 csrf_token=csrf_token),
114 csrf_token=csrf_token),
109 status=302)
115 status=302)
110
116
111 self.assert_repository_is_created_correctly(
117 self.assert_repository_is_created_correctly(
112 repo_name, description, backend)
118 repo_name, description, backend)
113
119
114 def test_create_numeric_name(self, autologin_user, backend, csrf_token):
120 def test_create_numeric_name(self, autologin_user, backend, csrf_token):
115 numeric_repo = '1234'
121 numeric_repo = '1234'
116 repo_name = numeric_repo
122 repo_name = numeric_repo
117 description = 'description for newly created repo' + numeric_repo
123 description = 'description for newly created repo' + numeric_repo
118 self.app.post(
124 self.app.post(
119 route_path('repo_create'),
125 route_path('repo_create'),
120 fixture._get_repo_create_params(
126 fixture._get_repo_create_params(
121 repo_private=False,
127 repo_private=False,
122 repo_name=repo_name,
128 repo_name=repo_name,
123 repo_type=backend.alias,
129 repo_type=backend.alias,
124 repo_description=description,
130 repo_description=description,
125 csrf_token=csrf_token))
131 csrf_token=csrf_token))
126
132
127 self.assert_repository_is_created_correctly(
133 self.assert_repository_is_created_correctly(
128 repo_name, description, backend)
134 repo_name, description, backend)
129
135
130 @pytest.mark.parametrize("suffix", [u'', u'ąćę'], ids=['', 'non-ascii'])
136 @pytest.mark.parametrize("suffix", ['', '_ąćę'], ids=['', 'non-ascii'])
131 def test_create_in_group(
137 def test_create_in_group(
132 self, autologin_user, backend, suffix, csrf_token):
138 self, autologin_user, backend, suffix, csrf_token):
133 # create GROUP
139 # create GROUP
134 group_name = 'sometest_%s' % backend.alias
140 group_name = f'sometest_{backend.alias}'
135 gr = RepoGroupModel().create(group_name=group_name,
141 gr = RepoGroupModel().create(group_name=group_name,
136 group_description='test',
142 group_description='test',
137 owner=TEST_USER_ADMIN_LOGIN)
143 owner=TEST_USER_ADMIN_LOGIN)
138 Session().commit()
144 Session().commit()
139
145
140 repo_name = u'ingroup' + suffix
146 repo_name = f'ingroup{suffix}'
141 repo_name_full = RepoGroup.url_sep().join(
147 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
142 [group_name, repo_name])
148 description = 'description for newly created repo'
143 description = u'description for newly created repo'
149
144 self.app.post(
150 self.app.post(
145 route_path('repo_create'),
151 route_path('repo_create'),
146 fixture._get_repo_create_params(
152 fixture._get_repo_create_params(
147 repo_private=False,
153 repo_private=False,
148 repo_name=safe_str(repo_name),
154 repo_name=safe_str(repo_name),
149 repo_type=backend.alias,
155 repo_type=backend.alias,
150 repo_description=description,
156 repo_description=description,
151 repo_group=gr.group_id,
157 repo_group=gr.group_id,
152 csrf_token=csrf_token))
158 csrf_token=csrf_token))
153
159
154 # TODO: johbo: Cleanup work to fixture
160 # TODO: johbo: Cleanup work to fixture
155 try:
161 try:
156 self.assert_repository_is_created_correctly(
162 self.assert_repository_is_created_correctly(
157 repo_name_full, description, backend)
163 repo_name_full, description, backend)
158
164
159 new_repo = RepoModel().get_by_repo_name(repo_name_full)
165 new_repo = RepoModel().get_by_repo_name(repo_name_full)
160 inherited_perms = UserRepoToPerm.query().filter(
166 inherited_perms = UserRepoToPerm.query().filter(
161 UserRepoToPerm.repository_id == new_repo.repo_id).all()
167 UserRepoToPerm.repository_id == new_repo.repo_id).all()
162 assert len(inherited_perms) == 1
168 assert len(inherited_perms) == 1
163 finally:
169 finally:
164 RepoModel().delete(repo_name_full)
170 RepoModel().delete(repo_name_full)
165 RepoGroupModel().delete(group_name)
171 RepoGroupModel().delete(group_name)
166 Session().commit()
172 Session().commit()
167
173
168 def test_create_in_group_numeric_name(
174 def test_create_in_group_numeric_name(
169 self, autologin_user, backend, csrf_token):
175 self, autologin_user, backend, csrf_token):
170 # create GROUP
176 # create GROUP
171 group_name = 'sometest_%s' % backend.alias
177 group_name = 'sometest_%s' % backend.alias
172 gr = RepoGroupModel().create(group_name=group_name,
178 gr = RepoGroupModel().create(group_name=group_name,
173 group_description='test',
179 group_description='test',
174 owner=TEST_USER_ADMIN_LOGIN)
180 owner=TEST_USER_ADMIN_LOGIN)
175 Session().commit()
181 Session().commit()
176
182
177 repo_name = '12345'
183 repo_name = '12345'
178 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
184 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
179 description = 'description for newly created repo'
185 description = 'description for newly created repo'
180 self.app.post(
186 self.app.post(
181 route_path('repo_create'),
187 route_path('repo_create'),
182 fixture._get_repo_create_params(
188 fixture._get_repo_create_params(
183 repo_private=False,
189 repo_private=False,
184 repo_name=repo_name,
190 repo_name=repo_name,
185 repo_type=backend.alias,
191 repo_type=backend.alias,
186 repo_description=description,
192 repo_description=description,
187 repo_group=gr.group_id,
193 repo_group=gr.group_id,
188 csrf_token=csrf_token))
194 csrf_token=csrf_token))
189
195
190 # TODO: johbo: Cleanup work to fixture
196 # TODO: johbo: Cleanup work to fixture
191 try:
197 try:
192 self.assert_repository_is_created_correctly(
198 self.assert_repository_is_created_correctly(
193 repo_name_full, description, backend)
199 repo_name_full, description, backend)
194
200
195 new_repo = RepoModel().get_by_repo_name(repo_name_full)
201 new_repo = RepoModel().get_by_repo_name(repo_name_full)
196 inherited_perms = UserRepoToPerm.query()\
202 inherited_perms = UserRepoToPerm.query()\
197 .filter(UserRepoToPerm.repository_id == new_repo.repo_id).all()
203 .filter(UserRepoToPerm.repository_id == new_repo.repo_id).all()
198 assert len(inherited_perms) == 1
204 assert len(inherited_perms) == 1
199 finally:
205 finally:
200 RepoModel().delete(repo_name_full)
206 RepoModel().delete(repo_name_full)
201 RepoGroupModel().delete(group_name)
207 RepoGroupModel().delete(group_name)
202 Session().commit()
208 Session().commit()
203
209
204 def test_create_in_group_without_needed_permissions(self, backend):
210 def test_create_in_group_without_needed_permissions(self, backend):
205 session = login_user_session(
211 session = login_user_session(
206 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
212 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
207 csrf_token = auth.get_csrf_token(session)
213 csrf_token = auth.get_csrf_token(session)
208 # revoke
214 # revoke
209 user_model = UserModel()
215 user_model = UserModel()
210 # disable fork and create on default user
216 # disable fork and create on default user
211 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
217 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
212 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
218 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
213 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
219 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
214 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
220 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
215
221
216 # disable on regular user
222 # disable on regular user
217 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
223 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
218 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
224 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
219 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
225 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
220 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
226 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
221 Session().commit()
227 Session().commit()
222
228
223 # create GROUP
229 # create GROUP
224 group_name = 'reg_sometest_%s' % backend.alias
230 group_name = 'reg_sometest_%s' % backend.alias
225 gr = RepoGroupModel().create(group_name=group_name,
231 gr = RepoGroupModel().create(group_name=group_name,
226 group_description='test',
232 group_description='test',
227 owner=TEST_USER_ADMIN_LOGIN)
233 owner=TEST_USER_ADMIN_LOGIN)
228 Session().commit()
234 Session().commit()
229 repo_group_id = gr.group_id
235 repo_group_id = gr.group_id
230
236
231 group_name_allowed = 'reg_sometest_allowed_%s' % backend.alias
237 group_name_allowed = 'reg_sometest_allowed_%s' % backend.alias
232 gr_allowed = RepoGroupModel().create(
238 gr_allowed = RepoGroupModel().create(
233 group_name=group_name_allowed,
239 group_name=group_name_allowed,
234 group_description='test',
240 group_description='test',
235 owner=TEST_USER_REGULAR_LOGIN)
241 owner=TEST_USER_REGULAR_LOGIN)
236 allowed_repo_group_id = gr_allowed.group_id
242 allowed_repo_group_id = gr_allowed.group_id
237 Session().commit()
243 Session().commit()
238
244
239 repo_name = 'ingroup'
245 repo_name = 'ingroup'
240 description = 'description for newly created repo'
246 description = 'description for newly created repo'
241 response = self.app.post(
247 response = self.app.post(
242 route_path('repo_create'),
248 route_path('repo_create'),
243 fixture._get_repo_create_params(
249 fixture._get_repo_create_params(
244 repo_private=False,
250 repo_private=False,
245 repo_name=repo_name,
251 repo_name=repo_name,
246 repo_type=backend.alias,
252 repo_type=backend.alias,
247 repo_description=description,
253 repo_description=description,
248 repo_group=repo_group_id,
254 repo_group=repo_group_id,
249 csrf_token=csrf_token))
255 csrf_token=csrf_token))
250
256
251 response.mustcontain('Invalid value')
257 response.mustcontain('Invalid value')
252
258
253 # user is allowed to create in this group
259 # user is allowed to create in this group
254 repo_name = 'ingroup'
260 repo_name = 'ingroup'
255 repo_name_full = RepoGroup.url_sep().join(
261 repo_name_full = RepoGroup.url_sep().join(
256 [group_name_allowed, repo_name])
262 [group_name_allowed, repo_name])
257 description = 'description for newly created repo'
263 description = 'description for newly created repo'
258 response = self.app.post(
264 response = self.app.post(
259 route_path('repo_create'),
265 route_path('repo_create'),
260 fixture._get_repo_create_params(
266 fixture._get_repo_create_params(
261 repo_private=False,
267 repo_private=False,
262 repo_name=repo_name,
268 repo_name=repo_name,
263 repo_type=backend.alias,
269 repo_type=backend.alias,
264 repo_description=description,
270 repo_description=description,
265 repo_group=allowed_repo_group_id,
271 repo_group=allowed_repo_group_id,
266 csrf_token=csrf_token))
272 csrf_token=csrf_token))
267
273
268 # TODO: johbo: Cleanup in pytest fixture
274 # TODO: johbo: Cleanup in pytest fixture
269 try:
275 try:
270 self.assert_repository_is_created_correctly(
276 self.assert_repository_is_created_correctly(
271 repo_name_full, description, backend)
277 repo_name_full, description, backend)
272
278
273 new_repo = RepoModel().get_by_repo_name(repo_name_full)
279 new_repo = RepoModel().get_by_repo_name(repo_name_full)
274 inherited_perms = UserRepoToPerm.query().filter(
280 inherited_perms = UserRepoToPerm.query().filter(
275 UserRepoToPerm.repository_id == new_repo.repo_id).all()
281 UserRepoToPerm.repository_id == new_repo.repo_id).all()
276 assert len(inherited_perms) == 1
282 assert len(inherited_perms) == 1
277
283
278 assert repo_on_filesystem(repo_name_full)
284 assert repo_on_filesystem(repo_name_full)
279 finally:
285 finally:
280 RepoModel().delete(repo_name_full)
286 RepoModel().delete(repo_name_full)
281 RepoGroupModel().delete(group_name)
287 RepoGroupModel().delete(group_name)
282 RepoGroupModel().delete(group_name_allowed)
288 RepoGroupModel().delete(group_name_allowed)
283 Session().commit()
289 Session().commit()
284
290
285 def test_create_in_group_inherit_permissions(self, autologin_user, backend,
291 def test_create_in_group_inherit_permissions(self, autologin_user, backend,
286 csrf_token):
292 csrf_token):
287 # create GROUP
293 # create GROUP
288 group_name = 'sometest_%s' % backend.alias
294 group_name = 'sometest_%s' % backend.alias
289 gr = RepoGroupModel().create(group_name=group_name,
295 gr = RepoGroupModel().create(group_name=group_name,
290 group_description='test',
296 group_description='test',
291 owner=TEST_USER_ADMIN_LOGIN)
297 owner=TEST_USER_ADMIN_LOGIN)
292 perm = Permission.get_by_key('repository.write')
298 perm = Permission.get_by_key('repository.write')
293 RepoGroupModel().grant_user_permission(
299 RepoGroupModel().grant_user_permission(
294 gr, TEST_USER_REGULAR_LOGIN, perm)
300 gr, TEST_USER_REGULAR_LOGIN, perm)
295
301
296 # add repo permissions
302 # add repo permissions
297 Session().commit()
303 Session().commit()
298 repo_group_id = gr.group_id
304 repo_group_id = gr.group_id
299 repo_name = 'ingroup_inherited_%s' % backend.alias
305 repo_name = 'ingroup_inherited_%s' % backend.alias
300 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
306 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
301 description = 'description for newly created repo'
307 description = 'description for newly created repo'
302 self.app.post(
308 self.app.post(
303 route_path('repo_create'),
309 route_path('repo_create'),
304 fixture._get_repo_create_params(
310 fixture._get_repo_create_params(
305 repo_private=False,
311 repo_private=False,
306 repo_name=repo_name,
312 repo_name=repo_name,
307 repo_type=backend.alias,
313 repo_type=backend.alias,
308 repo_description=description,
314 repo_description=description,
309 repo_group=repo_group_id,
315 repo_group=repo_group_id,
310 repo_copy_permissions=True,
316 repo_copy_permissions=True,
311 csrf_token=csrf_token))
317 csrf_token=csrf_token))
312
318
313 # TODO: johbo: Cleanup to pytest fixture
319 # TODO: johbo: Cleanup to pytest fixture
314 try:
320 try:
315 self.assert_repository_is_created_correctly(
321 self.assert_repository_is_created_correctly(
316 repo_name_full, description, backend)
322 repo_name_full, description, backend)
317 except Exception:
323 except Exception:
318 RepoGroupModel().delete(group_name)
324 RepoGroupModel().delete(group_name)
319 Session().commit()
325 Session().commit()
320 raise
326 raise
321
327
322 # check if inherited permissions are applied
328 # check if inherited permissions are applied
323 new_repo = RepoModel().get_by_repo_name(repo_name_full)
329 new_repo = RepoModel().get_by_repo_name(repo_name_full)
324 inherited_perms = UserRepoToPerm.query().filter(
330 inherited_perms = UserRepoToPerm.query().filter(
325 UserRepoToPerm.repository_id == new_repo.repo_id).all()
331 UserRepoToPerm.repository_id == new_repo.repo_id).all()
326 assert len(inherited_perms) == 2
332 assert len(inherited_perms) == 2
327
333
328 assert TEST_USER_REGULAR_LOGIN in [
334 assert TEST_USER_REGULAR_LOGIN in [
329 x.user.username for x in inherited_perms]
335 x.user.username for x in inherited_perms]
330 assert 'repository.write' in [
336 assert 'repository.write' in [
331 x.permission.permission_name for x in inherited_perms]
337 x.permission.permission_name for x in inherited_perms]
332
338
333 RepoModel().delete(repo_name_full)
339 RepoModel().delete(repo_name_full)
334 RepoGroupModel().delete(group_name)
340 RepoGroupModel().delete(group_name)
335 Session().commit()
341 Session().commit()
336
342
337 @pytest.mark.xfail_backends(
343 @pytest.mark.xfail_backends(
338 "git", "hg", reason="Missing reposerver support")
344 "git", "hg", reason="Missing reposerver support")
339 def test_create_with_clone_uri(self, autologin_user, backend, reposerver,
345 def test_create_with_clone_uri(self, autologin_user, backend, reposerver,
340 csrf_token):
346 csrf_token):
341 source_repo = backend.create_repo(number_of_commits=2)
347 source_repo = backend.create_repo(number_of_commits=2)
342 source_repo_name = source_repo.repo_name
348 source_repo_name = source_repo.repo_name
343 reposerver.serve(source_repo.scm_instance())
349 reposerver.serve(source_repo.scm_instance())
344
350
345 repo_name = backend.new_repo_name()
351 repo_name = backend.new_repo_name()
346 response = self.app.post(
352 response = self.app.post(
347 route_path('repo_create'),
353 route_path('repo_create'),
348 fixture._get_repo_create_params(
354 fixture._get_repo_create_params(
349 repo_private=False,
355 repo_private=False,
350 repo_name=repo_name,
356 repo_name=repo_name,
351 repo_type=backend.alias,
357 repo_type=backend.alias,
352 repo_description='',
358 repo_description='',
353 clone_uri=reposerver.url,
359 clone_uri=reposerver.url,
354 csrf_token=csrf_token),
360 csrf_token=csrf_token),
355 status=302)
361 status=302)
356
362
357 # Should be redirected to the creating page
363 # Should be redirected to the creating page
358 response.mustcontain('repo_creating')
364 response.mustcontain('repo_creating')
359
365
360 # Expecting that both repositories have same history
366 # Expecting that both repositories have same history
361 source_repo = RepoModel().get_by_repo_name(source_repo_name)
367 source_repo = RepoModel().get_by_repo_name(source_repo_name)
362 source_vcs = source_repo.scm_instance()
368 source_vcs = source_repo.scm_instance()
363 repo = RepoModel().get_by_repo_name(repo_name)
369 repo = RepoModel().get_by_repo_name(repo_name)
364 repo_vcs = repo.scm_instance()
370 repo_vcs = repo.scm_instance()
365 assert source_vcs[0].message == repo_vcs[0].message
371 assert source_vcs[0].message == repo_vcs[0].message
366 assert source_vcs.count() == repo_vcs.count()
372 assert source_vcs.count() == repo_vcs.count()
367 assert source_vcs.commit_ids == repo_vcs.commit_ids
373 assert source_vcs.commit_ids == repo_vcs.commit_ids
368
374
369 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
375 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
370 def test_create_remote_repo_wrong_clone_uri(self, autologin_user, backend,
376 def test_create_remote_repo_wrong_clone_uri(self, autologin_user, backend,
371 csrf_token):
377 csrf_token):
372 repo_name = backend.new_repo_name()
378 repo_name = backend.new_repo_name()
373 description = 'description for newly created repo'
379 description = 'description for newly created repo'
374 response = self.app.post(
380 response = self.app.post(
375 route_path('repo_create'),
381 route_path('repo_create'),
376 fixture._get_repo_create_params(
382 fixture._get_repo_create_params(
377 repo_private=False,
383 repo_private=False,
378 repo_name=repo_name,
384 repo_name=repo_name,
379 repo_type=backend.alias,
385 repo_type=backend.alias,
380 repo_description=description,
386 repo_description=description,
381 clone_uri='http://repo.invalid/repo',
387 clone_uri='http://repo.invalid/repo',
382 csrf_token=csrf_token))
388 csrf_token=csrf_token))
383 response.mustcontain('invalid clone url')
389 response.mustcontain('invalid clone url')
384
390
385 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
391 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
386 def test_create_remote_repo_wrong_clone_uri_hg_svn(
392 def test_create_remote_repo_wrong_clone_uri_hg_svn(
387 self, autologin_user, backend, csrf_token):
393 self, autologin_user, backend, csrf_token):
388 repo_name = backend.new_repo_name()
394 repo_name = backend.new_repo_name()
389 description = 'description for newly created repo'
395 description = 'description for newly created repo'
390 response = self.app.post(
396 response = self.app.post(
391 route_path('repo_create'),
397 route_path('repo_create'),
392 fixture._get_repo_create_params(
398 fixture._get_repo_create_params(
393 repo_private=False,
399 repo_private=False,
394 repo_name=repo_name,
400 repo_name=repo_name,
395 repo_type=backend.alias,
401 repo_type=backend.alias,
396 repo_description=description,
402 repo_description=description,
397 clone_uri='svn+http://svn.invalid/repo',
403 clone_uri='svn+http://svn.invalid/repo',
398 csrf_token=csrf_token))
404 csrf_token=csrf_token))
399 response.mustcontain('invalid clone url')
405 response.mustcontain('invalid clone url')
400
406
401 def test_create_with_git_suffix(
407 def test_create_with_git_suffix(
402 self, autologin_user, backend, csrf_token):
408 self, autologin_user, backend, csrf_token):
403 repo_name = backend.new_repo_name() + ".git"
409 repo_name = backend.new_repo_name() + ".git"
404 description = 'description for newly created repo'
410 description = 'description for newly created repo'
405 response = self.app.post(
411 response = self.app.post(
406 route_path('repo_create'),
412 route_path('repo_create'),
407 fixture._get_repo_create_params(
413 fixture._get_repo_create_params(
408 repo_private=False,
414 repo_private=False,
409 repo_name=repo_name,
415 repo_name=repo_name,
410 repo_type=backend.alias,
416 repo_type=backend.alias,
411 repo_description=description,
417 repo_description=description,
412 csrf_token=csrf_token))
418 csrf_token=csrf_token))
413 response.mustcontain('Repository name cannot end with .git')
419 response.mustcontain('Repository name cannot end with .git')
414
420
415 def test_default_user_cannot_access_private_repo_in_a_group(
421 def test_default_user_cannot_access_private_repo_in_a_group(
416 self, autologin_user, user_util, backend):
422 self, autologin_user, user_util, backend):
417
423
418 group = user_util.create_repo_group()
424 group = user_util.create_repo_group()
419
425
420 repo = backend.create_repo(
426 repo = backend.create_repo(
421 repo_private=True, repo_group=group, repo_copy_permissions=True)
427 repo_private=True, repo_group=group, repo_copy_permissions=True)
422
428
423 permissions = _get_permission_for_user(
429 permissions = _get_permission_for_user(
424 user='default', repo=repo.repo_name)
430 user='default', repo=repo.repo_name)
425 assert len(permissions) == 1
431 assert len(permissions) == 1
426 assert permissions[0].permission.permission_name == 'repository.none'
432 assert permissions[0].permission.permission_name == 'repository.none'
427 assert permissions[0].repository.private is True
433 assert permissions[0].repository.private is True
428
434
429 def test_create_on_top_level_without_permissions(self, backend):
435 def test_create_on_top_level_without_permissions(self, backend):
430 session = login_user_session(
436 session = login_user_session(
431 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
437 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
432 csrf_token = auth.get_csrf_token(session)
438 csrf_token = auth.get_csrf_token(session)
433
439
434 # revoke
440 # revoke
435 user_model = UserModel()
441 user_model = UserModel()
436 # disable fork and create on default user
442 # disable fork and create on default user
437 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
443 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
438 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
444 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
439 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
445 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
440 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
446 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
441
447
442 # disable on regular user
448 # disable on regular user
443 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
449 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
444 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
450 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
445 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
451 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
446 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
452 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
447 Session().commit()
453 Session().commit()
448
454
449 repo_name = backend.new_repo_name()
455 repo_name = backend.new_repo_name()
450 description = 'description for newly created repo'
456 description = 'description for newly created repo'
451 response = self.app.post(
457 response = self.app.post(
452 route_path('repo_create'),
458 route_path('repo_create'),
453 fixture._get_repo_create_params(
459 fixture._get_repo_create_params(
454 repo_private=False,
460 repo_private=False,
455 repo_name=repo_name,
461 repo_name=repo_name,
456 repo_type=backend.alias,
462 repo_type=backend.alias,
457 repo_description=description,
463 repo_description=description,
458 csrf_token=csrf_token))
464 csrf_token=csrf_token))
459
465
460 response.mustcontain(
466 response.mustcontain(
461 u"You do not have the permission to store repositories in "
467 u"You do not have the permission to store repositories in "
462 u"the root location.")
468 u"the root location.")
463
469
464 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
470 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
465 def test_create_repo_when_filesystem_op_fails(
471 def test_create_repo_when_filesystem_op_fails(
466 self, autologin_user, backend, csrf_token):
472 self, autologin_user, backend, csrf_token):
467 repo_name = backend.new_repo_name()
473 repo_name = backend.new_repo_name()
468 description = 'description for newly created repo'
474 description = 'description for newly created repo'
469
475
470 response = self.app.post(
476 response = self.app.post(
471 route_path('repo_create'),
477 route_path('repo_create'),
472 fixture._get_repo_create_params(
478 fixture._get_repo_create_params(
473 repo_private=False,
479 repo_private=False,
474 repo_name=repo_name,
480 repo_name=repo_name,
475 repo_type=backend.alias,
481 repo_type=backend.alias,
476 repo_description=description,
482 repo_description=description,
477 csrf_token=csrf_token))
483 csrf_token=csrf_token))
478
484
479 assert_session_flash(
485 assert_session_flash(
480 response, 'Error creating repository %s' % repo_name)
486 response, 'Error creating repository %s' % repo_name)
481 # repo must not be in db
487 # repo must not be in db
482 assert backend.repo is None
488 assert backend.repo is None
483 # repo must not be in filesystem !
489 # repo must not be in filesystem !
484 assert not repo_on_filesystem(repo_name)
490 assert not repo_on_filesystem(repo_name)
485
491
486 def assert_repository_is_created_correctly(
492 def assert_repository_is_created_correctly(self, repo_name, description, backend):
487 self, repo_name, description, backend):
493 url_quoted_repo_name = urllib.parse.quote(repo_name)
488 repo_name_utf8 = safe_str(repo_name)
489
494
490 # run the check page that triggers the flash message
495 # run the check page that triggers the flash message
491 response = self.app.get(
496 response = self.app.get(
492 route_path('repo_creating_check', repo_name=safe_str(repo_name)))
497 route_path('repo_creating_check', repo_name=repo_name))
493 assert response.json == {u'result': True}
498 assert response.json == {'result': True}
494
499
495 flash_msg = u'Created repository <a href="/{}">{}</a>'.format(
500 flash_msg = 'Created repository <a href="/{}">{}</a>'.format(url_quoted_repo_name, repo_name)
496 urllib.parse.quote(repo_name_utf8), repo_name)
497 assert_session_flash(response, flash_msg)
501 assert_session_flash(response, flash_msg)
498
502
499 # test if the repo was created in the database
503 # test if the repo was created in the database
500 new_repo = RepoModel().get_by_repo_name(repo_name)
504 new_repo = RepoModel().get_by_repo_name(repo_name)
501
505
502 assert new_repo.repo_name == repo_name
506 assert new_repo.repo_name == repo_name
503 assert new_repo.description == description
507 assert new_repo.description == description
504
508
505 # test if the repository is visible in the list ?
509 # test if the repository is visible in the list ?
506 response = self.app.get(
510 response = self.app.get(
507 h.route_path('repo_summary', repo_name=safe_str(repo_name)))
511 h.route_path('repo_summary', repo_name=repo_name))
508 response.mustcontain(repo_name)
512 response.mustcontain(repo_name)
509 response.mustcontain(backend.alias)
513 response.mustcontain(backend.alias)
510
514
511 assert repo_on_filesystem(repo_name)
515 assert repo_on_filesystem(repo_name)
@@ -1,193 +1,195 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import os
20 import os
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.lib import helpers as h
24 from rhodecode.lib import helpers as h
25 from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
25 from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
26 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
27 from rhodecode.model.repo_group import RepoGroupModel
27 from rhodecode.model.repo_group import RepoGroupModel
28 from rhodecode.tests import (
28 from rhodecode.tests import (
29 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
29 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
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.request, urllib.parse, urllib.error
36 import urllib.request
37 import urllib.parse
38 import urllib.error
37
39
38 base_url = {
40 base_url = {
39 'repo_groups': ADMIN_PREFIX + '/repo_groups',
41 'repo_groups': ADMIN_PREFIX + '/repo_groups',
40 'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data',
42 'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data',
41 'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
43 'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
42 'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
44 'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
43
45
44 }[name].format(**kwargs)
46 }[name].format(**kwargs)
45
47
46 if params:
48 if params:
47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 return base_url
50 return base_url
49
51
50
52
51 def _get_permission_for_user(user, repo):
53 def _get_permission_for_user(user, repo):
52 perm = UserRepoToPerm.query()\
54 perm = UserRepoToPerm.query()\
53 .filter(UserRepoToPerm.repository ==
55 .filter(UserRepoToPerm.repository ==
54 Repository.get_by_repo_name(repo))\
56 Repository.get_by_repo_name(repo))\
55 .filter(UserRepoToPerm.user == User.get_by_username(user))\
57 .filter(UserRepoToPerm.user == User.get_by_username(user))\
56 .all()
58 .all()
57 return perm
59 return perm
58
60
59
61
60 @pytest.mark.usefixtures("app")
62 @pytest.mark.usefixtures("app")
61 class TestAdminRepositoryGroups(object):
63 class TestAdminRepositoryGroups(object):
62
64
63 def test_show_repo_groups(self, autologin_user):
65 def test_show_repo_groups(self, autologin_user):
64 self.app.get(route_path('repo_groups'))
66 self.app.get(route_path('repo_groups'))
65
67
66 def test_show_repo_groups_data(self, autologin_user, xhr_header):
68 def test_show_repo_groups_data(self, autologin_user, xhr_header):
67 response = self.app.get(route_path(
69 response = self.app.get(route_path(
68 'repo_groups_data'), extra_environ=xhr_header)
70 'repo_groups_data'), extra_environ=xhr_header)
69
71
70 all_repo_groups = RepoGroup.query().count()
72 all_repo_groups = RepoGroup.query().count()
71 assert response.json['recordsTotal'] == all_repo_groups
73 assert response.json['recordsTotal'] == all_repo_groups
72
74
73 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
75 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
74 response = self.app.get(route_path(
76 response = self.app.get(route_path(
75 'repo_groups_data', params={'search[value]': 'empty_search'}),
77 'repo_groups_data', params={'search[value]': 'empty_search'}),
76 extra_environ=xhr_header)
78 extra_environ=xhr_header)
77
79
78 all_repo_groups = RepoGroup.query().count()
80 all_repo_groups = RepoGroup.query().count()
79 assert response.json['recordsTotal'] == all_repo_groups
81 assert response.json['recordsTotal'] == all_repo_groups
80 assert response.json['recordsFiltered'] == 0
82 assert response.json['recordsFiltered'] == 0
81
83
82 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
84 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
83 fixture.create_repo_group('test_repo_group')
85 fixture.create_repo_group('test_repo_group')
84 response = self.app.get(route_path(
86 response = self.app.get(route_path(
85 'repo_groups_data'), extra_environ=xhr_header)
87 'repo_groups_data'), extra_environ=xhr_header)
86 response.mustcontain('<a href=\\"/{}/_edit\\" title=\\"Edit\\">Edit</a>'.format('test_repo_group'))
88 response.mustcontain('<a href=\\"/{}/_edit\\" title=\\"Edit\\">Edit</a>'.format('test_repo_group'))
87 fixture.destroy_repo_group('test_repo_group')
89 fixture.destroy_repo_group('test_repo_group')
88
90
89 def test_new(self, autologin_user):
91 def test_new(self, autologin_user):
90 self.app.get(route_path('repo_group_new'))
92 self.app.get(route_path('repo_group_new'))
91
93
92 def test_new_with_parent_group(self, autologin_user, user_util):
94 def test_new_with_parent_group(self, autologin_user, user_util):
93 gr = user_util.create_repo_group()
95 gr = user_util.create_repo_group()
94
96
95 self.app.get(route_path('repo_group_new'),
97 self.app.get(route_path('repo_group_new'),
96 params=dict(parent_group=gr.group_name))
98 params=dict(parent_group=gr.group_name))
97
99
98 def test_new_by_regular_user_no_permission(self, autologin_regular_user):
100 def test_new_by_regular_user_no_permission(self, autologin_regular_user):
99 self.app.get(route_path('repo_group_new'), status=403)
101 self.app.get(route_path('repo_group_new'), status=403)
100
102
101 @pytest.mark.parametrize('repo_group_name', [
103 @pytest.mark.parametrize('repo_group_name', [
102 'git_repo',
104 'git_repo',
103 'git_repo_ąć',
105 'git_repo_ąć',
104 'hg_repo',
106 'hg_repo',
105 '12345',
107 '12345',
106 'hg_repo_ąć',
108 'hg_repo_ąć',
107 ])
109 ])
108 def test_create(self, autologin_user, repo_group_name, csrf_token):
110 def test_create(self, autologin_user, repo_group_name, csrf_token):
109 repo_group_name_unicode = repo_group_name.decode('utf8')
111 repo_group_name_non_ascii = repo_group_name
110 description = 'description for newly created repo group'
112 description = 'description for newly created repo group'
111
113
112 response = self.app.post(
114 response = self.app.post(
113 route_path('repo_group_create'),
115 route_path('repo_group_create'),
114 fixture._get_group_create_params(
116 fixture._get_group_create_params(
115 group_name=repo_group_name,
117 group_name=repo_group_name,
116 group_description=description,
118 group_description=description,
117 csrf_token=csrf_token))
119 csrf_token=csrf_token))
118
120
119 # run the check page that triggers the flash message
121 # run the check page that triggers the flash message
120 repo_gr_url = h.route_path(
122 repo_gr_url = h.route_path(
121 'repo_group_home', repo_group_name=repo_group_name)
123 'repo_group_home', repo_group_name=repo_group_name)
122
124
123 assert_session_flash(
125 assert_session_flash(
124 response,
126 response,
125 'Created repository group <a href="%s">%s</a>' % (
127 'Created repository group <a href="%s">%s</a>' % (
126 repo_gr_url, repo_group_name_unicode))
128 repo_gr_url, repo_group_name_non_ascii))
127
129
128 # # test if the repo group was created in the database
130 # # test if the repo group was created in the database
129 new_repo_group = RepoGroupModel()._get_repo_group(
131 new_repo_group = RepoGroupModel()._get_repo_group(
130 repo_group_name_unicode)
132 repo_group_name_non_ascii)
131 assert new_repo_group is not None
133 assert new_repo_group is not None
132
134
133 assert new_repo_group.group_name == repo_group_name_unicode
135 assert new_repo_group.group_name == repo_group_name_non_ascii
134 assert new_repo_group.group_description == description
136 assert new_repo_group.group_description == description
135
137
136 # test if the repository is visible in the list ?
138 # test if the repository is visible in the list ?
137 response = self.app.get(repo_gr_url)
139 response = self.app.get(repo_gr_url)
138 response.mustcontain(repo_group_name)
140 response.mustcontain(repo_group_name)
139
141
140 # test if the repository group was created on filesystem
142 # test if the repository group was created on filesystem
141 is_on_filesystem = os.path.isdir(
143 is_on_filesystem = os.path.isdir(
142 os.path.join(TESTS_TMP_PATH, repo_group_name))
144 os.path.join(TESTS_TMP_PATH, repo_group_name))
143 if not is_on_filesystem:
145 if not is_on_filesystem:
144 self.fail('no repo group %s in filesystem' % repo_group_name)
146 self.fail('no repo group %s in filesystem' % repo_group_name)
145
147
146 RepoGroupModel().delete(repo_group_name_unicode)
148 RepoGroupModel().delete(repo_group_name_non_ascii)
147 Session().commit()
149 Session().commit()
148
150
149 @pytest.mark.parametrize('repo_group_name', [
151 @pytest.mark.parametrize('repo_group_name', [
150 'git_repo',
152 'git_repo',
151 'git_repo_ąć',
153 'git_repo_ąć',
152 'hg_repo',
154 'hg_repo',
153 '12345',
155 '12345',
154 'hg_repo_ąć',
156 'hg_repo_ąć',
155 ])
157 ])
156 def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token):
158 def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token):
157 parent_group = user_util.create_repo_group()
159 parent_group = user_util.create_repo_group()
158 parent_group_name = parent_group.group_name
160 parent_group_name = parent_group.group_name
159
161
160 expected_group_name = '{}/{}'.format(
162 expected_group_name = '{}/{}'.format(
161 parent_group_name, repo_group_name)
163 parent_group_name, repo_group_name)
162 expected_group_name_unicode = expected_group_name.decode('utf8')
164 expected_group_name_non_ascii = expected_group_name
163
165
164 try:
166 try:
165 response = self.app.post(
167 response = self.app.post(
166 route_path('repo_group_create'),
168 route_path('repo_group_create'),
167 fixture._get_group_create_params(
169 fixture._get_group_create_params(
168 group_name=repo_group_name,
170 group_name=repo_group_name,
169 group_parent_id=parent_group.group_id,
171 group_parent_id=parent_group.group_id,
170 group_description='Test desciption',
172 group_description='Test desciption',
171 csrf_token=csrf_token))
173 csrf_token=csrf_token))
172
174
173 assert_session_flash(
175 assert_session_flash(
174 response,
176 response,
175 u'Created repository group <a href="%s">%s</a>' % (
177 u'Created repository group <a href="%s">%s</a>' % (
176 h.route_path('repo_group_home',
178 h.route_path('repo_group_home',
177 repo_group_name=expected_group_name),
179 repo_group_name=expected_group_name),
178 expected_group_name_unicode))
180 expected_group_name_non_ascii))
179 finally:
181 finally:
180 RepoGroupModel().delete(expected_group_name_unicode)
182 RepoGroupModel().delete(expected_group_name_non_ascii)
181 Session().commit()
183 Session().commit()
182
184
183 def test_user_with_creation_permissions_cannot_create_subgroups(
185 def test_user_with_creation_permissions_cannot_create_subgroups(
184 self, autologin_regular_user, user_util):
186 self, autologin_regular_user, user_util):
185
187
186 user_util.grant_user_permission(
188 user_util.grant_user_permission(
187 TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true')
189 TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true')
188 parent_group = user_util.create_repo_group()
190 parent_group = user_util.create_repo_group()
189 parent_group_id = parent_group.group_id
191 parent_group_id = parent_group.group_id
190 self.app.get(
192 self.app.get(
191 route_path('repo_group_new',
193 route_path('repo_group_new',
192 params=dict(parent_group=parent_group_id), ),
194 params=dict(parent_group=parent_group_id), ),
193 status=403)
195 status=403)
@@ -1,766 +1,768 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.lib.utils2 import md5
25 from rhodecode.lib.hash_utils import md5_safe
26 from rhodecode.model.db import RhodeCodeUi
26 from rhodecode.model.db import RhodeCodeUi
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
28 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
29 from rhodecode.tests import assert_session_flash
29 from rhodecode.tests import assert_session_flash
30 from rhodecode.tests.utils import AssertResponse
31
30
32
31
33 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
32 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
34
33
35
34
36 def route_path(name, params=None, **kwargs):
35 def route_path(name, params=None, **kwargs):
37 import urllib.request, urllib.parse, urllib.error
36 import urllib.request
37 import urllib.parse
38 import urllib.error
38 from rhodecode.apps._base import ADMIN_PREFIX
39 from rhodecode.apps._base import ADMIN_PREFIX
39
40
40 base_url = {
41 base_url = {
41
42
42 'admin_settings':
43 'admin_settings':
43 ADMIN_PREFIX +'/settings',
44 ADMIN_PREFIX +'/settings',
44 'admin_settings_update':
45 'admin_settings_update':
45 ADMIN_PREFIX + '/settings/update',
46 ADMIN_PREFIX + '/settings/update',
46 'admin_settings_global':
47 'admin_settings_global':
47 ADMIN_PREFIX + '/settings/global',
48 ADMIN_PREFIX + '/settings/global',
48 'admin_settings_global_update':
49 'admin_settings_global_update':
49 ADMIN_PREFIX + '/settings/global/update',
50 ADMIN_PREFIX + '/settings/global/update',
50 'admin_settings_vcs':
51 'admin_settings_vcs':
51 ADMIN_PREFIX + '/settings/vcs',
52 ADMIN_PREFIX + '/settings/vcs',
52 'admin_settings_vcs_update':
53 'admin_settings_vcs_update':
53 ADMIN_PREFIX + '/settings/vcs/update',
54 ADMIN_PREFIX + '/settings/vcs/update',
54 'admin_settings_vcs_svn_pattern_delete':
55 'admin_settings_vcs_svn_pattern_delete':
55 ADMIN_PREFIX + '/settings/vcs/svn_pattern_delete',
56 ADMIN_PREFIX + '/settings/vcs/svn_pattern_delete',
56 'admin_settings_mapping':
57 'admin_settings_mapping':
57 ADMIN_PREFIX + '/settings/mapping',
58 ADMIN_PREFIX + '/settings/mapping',
58 'admin_settings_mapping_update':
59 'admin_settings_mapping_update':
59 ADMIN_PREFIX + '/settings/mapping/update',
60 ADMIN_PREFIX + '/settings/mapping/update',
60 'admin_settings_visual':
61 'admin_settings_visual':
61 ADMIN_PREFIX + '/settings/visual',
62 ADMIN_PREFIX + '/settings/visual',
62 'admin_settings_visual_update':
63 'admin_settings_visual_update':
63 ADMIN_PREFIX + '/settings/visual/update',
64 ADMIN_PREFIX + '/settings/visual/update',
64 'admin_settings_issuetracker':
65 'admin_settings_issuetracker':
65 ADMIN_PREFIX + '/settings/issue-tracker',
66 ADMIN_PREFIX + '/settings/issue-tracker',
66 'admin_settings_issuetracker_update':
67 'admin_settings_issuetracker_update':
67 ADMIN_PREFIX + '/settings/issue-tracker/update',
68 ADMIN_PREFIX + '/settings/issue-tracker/update',
68 'admin_settings_issuetracker_test':
69 'admin_settings_issuetracker_test':
69 ADMIN_PREFIX + '/settings/issue-tracker/test',
70 ADMIN_PREFIX + '/settings/issue-tracker/test',
70 'admin_settings_issuetracker_delete':
71 'admin_settings_issuetracker_delete':
71 ADMIN_PREFIX + '/settings/issue-tracker/delete',
72 ADMIN_PREFIX + '/settings/issue-tracker/delete',
72 'admin_settings_email':
73 'admin_settings_email':
73 ADMIN_PREFIX + '/settings/email',
74 ADMIN_PREFIX + '/settings/email',
74 'admin_settings_email_update':
75 'admin_settings_email_update':
75 ADMIN_PREFIX + '/settings/email/update',
76 ADMIN_PREFIX + '/settings/email/update',
76 'admin_settings_hooks':
77 'admin_settings_hooks':
77 ADMIN_PREFIX + '/settings/hooks',
78 ADMIN_PREFIX + '/settings/hooks',
78 'admin_settings_hooks_update':
79 'admin_settings_hooks_update':
79 ADMIN_PREFIX + '/settings/hooks/update',
80 ADMIN_PREFIX + '/settings/hooks/update',
80 'admin_settings_hooks_delete':
81 'admin_settings_hooks_delete':
81 ADMIN_PREFIX + '/settings/hooks/delete',
82 ADMIN_PREFIX + '/settings/hooks/delete',
82 'admin_settings_search':
83 'admin_settings_search':
83 ADMIN_PREFIX + '/settings/search',
84 ADMIN_PREFIX + '/settings/search',
84 'admin_settings_labs':
85 'admin_settings_labs':
85 ADMIN_PREFIX + '/settings/labs',
86 ADMIN_PREFIX + '/settings/labs',
86 'admin_settings_labs_update':
87 'admin_settings_labs_update':
87 ADMIN_PREFIX + '/settings/labs/update',
88 ADMIN_PREFIX + '/settings/labs/update',
88
89
89 'admin_settings_sessions':
90 'admin_settings_sessions':
90 ADMIN_PREFIX + '/settings/sessions',
91 ADMIN_PREFIX + '/settings/sessions',
91 'admin_settings_sessions_cleanup':
92 'admin_settings_sessions_cleanup':
92 ADMIN_PREFIX + '/settings/sessions/cleanup',
93 ADMIN_PREFIX + '/settings/sessions/cleanup',
93 'admin_settings_system':
94 'admin_settings_system':
94 ADMIN_PREFIX + '/settings/system',
95 ADMIN_PREFIX + '/settings/system',
95 'admin_settings_system_update':
96 'admin_settings_system_update':
96 ADMIN_PREFIX + '/settings/system/updates',
97 ADMIN_PREFIX + '/settings/system/updates',
97 'admin_settings_open_source':
98 'admin_settings_open_source':
98 ADMIN_PREFIX + '/settings/open_source',
99 ADMIN_PREFIX + '/settings/open_source',
99
100
100
101
101 }[name].format(**kwargs)
102 }[name].format(**kwargs)
102
103
103 if params:
104 if params:
104 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
105 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
105 return base_url
106 return base_url
106
107
107
108
108 @pytest.mark.usefixtures('autologin_user', 'app')
109 @pytest.mark.usefixtures('autologin_user', 'app')
109 class TestAdminSettingsController(object):
110 class TestAdminSettingsController(object):
110
111
111 @pytest.mark.parametrize('urlname', [
112 @pytest.mark.parametrize('urlname', [
112 'admin_settings_vcs',
113 'admin_settings_vcs',
113 'admin_settings_mapping',
114 'admin_settings_mapping',
114 'admin_settings_global',
115 'admin_settings_global',
115 'admin_settings_visual',
116 'admin_settings_visual',
116 'admin_settings_email',
117 'admin_settings_email',
117 'admin_settings_hooks',
118 'admin_settings_hooks',
118 'admin_settings_search',
119 'admin_settings_search',
119 ])
120 ])
120 def test_simple_get(self, urlname):
121 def test_simple_get(self, urlname):
121 self.app.get(route_path(urlname))
122 self.app.get(route_path(urlname))
122
123
123 def test_create_custom_hook(self, csrf_token):
124 def test_create_custom_hook(self, csrf_token):
124 response = self.app.post(
125 response = self.app.post(
125 route_path('admin_settings_hooks_update'),
126 route_path('admin_settings_hooks_update'),
126 params={
127 params={
127 'new_hook_ui_key': 'test_hooks_1',
128 'new_hook_ui_key': 'test_hooks_1',
128 'new_hook_ui_value': 'cd /tmp',
129 'new_hook_ui_value': 'cd /tmp',
129 'csrf_token': csrf_token})
130 'csrf_token': csrf_token})
130
131
131 response = response.follow()
132 response = response.follow()
132 response.mustcontain('test_hooks_1')
133 response.mustcontain('test_hooks_1')
133 response.mustcontain('cd /tmp')
134 response.mustcontain('cd /tmp')
134
135
135 def test_create_custom_hook_delete(self, csrf_token):
136 def test_create_custom_hook_delete(self, csrf_token):
136 response = self.app.post(
137 response = self.app.post(
137 route_path('admin_settings_hooks_update'),
138 route_path('admin_settings_hooks_update'),
138 params={
139 params={
139 'new_hook_ui_key': 'test_hooks_2',
140 'new_hook_ui_key': 'test_hooks_2',
140 'new_hook_ui_value': 'cd /tmp2',
141 'new_hook_ui_value': 'cd /tmp2',
141 'csrf_token': csrf_token})
142 'csrf_token': csrf_token})
142
143
143 response = response.follow()
144 response = response.follow()
144 response.mustcontain('test_hooks_2')
145 response.mustcontain('test_hooks_2')
145 response.mustcontain('cd /tmp2')
146 response.mustcontain('cd /tmp2')
146
147
147 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
148
149
149 # delete
150 # delete
150 self.app.post(
151 self.app.post(
151 route_path('admin_settings_hooks_delete'),
152 route_path('admin_settings_hooks_delete'),
152 params={'hook_id': hook_id, 'csrf_token': csrf_token})
153 params={'hook_id': hook_id, 'csrf_token': csrf_token})
153 response = self.app.get(route_path('admin_settings_hooks'))
154 response = self.app.get(route_path('admin_settings_hooks'))
154 response.mustcontain(no=['test_hooks_2'])
155 response.mustcontain(no=['test_hooks_2'])
155 response.mustcontain(no=['cd /tmp2'])
156 response.mustcontain(no=['cd /tmp2'])
156
157
157
158
158 @pytest.mark.usefixtures('autologin_user', 'app')
159 @pytest.mark.usefixtures('autologin_user', 'app')
159 class TestAdminSettingsGlobal(object):
160 class TestAdminSettingsGlobal(object):
160
161
161 def test_pre_post_code_code_active(self, csrf_token):
162 def test_pre_post_code_code_active(self, csrf_token):
162 pre_code = 'rc-pre-code-187652122'
163 pre_code = 'rc-pre-code-187652122'
163 post_code = 'rc-postcode-98165231'
164 post_code = 'rc-postcode-98165231'
164
165
165 response = self.post_and_verify_settings({
166 response = self.post_and_verify_settings({
166 'rhodecode_pre_code': pre_code,
167 'rhodecode_pre_code': pre_code,
167 'rhodecode_post_code': post_code,
168 'rhodecode_post_code': post_code,
168 'csrf_token': csrf_token,
169 'csrf_token': csrf_token,
169 })
170 })
170
171
171 response = response.follow()
172 response = response.follow()
172 response.mustcontain(pre_code, post_code)
173 response.mustcontain(pre_code, post_code)
173
174
174 def test_pre_post_code_code_inactive(self, csrf_token):
175 def test_pre_post_code_code_inactive(self, csrf_token):
175 pre_code = 'rc-pre-code-187652122'
176 pre_code = 'rc-pre-code-187652122'
176 post_code = 'rc-postcode-98165231'
177 post_code = 'rc-postcode-98165231'
177 response = self.post_and_verify_settings({
178 response = self.post_and_verify_settings({
178 'rhodecode_pre_code': '',
179 'rhodecode_pre_code': '',
179 'rhodecode_post_code': '',
180 'rhodecode_post_code': '',
180 'csrf_token': csrf_token,
181 'csrf_token': csrf_token,
181 })
182 })
182
183
183 response = response.follow()
184 response = response.follow()
184 response.mustcontain(no=[pre_code, post_code])
185 response.mustcontain(no=[pre_code, post_code])
185
186
186 def test_captcha_activate(self, csrf_token):
187 def test_captcha_activate(self, csrf_token):
187 self.post_and_verify_settings({
188 self.post_and_verify_settings({
188 'rhodecode_captcha_private_key': '1234567890',
189 'rhodecode_captcha_private_key': '1234567890',
189 'rhodecode_captcha_public_key': '1234567890',
190 'rhodecode_captcha_public_key': '1234567890',
190 'csrf_token': csrf_token,
191 'csrf_token': csrf_token,
191 })
192 })
192
193
193 response = self.app.get(ADMIN_PREFIX + '/register')
194 response = self.app.get(ADMIN_PREFIX + '/register')
194 response.mustcontain('captcha')
195 response.mustcontain('captcha')
195
196
196 def test_captcha_deactivate(self, csrf_token):
197 def test_captcha_deactivate(self, csrf_token):
197 self.post_and_verify_settings({
198 self.post_and_verify_settings({
198 'rhodecode_captcha_private_key': '',
199 'rhodecode_captcha_private_key': '',
199 'rhodecode_captcha_public_key': '1234567890',
200 'rhodecode_captcha_public_key': '1234567890',
200 'csrf_token': csrf_token,
201 'csrf_token': csrf_token,
201 })
202 })
202
203
203 response = self.app.get(ADMIN_PREFIX + '/register')
204 response = self.app.get(ADMIN_PREFIX + '/register')
204 response.mustcontain(no=['captcha'])
205 response.mustcontain(no=['captcha'])
205
206
206 def test_title_change(self, csrf_token):
207 def test_title_change(self, csrf_token):
207 old_title = 'RhodeCode'
208 old_title = 'RhodeCode'
208
209
209 for new_title in ['Changed', 'Żółwik', old_title]:
210 for new_title in ['Changed', 'Żółwik', old_title]:
210 response = self.post_and_verify_settings({
211 response = self.post_and_verify_settings({
211 'rhodecode_title': new_title,
212 'rhodecode_title': new_title,
212 'csrf_token': csrf_token,
213 'csrf_token': csrf_token,
213 })
214 })
214
215
215 response = response.follow()
216 response = response.follow()
216 response.mustcontain(new_title)
217 response.mustcontain(new_title)
217
218
218 def post_and_verify_settings(self, settings):
219 def post_and_verify_settings(self, settings):
219 old_title = 'RhodeCode'
220 old_title = 'RhodeCode'
220 old_realm = 'RhodeCode authentication'
221 old_realm = 'RhodeCode authentication'
221 params = {
222 params = {
222 'rhodecode_title': old_title,
223 'rhodecode_title': old_title,
223 'rhodecode_realm': old_realm,
224 'rhodecode_realm': old_realm,
224 'rhodecode_pre_code': '',
225 'rhodecode_pre_code': '',
225 'rhodecode_post_code': '',
226 'rhodecode_post_code': '',
226 'rhodecode_captcha_private_key': '',
227 'rhodecode_captcha_private_key': '',
227 'rhodecode_captcha_public_key': '',
228 'rhodecode_captcha_public_key': '',
228 'rhodecode_create_personal_repo_group': False,
229 'rhodecode_create_personal_repo_group': False,
229 'rhodecode_personal_repo_group_pattern': '${username}',
230 'rhodecode_personal_repo_group_pattern': '${username}',
230 }
231 }
231 params.update(settings)
232 params.update(settings)
232 response = self.app.post(
233 response = self.app.post(
233 route_path('admin_settings_global_update'), params=params)
234 route_path('admin_settings_global_update'), params=params)
234
235
235 assert_session_flash(response, 'Updated application settings')
236 assert_session_flash(response, 'Updated application settings')
237
236 app_settings = SettingsModel().get_all_settings()
238 app_settings = SettingsModel().get_all_settings()
237 del settings['csrf_token']
239 del settings['csrf_token']
238 for key, value in settings.items():
240 for key, value in settings.items():
239 assert app_settings[key] == value
241 assert app_settings[key] == value
240
242
241 return response
243 return response
242
244
243
245
244 @pytest.mark.usefixtures('autologin_user', 'app')
246 @pytest.mark.usefixtures('autologin_user', 'app')
245 class TestAdminSettingsVcs(object):
247 class TestAdminSettingsVcs(object):
246
248
247 def test_contains_svn_default_patterns(self):
249 def test_contains_svn_default_patterns(self):
248 response = self.app.get(route_path('admin_settings_vcs'))
250 response = self.app.get(route_path('admin_settings_vcs'))
249 expected_patterns = [
251 expected_patterns = [
250 '/trunk',
252 '/trunk',
251 '/branches/*',
253 '/branches/*',
252 '/tags/*',
254 '/tags/*',
253 ]
255 ]
254 for pattern in expected_patterns:
256 for pattern in expected_patterns:
255 response.mustcontain(pattern)
257 response.mustcontain(pattern)
256
258
257 def test_add_new_svn_branch_and_tag_pattern(
259 def test_add_new_svn_branch_and_tag_pattern(
258 self, backend_svn, form_defaults, disable_sql_cache,
260 self, backend_svn, form_defaults, disable_sql_cache,
259 csrf_token):
261 csrf_token):
260 form_defaults.update({
262 form_defaults.update({
261 'new_svn_branch': '/exp/branches/*',
263 'new_svn_branch': '/exp/branches/*',
262 'new_svn_tag': '/important_tags/*',
264 'new_svn_tag': '/important_tags/*',
263 'csrf_token': csrf_token,
265 'csrf_token': csrf_token,
264 })
266 })
265
267
266 response = self.app.post(
268 response = self.app.post(
267 route_path('admin_settings_vcs_update'),
269 route_path('admin_settings_vcs_update'),
268 params=form_defaults, status=302)
270 params=form_defaults, status=302)
269 response = response.follow()
271 response = response.follow()
270
272
271 # Expect to find the new values on the page
273 # Expect to find the new values on the page
272 response.mustcontain('/exp/branches/*')
274 response.mustcontain('/exp/branches/*')
273 response.mustcontain('/important_tags/*')
275 response.mustcontain('/important_tags/*')
274
276
275 # Expect that those patterns are used to match branches and tags now
277 # Expect that those patterns are used to match branches and tags now
276 repo = backend_svn['svn-simple-layout'].scm_instance()
278 repo = backend_svn['svn-simple-layout'].scm_instance()
277 assert 'exp/branches/exp-sphinx-docs' in repo.branches
279 assert 'exp/branches/exp-sphinx-docs' in repo.branches
278 assert 'important_tags/v0.5' in repo.tags
280 assert 'important_tags/v0.5' in repo.tags
279
281
280 def test_add_same_svn_value_twice_shows_an_error_message(
282 def test_add_same_svn_value_twice_shows_an_error_message(
281 self, form_defaults, csrf_token, settings_util):
283 self, form_defaults, csrf_token, settings_util):
282 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
284 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
283 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
285 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
284
286
285 response = self.app.post(
287 response = self.app.post(
286 route_path('admin_settings_vcs_update'),
288 route_path('admin_settings_vcs_update'),
287 params={
289 params={
288 'paths_root_path': form_defaults['paths_root_path'],
290 'paths_root_path': form_defaults['paths_root_path'],
289 'new_svn_branch': '/test',
291 'new_svn_branch': '/test',
290 'new_svn_tag': '/test',
292 'new_svn_tag': '/test',
291 'csrf_token': csrf_token,
293 'csrf_token': csrf_token,
292 },
294 },
293 status=200)
295 status=200)
294
296
295 response.mustcontain("Pattern already exists")
297 response.mustcontain("Pattern already exists")
296 response.mustcontain("Some form inputs contain invalid data.")
298 response.mustcontain("Some form inputs contain invalid data.")
297
299
298 @pytest.mark.parametrize('section', [
300 @pytest.mark.parametrize('section', [
299 'vcs_svn_branch',
301 'vcs_svn_branch',
300 'vcs_svn_tag',
302 'vcs_svn_tag',
301 ])
303 ])
302 def test_delete_svn_patterns(
304 def test_delete_svn_patterns(
303 self, section, csrf_token, settings_util):
305 self, section, csrf_token, settings_util):
304 setting = settings_util.create_rhodecode_ui(
306 setting = settings_util.create_rhodecode_ui(
305 section, '/test_delete', cleanup=False)
307 section, '/test_delete', cleanup=False)
306
308
307 self.app.post(
309 self.app.post(
308 route_path('admin_settings_vcs_svn_pattern_delete'),
310 route_path('admin_settings_vcs_svn_pattern_delete'),
309 params={
311 params={
310 'delete_svn_pattern': setting.ui_id,
312 'delete_svn_pattern': setting.ui_id,
311 'csrf_token': csrf_token},
313 'csrf_token': csrf_token},
312 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
314 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
313
315
314 @pytest.mark.parametrize('section', [
316 @pytest.mark.parametrize('section', [
315 'vcs_svn_branch',
317 'vcs_svn_branch',
316 'vcs_svn_tag',
318 'vcs_svn_tag',
317 ])
319 ])
318 def test_delete_svn_patterns_raises_404_when_no_xhr(
320 def test_delete_svn_patterns_raises_404_when_no_xhr(
319 self, section, csrf_token, settings_util):
321 self, section, csrf_token, settings_util):
320 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
322 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
321
323
322 self.app.post(
324 self.app.post(
323 route_path('admin_settings_vcs_svn_pattern_delete'),
325 route_path('admin_settings_vcs_svn_pattern_delete'),
324 params={
326 params={
325 'delete_svn_pattern': setting.ui_id,
327 'delete_svn_pattern': setting.ui_id,
326 'csrf_token': csrf_token},
328 'csrf_token': csrf_token},
327 status=404)
329 status=404)
328
330
329 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
331 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
330 form_defaults.update({
332 form_defaults.update({
331 'csrf_token': csrf_token,
333 'csrf_token': csrf_token,
332 'extensions_hgsubversion': 'True',
334 'extensions_hgsubversion': 'True',
333 })
335 })
334 response = self.app.post(
336 response = self.app.post(
335 route_path('admin_settings_vcs_update'),
337 route_path('admin_settings_vcs_update'),
336 params=form_defaults,
338 params=form_defaults,
337 status=302)
339 status=302)
338
340
339 response = response.follow()
341 response = response.follow()
340 extensions_input = (
342 extensions_input = (
341 '<input id="extensions_hgsubversion" '
343 '<input id="extensions_hgsubversion" '
342 'name="extensions_hgsubversion" type="checkbox" '
344 'name="extensions_hgsubversion" type="checkbox" '
343 'value="True" checked="checked" />')
345 'value="True" checked="checked" />')
344 response.mustcontain(extensions_input)
346 response.mustcontain(extensions_input)
345
347
346 def test_extensions_hgevolve(self, form_defaults, csrf_token):
348 def test_extensions_hgevolve(self, form_defaults, csrf_token):
347 form_defaults.update({
349 form_defaults.update({
348 'csrf_token': csrf_token,
350 'csrf_token': csrf_token,
349 'extensions_evolve': 'True',
351 'extensions_evolve': 'True',
350 })
352 })
351 response = self.app.post(
353 response = self.app.post(
352 route_path('admin_settings_vcs_update'),
354 route_path('admin_settings_vcs_update'),
353 params=form_defaults,
355 params=form_defaults,
354 status=302)
356 status=302)
355
357
356 response = response.follow()
358 response = response.follow()
357 extensions_input = (
359 extensions_input = (
358 '<input id="extensions_evolve" '
360 '<input id="extensions_evolve" '
359 'name="extensions_evolve" type="checkbox" '
361 'name="extensions_evolve" type="checkbox" '
360 'value="True" checked="checked" />')
362 'value="True" checked="checked" />')
361 response.mustcontain(extensions_input)
363 response.mustcontain(extensions_input)
362
364
363 def test_has_a_section_for_pull_request_settings(self):
365 def test_has_a_section_for_pull_request_settings(self):
364 response = self.app.get(route_path('admin_settings_vcs'))
366 response = self.app.get(route_path('admin_settings_vcs'))
365 response.mustcontain('Pull Request Settings')
367 response.mustcontain('Pull Request Settings')
366
368
367 def test_has_an_input_for_invalidation_of_inline_comments(self):
369 def test_has_an_input_for_invalidation_of_inline_comments(self):
368 response = self.app.get(route_path('admin_settings_vcs'))
370 response = self.app.get(route_path('admin_settings_vcs'))
369 assert_response = response.assert_response()
371 assert_response = response.assert_response()
370 assert_response.one_element_exists(
372 assert_response.one_element_exists(
371 '[name=rhodecode_use_outdated_comments]')
373 '[name=rhodecode_use_outdated_comments]')
372
374
373 @pytest.mark.parametrize('new_value', [True, False])
375 @pytest.mark.parametrize('new_value', [True, False])
374 def test_allows_to_change_invalidation_of_inline_comments(
376 def test_allows_to_change_invalidation_of_inline_comments(
375 self, form_defaults, csrf_token, new_value):
377 self, form_defaults, csrf_token, new_value):
376 setting_key = 'use_outdated_comments'
378 setting_key = 'use_outdated_comments'
377 setting = SettingsModel().create_or_update_setting(
379 setting = SettingsModel().create_or_update_setting(
378 setting_key, not new_value, 'bool')
380 setting_key, not new_value, 'bool')
379 Session().add(setting)
381 Session().add(setting)
380 Session().commit()
382 Session().commit()
381
383
382 form_defaults.update({
384 form_defaults.update({
383 'csrf_token': csrf_token,
385 'csrf_token': csrf_token,
384 'rhodecode_use_outdated_comments': str(new_value),
386 'rhodecode_use_outdated_comments': str(new_value),
385 })
387 })
386 response = self.app.post(
388 response = self.app.post(
387 route_path('admin_settings_vcs_update'),
389 route_path('admin_settings_vcs_update'),
388 params=form_defaults,
390 params=form_defaults,
389 status=302)
391 status=302)
390 response = response.follow()
392 response = response.follow()
391 setting = SettingsModel().get_setting_by_name(setting_key)
393 setting = SettingsModel().get_setting_by_name(setting_key)
392 assert setting.app_settings_value is new_value
394 assert setting.app_settings_value is new_value
393
395
394 @pytest.mark.parametrize('new_value', [True, False])
396 @pytest.mark.parametrize('new_value', [True, False])
395 def test_allows_to_change_hg_rebase_merge_strategy(
397 def test_allows_to_change_hg_rebase_merge_strategy(
396 self, form_defaults, csrf_token, new_value):
398 self, form_defaults, csrf_token, new_value):
397 setting_key = 'hg_use_rebase_for_merging'
399 setting_key = 'hg_use_rebase_for_merging'
398
400
399 form_defaults.update({
401 form_defaults.update({
400 'csrf_token': csrf_token,
402 'csrf_token': csrf_token,
401 'rhodecode_' + setting_key: str(new_value),
403 'rhodecode_' + setting_key: str(new_value),
402 })
404 })
403
405
404 with mock.patch.dict(
406 with mock.patch.dict(
405 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
407 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
406 self.app.post(
408 self.app.post(
407 route_path('admin_settings_vcs_update'),
409 route_path('admin_settings_vcs_update'),
408 params=form_defaults,
410 params=form_defaults,
409 status=302)
411 status=302)
410
412
411 setting = SettingsModel().get_setting_by_name(setting_key)
413 setting = SettingsModel().get_setting_by_name(setting_key)
412 assert setting.app_settings_value is new_value
414 assert setting.app_settings_value is new_value
413
415
414 @pytest.fixture()
416 @pytest.fixture()
415 def disable_sql_cache(self, request):
417 def disable_sql_cache(self, request):
418 # patch _do_orm_execute so it returns None similar like if we don't use a cached query
416 patcher = mock.patch(
419 patcher = mock.patch(
417 'rhodecode.lib.caching_query.FromCache.process_query')
420 'rhodecode.lib.caching_query.ORMCache._do_orm_execute', return_value=None)
418 request.addfinalizer(patcher.stop)
421 request.addfinalizer(patcher.stop)
419 patcher.start()
422 patcher.start()
420
423
421 @pytest.fixture()
424 @pytest.fixture()
422 def form_defaults(self):
425 def form_defaults(self):
423 from rhodecode.apps.admin.views.settings import AdminSettingsView
426 from rhodecode.apps.admin.views.settings import AdminSettingsView
424 return AdminSettingsView._form_defaults()
427 return AdminSettingsView._form_defaults()
425
428
426 # TODO: johbo: What we really want is to checkpoint before a test run and
429 # TODO: johbo: What we really want is to checkpoint before a test run and
427 # reset the session afterwards.
430 # reset the session afterwards.
428 @pytest.fixture(scope='class', autouse=True)
431 @pytest.fixture(scope='class', autouse=True)
429 def cleanup_settings(self, request, baseapp):
432 def cleanup_settings(self, request, baseapp):
430 ui_id = RhodeCodeUi.ui_id
433 ui_id = RhodeCodeUi.ui_id
431 original_ids = list(
434 original_ids = [r.ui_id for r in RhodeCodeUi.query().with_entities(ui_id)]
432 r.ui_id for r in RhodeCodeUi.query().values(ui_id))
433
435
434 @request.addfinalizer
436 @request.addfinalizer
435 def cleanup():
437 def cleanup():
436 RhodeCodeUi.query().filter(
438 RhodeCodeUi.query().filter(
437 ui_id.notin_(original_ids)).delete(False)
439 ui_id.notin_(original_ids)).delete(False)
438
440
439
441
440 @pytest.mark.usefixtures('autologin_user', 'app')
442 @pytest.mark.usefixtures('autologin_user', 'app')
441 class TestLabsSettings(object):
443 class TestLabsSettings(object):
442 def test_get_settings_page_disabled(self):
444 def test_get_settings_page_disabled(self):
443 with mock.patch.dict(
445 with mock.patch.dict(
444 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
446 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
445
447
446 response = self.app.get(
448 response = self.app.get(
447 route_path('admin_settings_labs'), status=302)
449 route_path('admin_settings_labs'), status=302)
448
450
449 assert response.location.endswith(route_path('admin_settings'))
451 assert response.location.endswith(route_path('admin_settings'))
450
452
451 def test_get_settings_page_enabled(self):
453 def test_get_settings_page_enabled(self):
452 from rhodecode.apps.admin.views import settings
454 from rhodecode.apps.admin.views import settings
453 lab_settings = [
455 lab_settings = [
454 settings.LabSetting(
456 settings.LabSetting(
455 key='rhodecode_bool',
457 key='rhodecode_bool',
456 type='bool',
458 type='bool',
457 group='bool group',
459 group='bool group',
458 label='bool label',
460 label='bool label',
459 help='bool help'
461 help='bool help'
460 ),
462 ),
461 settings.LabSetting(
463 settings.LabSetting(
462 key='rhodecode_text',
464 key='rhodecode_text',
463 type='unicode',
465 type='unicode',
464 group='text group',
466 group='text group',
465 label='text label',
467 label='text label',
466 help='text help'
468 help='text help'
467 ),
469 ),
468 ]
470 ]
469 with mock.patch.dict(rhodecode.CONFIG,
471 with mock.patch.dict(rhodecode.CONFIG,
470 {'labs_settings_active': 'true'}):
472 {'labs_settings_active': 'true'}):
471 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
473 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
472 response = self.app.get(route_path('admin_settings_labs'))
474 response = self.app.get(route_path('admin_settings_labs'))
473
475
474 assert '<label>bool group:</label>' in response
476 assert '<label>bool group:</label>' in response
475 assert '<label for="rhodecode_bool">bool label</label>' in response
477 assert '<label for="rhodecode_bool">bool label</label>' in response
476 assert '<p class="help-block">bool help</p>' in response
478 assert '<p class="help-block">bool help</p>' in response
477 assert 'name="rhodecode_bool" type="checkbox"' in response
479 assert 'name="rhodecode_bool" type="checkbox"' in response
478
480
479 assert '<label>text group:</label>' in response
481 assert '<label>text group:</label>' in response
480 assert '<label for="rhodecode_text">text label</label>' in response
482 assert '<label for="rhodecode_text">text label</label>' in response
481 assert '<p class="help-block">text help</p>' in response
483 assert '<p class="help-block">text help</p>' in response
482 assert 'name="rhodecode_text" size="60" type="text"' in response
484 assert 'name="rhodecode_text" size="60" type="text"' in response
483
485
484
486
485 @pytest.mark.usefixtures('app')
487 @pytest.mark.usefixtures('app')
486 class TestOpenSourceLicenses(object):
488 class TestOpenSourceLicenses(object):
487
489
488 def test_records_are_displayed(self, autologin_user):
490 def test_records_are_displayed(self, autologin_user):
489 sample_licenses = [
491 sample_licenses = [
490 {
492 {
491 "license": [
493 "license": [
492 {
494 {
493 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
495 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
494 "shortName": "bsdOriginal",
496 "shortName": "bsdOriginal",
495 "spdxId": "BSD-4-Clause",
497 "spdxId": "BSD-4-Clause",
496 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
498 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
497 }
499 }
498 ],
500 ],
499 "name": "python2.7-coverage-3.7.1"
501 "name": "python2.7-coverage-3.7.1"
500 },
502 },
501 {
503 {
502 "license": [
504 "license": [
503 {
505 {
504 "fullName": "MIT License",
506 "fullName": "MIT License",
505 "shortName": "mit",
507 "shortName": "mit",
506 "spdxId": "MIT",
508 "spdxId": "MIT",
507 "url": "http://spdx.org/licenses/MIT.html"
509 "url": "http://spdx.org/licenses/MIT.html"
508 }
510 }
509 ],
511 ],
510 "name": "python2.7-bootstrapped-pip-9.0.1"
512 "name": "python2.7-bootstrapped-pip-9.0.1"
511 },
513 },
512 ]
514 ]
513 read_licenses_patch = mock.patch(
515 read_licenses_patch = mock.patch(
514 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
516 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
515 return_value=sample_licenses)
517 return_value=sample_licenses)
516 with read_licenses_patch:
518 with read_licenses_patch:
517 response = self.app.get(
519 response = self.app.get(
518 route_path('admin_settings_open_source'), status=200)
520 route_path('admin_settings_open_source'), status=200)
519
521
520 assert_response = response.assert_response()
522 assert_response = response.assert_response()
521 assert_response.element_contains(
523 assert_response.element_contains(
522 '.panel-heading', 'Licenses of Third Party Packages')
524 '.panel-heading', 'Licenses of Third Party Packages')
523 for license_data in sample_licenses:
525 for license_data in sample_licenses:
524 response.mustcontain(license_data["license"][0]["spdxId"])
526 response.mustcontain(license_data["license"][0]["spdxId"])
525 assert_response.element_contains('.panel-body', license_data["name"])
527 assert_response.element_contains('.panel-body', license_data["name"])
526
528
527 def test_records_can_be_read(self, autologin_user):
529 def test_records_can_be_read(self, autologin_user):
528 response = self.app.get(
530 response = self.app.get(
529 route_path('admin_settings_open_source'), status=200)
531 route_path('admin_settings_open_source'), status=200)
530 assert_response = response.assert_response()
532 assert_response = response.assert_response()
531 assert_response.element_contains(
533 assert_response.element_contains(
532 '.panel-heading', 'Licenses of Third Party Packages')
534 '.panel-heading', 'Licenses of Third Party Packages')
533
535
534 def test_forbidden_when_normal_user(self, autologin_regular_user):
536 def test_forbidden_when_normal_user(self, autologin_regular_user):
535 self.app.get(
537 self.app.get(
536 route_path('admin_settings_open_source'), status=404)
538 route_path('admin_settings_open_source'), status=404)
537
539
538
540
539 @pytest.mark.usefixtures('app')
541 @pytest.mark.usefixtures('app')
540 class TestUserSessions(object):
542 class TestUserSessions(object):
541
543
542 def test_forbidden_when_normal_user(self, autologin_regular_user):
544 def test_forbidden_when_normal_user(self, autologin_regular_user):
543 self.app.get(route_path('admin_settings_sessions'), status=404)
545 self.app.get(route_path('admin_settings_sessions'), status=404)
544
546
545 def test_show_sessions_page(self, autologin_user):
547 def test_show_sessions_page(self, autologin_user):
546 response = self.app.get(route_path('admin_settings_sessions'), status=200)
548 response = self.app.get(route_path('admin_settings_sessions'), status=200)
547 response.mustcontain('file')
549 response.mustcontain('file')
548
550
549 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
551 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
550
552
551 post_data = {
553 post_data = {
552 'csrf_token': csrf_token,
554 'csrf_token': csrf_token,
553 'expire_days': '60'
555 'expire_days': '60'
554 }
556 }
555 response = self.app.post(
557 response = self.app.post(
556 route_path('admin_settings_sessions_cleanup'), params=post_data,
558 route_path('admin_settings_sessions_cleanup'), params=post_data,
557 status=302)
559 status=302)
558 assert_session_flash(response, 'Cleaned up old sessions')
560 assert_session_flash(response, 'Cleaned up old sessions')
559
561
560
562
561 @pytest.mark.usefixtures('app')
563 @pytest.mark.usefixtures('app')
562 class TestAdminSystemInfo(object):
564 class TestAdminSystemInfo(object):
563
565
564 def test_forbidden_when_normal_user(self, autologin_regular_user):
566 def test_forbidden_when_normal_user(self, autologin_regular_user):
565 self.app.get(route_path('admin_settings_system'), status=404)
567 self.app.get(route_path('admin_settings_system'), status=404)
566
568
567 def test_system_info_page(self, autologin_user):
569 def test_system_info_page(self, autologin_user):
568 response = self.app.get(route_path('admin_settings_system'))
570 response = self.app.get(route_path('admin_settings_system'))
569 response.mustcontain('RhodeCode Community Edition, version {}'.format(
571 response.mustcontain('RhodeCode Community Edition, version {}'.format(
570 rhodecode.__version__))
572 rhodecode.__version__))
571
573
572 def test_system_update_new_version(self, autologin_user):
574 def test_system_update_new_version(self, autologin_user):
573 update_data = {
575 update_data = {
574 'versions': [
576 'versions': [
575 {
577 {
576 'version': '100.3.1415926535',
578 'version': '100.3.1415926535',
577 'general': 'The latest version we are ever going to ship'
579 'general': 'The latest version we are ever going to ship'
578 },
580 },
579 {
581 {
580 'version': '0.0.0',
582 'version': '0.0.0',
581 'general': 'The first version we ever shipped'
583 'general': 'The first version we ever shipped'
582 }
584 }
583 ]
585 ]
584 }
586 }
585 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
587 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
586 response = self.app.get(route_path('admin_settings_system_update'))
588 response = self.app.get(route_path('admin_settings_system_update'))
587 response.mustcontain('A <b>new version</b> is available')
589 response.mustcontain('A <b>new version</b> is available')
588
590
589 def test_system_update_nothing_new(self, autologin_user):
591 def test_system_update_nothing_new(self, autologin_user):
590 update_data = {
592 update_data = {
591 'versions': [
593 'versions': [
592 {
594 {
593 'version': '0.0.0',
595 'version': '0.0.0',
594 'general': 'The first version we ever shipped'
596 'general': 'The first version we ever shipped'
595 }
597 }
596 ]
598 ]
597 }
599 }
598 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
600 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
599 response = self.app.get(route_path('admin_settings_system_update'))
601 response = self.app.get(route_path('admin_settings_system_update'))
600 response.mustcontain(
602 response.mustcontain(
601 'This instance is already running the <b>latest</b> stable version')
603 'This instance is already running the <b>latest</b> stable version')
602
604
603 def test_system_update_bad_response(self, autologin_user):
605 def test_system_update_bad_response(self, autologin_user):
604 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
606 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
605 response = self.app.get(route_path('admin_settings_system_update'))
607 response = self.app.get(route_path('admin_settings_system_update'))
606 response.mustcontain(
608 response.mustcontain(
607 'Bad data sent from update server')
609 'Bad data sent from update server')
608
610
609
611
610 @pytest.mark.usefixtures("app")
612 @pytest.mark.usefixtures("app")
611 class TestAdminSettingsIssueTracker(object):
613 class TestAdminSettingsIssueTracker(object):
612 RC_PREFIX = 'rhodecode_'
614 RC_PREFIX = 'rhodecode_'
613 SHORT_PATTERN_KEY = 'issuetracker_pat_'
615 SHORT_PATTERN_KEY = 'issuetracker_pat_'
614 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
616 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
615 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
617 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
616
618
617 def test_issuetracker_index(self, autologin_user):
619 def test_issuetracker_index(self, autologin_user):
618 response = self.app.get(route_path('admin_settings_issuetracker'))
620 response = self.app.get(route_path('admin_settings_issuetracker'))
619 assert response.status_code == 200
621 assert response.status_code == 200
620
622
621 def test_add_empty_issuetracker_pattern(
623 def test_add_empty_issuetracker_pattern(
622 self, request, autologin_user, csrf_token):
624 self, request, autologin_user, csrf_token):
623 post_url = route_path('admin_settings_issuetracker_update')
625 post_url = route_path('admin_settings_issuetracker_update')
624 post_data = {
626 post_data = {
625 'csrf_token': csrf_token
627 'csrf_token': csrf_token
626 }
628 }
627 self.app.post(post_url, post_data, status=302)
629 self.app.post(post_url, post_data, status=302)
628
630
629 def test_add_issuetracker_pattern(
631 def test_add_issuetracker_pattern(
630 self, request, autologin_user, csrf_token):
632 self, request, autologin_user, csrf_token):
631 pattern = 'issuetracker_pat'
633 pattern = 'issuetracker_pat'
632 another_pattern = pattern+'1'
634 another_pattern = pattern+'1'
633 post_url = route_path('admin_settings_issuetracker_update')
635 post_url = route_path('admin_settings_issuetracker_update')
634 post_data = {
636 post_data = {
635 'new_pattern_pattern_0': pattern,
637 'new_pattern_pattern_0': pattern,
636 'new_pattern_url_0': 'http://url',
638 'new_pattern_url_0': 'http://url',
637 'new_pattern_prefix_0': 'prefix',
639 'new_pattern_prefix_0': 'prefix',
638 'new_pattern_description_0': 'description',
640 'new_pattern_description_0': 'description',
639 'new_pattern_pattern_1': another_pattern,
641 'new_pattern_pattern_1': another_pattern,
640 'new_pattern_url_1': 'https://url1',
642 'new_pattern_url_1': 'https://url1',
641 'new_pattern_prefix_1': 'prefix1',
643 'new_pattern_prefix_1': 'prefix1',
642 'new_pattern_description_1': 'description1',
644 'new_pattern_description_1': 'description1',
643 'csrf_token': csrf_token
645 'csrf_token': csrf_token
644 }
646 }
645 self.app.post(post_url, post_data, status=302)
647 self.app.post(post_url, post_data, status=302)
646 settings = SettingsModel().get_all_settings()
648 settings = SettingsModel().get_all_settings()
647 self.uid = md5(pattern)
649 self.uid = md5_safe(pattern)
648 assert settings[self.PATTERN_KEY+self.uid] == pattern
650 assert settings[self.PATTERN_KEY+self.uid] == pattern
649 self.another_uid = md5(another_pattern)
651 self.another_uid = md5_safe(another_pattern)
650 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
652 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
651
653
652 @request.addfinalizer
654 @request.addfinalizer
653 def cleanup():
655 def cleanup():
654 defaults = SettingsModel().get_all_settings()
656 defaults = SettingsModel().get_all_settings()
655
657
656 entries = [name for name in defaults if (
658 entries = [name for name in defaults if (
657 (self.uid in name) or (self.another_uid) in name)]
659 (self.uid in name) or (self.another_uid in name))]
658 start = len(self.RC_PREFIX)
660 start = len(self.RC_PREFIX)
659 for del_key in entries:
661 for del_key in entries:
660 # TODO: anderson: get_by_name needs name without prefix
662 # TODO: anderson: get_by_name needs name without prefix
661 entry = SettingsModel().get_setting_by_name(del_key[start:])
663 entry = SettingsModel().get_setting_by_name(del_key[start:])
662 Session().delete(entry)
664 Session().delete(entry)
663
665
664 Session().commit()
666 Session().commit()
665
667
666 def test_edit_issuetracker_pattern(
668 def test_edit_issuetracker_pattern(
667 self, autologin_user, backend, csrf_token, request):
669 self, autologin_user, backend, csrf_token, request):
668
670
669 old_pattern = 'issuetracker_pat1'
671 old_pattern = 'issuetracker_pat1'
670 old_uid = md5(old_pattern)
672 old_uid = md5_safe(old_pattern)
671
673
672 post_url = route_path('admin_settings_issuetracker_update')
674 post_url = route_path('admin_settings_issuetracker_update')
673 post_data = {
675 post_data = {
674 'new_pattern_pattern_0': old_pattern,
676 'new_pattern_pattern_0': old_pattern,
675 'new_pattern_url_0': 'http://url',
677 'new_pattern_url_0': 'http://url',
676 'new_pattern_prefix_0': 'prefix',
678 'new_pattern_prefix_0': 'prefix',
677 'new_pattern_description_0': 'description',
679 'new_pattern_description_0': 'description',
678
680
679 'csrf_token': csrf_token
681 'csrf_token': csrf_token
680 }
682 }
681 self.app.post(post_url, post_data, status=302)
683 self.app.post(post_url, post_data, status=302)
682
684
683 new_pattern = 'issuetracker_pat1_edited'
685 new_pattern = 'issuetracker_pat1_edited'
684 self.new_uid = md5(new_pattern)
686 self.new_uid = md5_safe(new_pattern)
685
687
686 post_url = route_path('admin_settings_issuetracker_update')
688 post_url = route_path('admin_settings_issuetracker_update')
687 post_data = {
689 post_data = {
688 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
690 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
689 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
691 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
690 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
692 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
691 'new_pattern_description_{}'.format(old_uid): 'description_edited',
693 'new_pattern_description_{}'.format(old_uid): 'description_edited',
692 'uid': old_uid,
694 'uid': old_uid,
693 'csrf_token': csrf_token
695 'csrf_token': csrf_token
694 }
696 }
695 self.app.post(post_url, post_data, status=302)
697 self.app.post(post_url, post_data, status=302)
696
698
697 settings = SettingsModel().get_all_settings()
699 settings = SettingsModel().get_all_settings()
698 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
700 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
699 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
701 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
700 assert self.PATTERN_KEY+old_uid not in settings
702 assert self.PATTERN_KEY+old_uid not in settings
701
703
702 @request.addfinalizer
704 @request.addfinalizer
703 def cleanup():
705 def cleanup():
704 IssueTrackerSettingsModel().delete_entries(old_uid)
706 IssueTrackerSettingsModel().delete_entries(old_uid)
705 IssueTrackerSettingsModel().delete_entries(self.new_uid)
707 IssueTrackerSettingsModel().delete_entries(self.new_uid)
706
708
707 def test_replace_issuetracker_pattern_description(
709 def test_replace_issuetracker_pattern_description(
708 self, autologin_user, csrf_token, request, settings_util):
710 self, autologin_user, csrf_token, request, settings_util):
709 prefix = 'issuetracker'
711 prefix = 'issuetracker'
710 pattern = 'issuetracker_pat'
712 pattern = 'issuetracker_pat'
711 self.uid = md5(pattern)
713 self.uid = md5_safe(pattern)
712 pattern_key = '_'.join([prefix, 'pat', self.uid])
714 pattern_key = '_'.join([prefix, 'pat', self.uid])
713 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
715 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
714 desc_key = '_'.join([prefix, 'desc', self.uid])
716 desc_key = '_'.join([prefix, 'desc', self.uid])
715 rc_desc_key = '_'.join(['rhodecode', desc_key])
717 rc_desc_key = '_'.join(['rhodecode', desc_key])
716 new_description = 'new_description'
718 new_description = 'new_description'
717
719
718 settings_util.create_rhodecode_setting(
720 settings_util.create_rhodecode_setting(
719 pattern_key, pattern, 'unicode', cleanup=False)
721 pattern_key, pattern, 'unicode', cleanup=False)
720 settings_util.create_rhodecode_setting(
722 settings_util.create_rhodecode_setting(
721 desc_key, 'old description', 'unicode', cleanup=False)
723 desc_key, 'old description', 'unicode', cleanup=False)
722
724
723 post_url = route_path('admin_settings_issuetracker_update')
725 post_url = route_path('admin_settings_issuetracker_update')
724 post_data = {
726 post_data = {
725 'new_pattern_pattern_0': pattern,
727 'new_pattern_pattern_0': pattern,
726 'new_pattern_url_0': 'https://url',
728 'new_pattern_url_0': 'https://url',
727 'new_pattern_prefix_0': 'prefix',
729 'new_pattern_prefix_0': 'prefix',
728 'new_pattern_description_0': new_description,
730 'new_pattern_description_0': new_description,
729 'uid': self.uid,
731 'uid': self.uid,
730 'csrf_token': csrf_token
732 'csrf_token': csrf_token
731 }
733 }
732 self.app.post(post_url, post_data, status=302)
734 self.app.post(post_url, post_data, status=302)
733 settings = SettingsModel().get_all_settings()
735 settings = SettingsModel().get_all_settings()
734 assert settings[rc_pattern_key] == pattern
736 assert settings[rc_pattern_key] == pattern
735 assert settings[rc_desc_key] == new_description
737 assert settings[rc_desc_key] == new_description
736
738
737 @request.addfinalizer
739 @request.addfinalizer
738 def cleanup():
740 def cleanup():
739 IssueTrackerSettingsModel().delete_entries(self.uid)
741 IssueTrackerSettingsModel().delete_entries(self.uid)
740
742
741 def test_delete_issuetracker_pattern(
743 def test_delete_issuetracker_pattern(
742 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
744 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
743
745
744 old_pattern = 'issuetracker_pat_deleted'
746 old_pattern = 'issuetracker_pat_deleted'
745 old_uid = md5(old_pattern)
747 old_uid = md5_safe(old_pattern)
746
748
747 post_url = route_path('admin_settings_issuetracker_update')
749 post_url = route_path('admin_settings_issuetracker_update')
748 post_data = {
750 post_data = {
749 'new_pattern_pattern_0': old_pattern,
751 'new_pattern_pattern_0': old_pattern,
750 'new_pattern_url_0': 'http://url',
752 'new_pattern_url_0': 'http://url',
751 'new_pattern_prefix_0': 'prefix',
753 'new_pattern_prefix_0': 'prefix',
752 'new_pattern_description_0': 'description',
754 'new_pattern_description_0': 'description',
753
755
754 'csrf_token': csrf_token
756 'csrf_token': csrf_token
755 }
757 }
756 self.app.post(post_url, post_data, status=302)
758 self.app.post(post_url, post_data, status=302)
757
759
758 post_url = route_path('admin_settings_issuetracker_delete')
760 post_url = route_path('admin_settings_issuetracker_delete')
759 post_data = {
761 post_data = {
760 'uid': old_uid,
762 'uid': old_uid,
761 'csrf_token': csrf_token
763 'csrf_token': csrf_token
762 }
764 }
763 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
765 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
764 settings = SettingsModel().get_all_settings()
766 settings = SettingsModel().get_all_settings()
765 assert self.PATTERN_KEY+old_uid not in settings
767 assert self.PATTERN_KEY+old_uid not in settings
766 assert self.DESC_KEY + old_uid not in settings
768 assert self.DESC_KEY + old_uid not in settings
@@ -1,169 +1,171 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.model.db import UserGroup, User
22 from rhodecode.model.db import UserGroup, User
23 from rhodecode.model.meta import Session
23 from rhodecode.model.meta import Session
24
24
25 from rhodecode.tests import (
25 from rhodecode.tests import (
26 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
26 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
27 from rhodecode.tests.fixture import Fixture
27 from rhodecode.tests.fixture import Fixture
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.request, urllib.parse, urllib.error
33 import urllib.request
34 import urllib.parse
35 import urllib.error
34 from rhodecode.apps._base import ADMIN_PREFIX
36 from rhodecode.apps._base import ADMIN_PREFIX
35
37
36 base_url = {
38 base_url = {
37 'user_groups': ADMIN_PREFIX + '/user_groups',
39 'user_groups': ADMIN_PREFIX + '/user_groups',
38 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
40 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
39 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
41 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
40 'user_groups_new': ADMIN_PREFIX + '/user_groups/new',
42 'user_groups_new': ADMIN_PREFIX + '/user_groups/new',
41 'user_groups_create': ADMIN_PREFIX + '/user_groups/create',
43 'user_groups_create': ADMIN_PREFIX + '/user_groups/create',
42 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit',
44 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit',
43 }[name].format(**kwargs)
45 }[name].format(**kwargs)
44
46
45 if params:
47 if params:
46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
47 return base_url
49 return base_url
48
50
49
51
50 class TestAdminUserGroupsView(TestController):
52 class TestAdminUserGroupsView(TestController):
51
53
52 def test_show_users(self):
54 def test_show_users(self):
53 self.log_user()
55 self.log_user()
54 self.app.get(route_path('user_groups'))
56 self.app.get(route_path('user_groups'))
55
57
56 def test_show_user_groups_data(self, xhr_header):
58 def test_show_user_groups_data(self, xhr_header):
57 self.log_user()
59 self.log_user()
58 response = self.app.get(route_path(
60 response = self.app.get(route_path(
59 'user_groups_data'), extra_environ=xhr_header)
61 'user_groups_data'), extra_environ=xhr_header)
60
62
61 all_user_groups = UserGroup.query().count()
63 all_user_groups = UserGroup.query().count()
62 assert response.json['recordsTotal'] == all_user_groups
64 assert response.json['recordsTotal'] == all_user_groups
63
65
64 def test_show_user_groups_data_filtered(self, xhr_header):
66 def test_show_user_groups_data_filtered(self, xhr_header):
65 self.log_user()
67 self.log_user()
66 response = self.app.get(route_path(
68 response = self.app.get(route_path(
67 'user_groups_data', params={'search[value]': 'empty_search'}),
69 'user_groups_data', params={'search[value]': 'empty_search'}),
68 extra_environ=xhr_header)
70 extra_environ=xhr_header)
69
71
70 all_user_groups = UserGroup.query().count()
72 all_user_groups = UserGroup.query().count()
71 assert response.json['recordsTotal'] == all_user_groups
73 assert response.json['recordsTotal'] == all_user_groups
72 assert response.json['recordsFiltered'] == 0
74 assert response.json['recordsFiltered'] == 0
73
75
74 def test_usergroup_escape(self, user_util, xhr_header):
76 def test_usergroup_escape(self, user_util, xhr_header):
75 self.log_user()
77 self.log_user()
76
78
77 xss_img = '<img src="/image1" onload="alert(\'Hello, World!\');">'
79 xss_img = '<img src="/image1" onload="alert(\'Hello, World!\');">'
78 user = user_util.create_user()
80 user = user_util.create_user()
79 user.name = xss_img
81 user.name = xss_img
80 user.lastname = xss_img
82 user.lastname = xss_img
81 Session().add(user)
83 Session().add(user)
82 Session().commit()
84 Session().commit()
83
85
84 user_group = user_util.create_user_group()
86 user_group = user_util.create_user_group()
85
87
86 user_group.users_group_name = xss_img
88 user_group.users_group_name = xss_img
87 user_group.user_group_description = '<strong onload="alert();">DESC</strong>'
89 user_group.user_group_description = '<strong onload="alert();">DESC</strong>'
88
90
89 response = self.app.get(
91 response = self.app.get(
90 route_path('user_groups_data'), extra_environ=xhr_header)
92 route_path('user_groups_data'), extra_environ=xhr_header)
91
93
92 response.mustcontain(
94 response.mustcontain(
93 '&lt;strong onload=&#34;alert();&#34;&gt;DESC&lt;/strong&gt;')
95 '&lt;strong onload=&#34;alert();&#34;&gt;DESC&lt;/strong&gt;')
94 response.mustcontain(
96 response.mustcontain(
95 '&lt;img src=&#34;/image1&#34; onload=&#34;'
97 '&lt;img src=&#34;/image1&#34; onload=&#34;'
96 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
98 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
97
99
98 def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
100 def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
99 self.log_user()
101 self.log_user()
100 ug = user_util.create_user_group()
102 ug = user_util.create_user_group()
101 response = self.app.get(
103 response = self.app.get(
102 route_path('user_group_members_data', user_group_id=ug.users_group_id),
104 route_path('user_group_members_data', user_group_id=ug.users_group_id),
103 extra_environ=xhr_header)
105 extra_environ=xhr_header)
104
106
105 assert response.json == {'members': []}
107 assert response.json == {'members': []}
106
108
107 def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
109 def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
108 self.log_user()
110 self.log_user()
109 members = [u.user_id for u in User.get_all()]
111 members = [u.user_id for u in User.get_all()]
110 ug = user_util.create_user_group(members=members)
112 ug = user_util.create_user_group(members=members)
111 response = self.app.get(
113 response = self.app.get(
112 route_path('user_group_members_data',
114 route_path('user_group_members_data',
113 user_group_id=ug.users_group_id),
115 user_group_id=ug.users_group_id),
114 extra_environ=xhr_header)
116 extra_environ=xhr_header)
115
117
116 assert len(response.json['members']) == len(members)
118 assert len(response.json['members']) == len(members)
117
119
118 def test_creation_page(self):
120 def test_creation_page(self):
119 self.log_user()
121 self.log_user()
120 self.app.get(route_path('user_groups_new'), status=200)
122 self.app.get(route_path('user_groups_new'), status=200)
121
123
122 def test_create(self):
124 def test_create(self):
123 from rhodecode.lib import helpers as h
125 from rhodecode.lib import helpers as h
124
126
125 self.log_user()
127 self.log_user()
126 users_group_name = 'test_user_group'
128 users_group_name = 'test_user_group'
127 response = self.app.post(route_path('user_groups_create'), {
129 response = self.app.post(route_path('user_groups_create'), {
128 'users_group_name': users_group_name,
130 'users_group_name': users_group_name,
129 'user_group_description': 'DESC',
131 'user_group_description': 'DESC',
130 'active': True,
132 'active': True,
131 'csrf_token': self.csrf_token})
133 'csrf_token': self.csrf_token})
132
134
133 user_group_id = UserGroup.get_by_group_name(
135 user_group_id = UserGroup.get_by_group_name(
134 users_group_name).users_group_id
136 users_group_name).users_group_id
135
137
136 user_group_link = h.link_to(
138 user_group_link = h.link_to(
137 users_group_name,
139 users_group_name,
138 route_path('edit_user_group', user_group_id=user_group_id))
140 route_path('edit_user_group', user_group_id=user_group_id))
139
141
140 assert_session_flash(
142 assert_session_flash(
141 response,
143 response,
142 'Created user group %s' % user_group_link)
144 'Created user group %s' % user_group_link)
143
145
144 fixture.destroy_user_group(users_group_name)
146 fixture.destroy_user_group(users_group_name)
145
147
146 def test_create_with_empty_name(self):
148 def test_create_with_empty_name(self):
147 self.log_user()
149 self.log_user()
148
150
149 response = self.app.post(route_path('user_groups_create'), {
151 response = self.app.post(route_path('user_groups_create'), {
150 'users_group_name': '',
152 'users_group_name': '',
151 'user_group_description': 'DESC',
153 'user_group_description': 'DESC',
152 'active': True,
154 'active': True,
153 'csrf_token': self.csrf_token}, status=200)
155 'csrf_token': self.csrf_token}, status=200)
154
156
155 response.mustcontain('Please enter a value')
157 response.mustcontain('Please enter a value')
156
158
157 def test_create_duplicate(self, user_util):
159 def test_create_duplicate(self, user_util):
158 self.log_user()
160 self.log_user()
159
161
160 user_group = user_util.create_user_group()
162 user_group = user_util.create_user_group()
161 duplicate_name = user_group.users_group_name
163 duplicate_name = user_group.users_group_name
162 response = self.app.post(route_path('user_groups_create'), {
164 response = self.app.post(route_path('user_groups_create'), {
163 'users_group_name': duplicate_name,
165 'users_group_name': duplicate_name,
164 'user_group_description': 'DESC',
166 'user_group_description': 'DESC',
165 'active': True,
167 'active': True,
166 'csrf_token': self.csrf_token}, status=200)
168 'csrf_token': self.csrf_token}, status=200)
167
169
168 response.mustcontain(
170 response.mustcontain(
169 'User group `{}` already exists'.format(duplicate_name))
171 'User group `{}` already exists'.format(duplicate_name))
@@ -1,793 +1,795 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21 from sqlalchemy.orm.exc import NoResultFound
21 from sqlalchemy.orm.exc import NoResultFound
22
22
23 from rhodecode.lib import auth
23 from rhodecode.lib import auth
24 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
25 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
25 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
26 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
27 from rhodecode.model.user import UserModel
27 from rhodecode.model.user import UserModel
28
28
29 from rhodecode.tests import (
29 from rhodecode.tests import (
30 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
30 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
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.request, urllib.parse, urllib.error
37 import urllib.request
38 import urllib.parse
39 import urllib.error
38 from rhodecode.apps._base import ADMIN_PREFIX
40 from rhodecode.apps._base import ADMIN_PREFIX
39
41
40 base_url = {
42 base_url = {
41 'users':
43 'users':
42 ADMIN_PREFIX + '/users',
44 ADMIN_PREFIX + '/users',
43 'users_data':
45 'users_data':
44 ADMIN_PREFIX + '/users_data',
46 ADMIN_PREFIX + '/users_data',
45 'users_create':
47 'users_create':
46 ADMIN_PREFIX + '/users/create',
48 ADMIN_PREFIX + '/users/create',
47 'users_new':
49 'users_new':
48 ADMIN_PREFIX + '/users/new',
50 ADMIN_PREFIX + '/users/new',
49 'user_edit':
51 'user_edit':
50 ADMIN_PREFIX + '/users/{user_id}/edit',
52 ADMIN_PREFIX + '/users/{user_id}/edit',
51 'user_edit_advanced':
53 'user_edit_advanced':
52 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
54 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
53 'user_edit_global_perms':
55 'user_edit_global_perms':
54 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
56 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
55 'user_edit_global_perms_update':
57 'user_edit_global_perms_update':
56 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
58 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
57 'user_update':
59 'user_update':
58 ADMIN_PREFIX + '/users/{user_id}/update',
60 ADMIN_PREFIX + '/users/{user_id}/update',
59 'user_delete':
61 'user_delete':
60 ADMIN_PREFIX + '/users/{user_id}/delete',
62 ADMIN_PREFIX + '/users/{user_id}/delete',
61 'user_create_personal_repo_group':
63 'user_create_personal_repo_group':
62 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
64 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
63
65
64 'edit_user_auth_tokens':
66 'edit_user_auth_tokens':
65 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
67 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
66 'edit_user_auth_tokens_add':
68 'edit_user_auth_tokens_add':
67 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
69 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
68 'edit_user_auth_tokens_delete':
70 'edit_user_auth_tokens_delete':
69 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
71 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
70
72
71 'edit_user_emails':
73 'edit_user_emails':
72 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
74 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
73 'edit_user_emails_add':
75 'edit_user_emails_add':
74 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
76 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
75 'edit_user_emails_delete':
77 'edit_user_emails_delete':
76 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
78 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
77
79
78 'edit_user_ips':
80 'edit_user_ips':
79 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
81 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
80 'edit_user_ips_add':
82 'edit_user_ips_add':
81 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
83 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
82 'edit_user_ips_delete':
84 'edit_user_ips_delete':
83 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
85 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
84
86
85 'edit_user_perms_summary':
87 'edit_user_perms_summary':
86 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
88 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
87 'edit_user_perms_summary_json':
89 'edit_user_perms_summary_json':
88 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
90 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
89
91
90 'edit_user_audit_logs':
92 'edit_user_audit_logs':
91 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
93 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
92
94
93 'edit_user_audit_logs_download':
95 'edit_user_audit_logs_download':
94 ADMIN_PREFIX + '/users/{user_id}/edit/audit/download',
96 ADMIN_PREFIX + '/users/{user_id}/edit/audit/download',
95
97
96 }[name].format(**kwargs)
98 }[name].format(**kwargs)
97
99
98 if params:
100 if params:
99 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
101 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
100 return base_url
102 return base_url
101
103
102
104
103 class TestAdminUsersView(TestController):
105 class TestAdminUsersView(TestController):
104
106
105 def test_show_users(self):
107 def test_show_users(self):
106 self.log_user()
108 self.log_user()
107 self.app.get(route_path('users'))
109 self.app.get(route_path('users'))
108
110
109 def test_show_users_data(self, xhr_header):
111 def test_show_users_data(self, xhr_header):
110 self.log_user()
112 self.log_user()
111 response = self.app.get(route_path(
113 response = self.app.get(route_path(
112 'users_data'), extra_environ=xhr_header)
114 'users_data'), extra_environ=xhr_header)
113
115
114 all_users = User.query().filter(
116 all_users = User.query().filter(
115 User.username != User.DEFAULT_USER).count()
117 User.username != User.DEFAULT_USER).count()
116 assert response.json['recordsTotal'] == all_users
118 assert response.json['recordsTotal'] == all_users
117
119
118 def test_show_users_data_filtered(self, xhr_header):
120 def test_show_users_data_filtered(self, xhr_header):
119 self.log_user()
121 self.log_user()
120 response = self.app.get(route_path(
122 response = self.app.get(route_path(
121 'users_data', params={'search[value]': 'empty_search'}),
123 'users_data', params={'search[value]': 'empty_search'}),
122 extra_environ=xhr_header)
124 extra_environ=xhr_header)
123
125
124 all_users = User.query().filter(
126 all_users = User.query().filter(
125 User.username != User.DEFAULT_USER).count()
127 User.username != User.DEFAULT_USER).count()
126 assert response.json['recordsTotal'] == all_users
128 assert response.json['recordsTotal'] == all_users
127 assert response.json['recordsFiltered'] == 0
129 assert response.json['recordsFiltered'] == 0
128
130
129 def test_auth_tokens_default_user(self):
131 def test_auth_tokens_default_user(self):
130 self.log_user()
132 self.log_user()
131 user = User.get_default_user()
133 user = User.get_default_user()
132 response = self.app.get(
134 response = self.app.get(
133 route_path('edit_user_auth_tokens', user_id=user.user_id),
135 route_path('edit_user_auth_tokens', user_id=user.user_id),
134 status=302)
136 status=302)
135
137
136 def test_auth_tokens(self):
138 def test_auth_tokens(self):
137 self.log_user()
139 self.log_user()
138
140
139 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
141 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
140 user_id = user.user_id
142 user_id = user.user_id
141 auth_tokens = user.auth_tokens
143 auth_tokens = user.auth_tokens
142 response = self.app.get(
144 response = self.app.get(
143 route_path('edit_user_auth_tokens', user_id=user_id))
145 route_path('edit_user_auth_tokens', user_id=user_id))
144 for token in auth_tokens:
146 for token in auth_tokens:
145 response.mustcontain(token[:4])
147 response.mustcontain(token[:4])
146 response.mustcontain('never')
148 response.mustcontain('never')
147
149
148 @pytest.mark.parametrize("desc, lifetime", [
150 @pytest.mark.parametrize("desc, lifetime", [
149 ('forever', -1),
151 ('forever', -1),
150 ('5mins', 60*5),
152 ('5mins', 60*5),
151 ('30days', 60*60*24*30),
153 ('30days', 60*60*24*30),
152 ])
154 ])
153 def test_add_auth_token(self, desc, lifetime, user_util):
155 def test_add_auth_token(self, desc, lifetime, user_util):
154 self.log_user()
156 self.log_user()
155 user = user_util.create_user()
157 user = user_util.create_user()
156 user_id = user.user_id
158 user_id = user.user_id
157
159
158 response = self.app.post(
160 response = self.app.post(
159 route_path('edit_user_auth_tokens_add', user_id=user_id),
161 route_path('edit_user_auth_tokens_add', user_id=user_id),
160 {'description': desc, 'lifetime': lifetime,
162 {'description': desc, 'lifetime': lifetime,
161 'csrf_token': self.csrf_token})
163 'csrf_token': self.csrf_token})
162 assert_session_flash(response, 'Auth token successfully created')
164 assert_session_flash(response, 'Auth token successfully created')
163
165
164 response = response.follow()
166 response = response.follow()
165 user = User.get(user_id)
167 user = User.get(user_id)
166 for auth_token in user.auth_tokens:
168 for auth_token in user.auth_tokens:
167 response.mustcontain(auth_token[:4])
169 response.mustcontain(auth_token[:4])
168
170
169 def test_delete_auth_token(self, user_util):
171 def test_delete_auth_token(self, user_util):
170 self.log_user()
172 self.log_user()
171 user = user_util.create_user()
173 user = user_util.create_user()
172 user_id = user.user_id
174 user_id = user.user_id
173 keys = user.auth_tokens
175 keys = user.auth_tokens
174 assert 2 == len(keys)
176 assert 2 == len(keys)
175
177
176 response = self.app.post(
178 response = self.app.post(
177 route_path('edit_user_auth_tokens_add', user_id=user_id),
179 route_path('edit_user_auth_tokens_add', user_id=user_id),
178 {'description': 'desc', 'lifetime': -1,
180 {'description': 'desc', 'lifetime': -1,
179 'csrf_token': self.csrf_token})
181 'csrf_token': self.csrf_token})
180 assert_session_flash(response, 'Auth token successfully created')
182 assert_session_flash(response, 'Auth token successfully created')
181 response.follow()
183 response.follow()
182
184
183 # now delete our key
185 # now delete our key
184 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
186 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
185 assert 3 == len(keys)
187 assert 3 == len(keys)
186
188
187 response = self.app.post(
189 response = self.app.post(
188 route_path('edit_user_auth_tokens_delete', user_id=user_id),
190 route_path('edit_user_auth_tokens_delete', user_id=user_id),
189 {'del_auth_token': keys[0].user_api_key_id,
191 {'del_auth_token': keys[0].user_api_key_id,
190 'csrf_token': self.csrf_token})
192 'csrf_token': self.csrf_token})
191
193
192 assert_session_flash(response, 'Auth token successfully deleted')
194 assert_session_flash(response, 'Auth token successfully deleted')
193 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
195 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
194 assert 2 == len(keys)
196 assert 2 == len(keys)
195
197
196 def test_ips(self):
198 def test_ips(self):
197 self.log_user()
199 self.log_user()
198 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
200 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
199 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
201 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
200 response.mustcontain('All IP addresses are allowed')
202 response.mustcontain('All IP addresses are allowed')
201
203
202 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
204 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
203 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
205 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
204 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
206 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
205 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
207 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
206 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
208 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
207 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
209 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
208 ('127_bad_ip', 'foobar', 'foobar', True),
210 ('127_bad_ip', 'foobar', 'foobar', True),
209 ])
211 ])
210 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
212 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
211 self.log_user()
213 self.log_user()
212 user = user_util.create_user(username=test_name)
214 user = user_util.create_user(username=test_name)
213 user_id = user.user_id
215 user_id = user.user_id
214
216
215 response = self.app.post(
217 response = self.app.post(
216 route_path('edit_user_ips_add', user_id=user_id),
218 route_path('edit_user_ips_add', user_id=user_id),
217 params={'new_ip': ip, 'csrf_token': self.csrf_token})
219 params={'new_ip': ip, 'csrf_token': self.csrf_token})
218
220
219 if failure:
221 if failure:
220 assert_session_flash(
222 assert_session_flash(
221 response, 'Please enter a valid IPv4 or IpV6 address')
223 response, 'Please enter a valid IPv4 or IpV6 address')
222 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
224 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
223
225
224 response.mustcontain(no=[ip])
226 response.mustcontain(no=[ip])
225 response.mustcontain(no=[ip_range])
227 response.mustcontain(no=[ip_range])
226
228
227 else:
229 else:
228 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
230 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
229 response.mustcontain(ip)
231 response.mustcontain(ip)
230 response.mustcontain(ip_range)
232 response.mustcontain(ip_range)
231
233
232 def test_ips_delete(self, user_util):
234 def test_ips_delete(self, user_util):
233 self.log_user()
235 self.log_user()
234 user = user_util.create_user()
236 user = user_util.create_user()
235 user_id = user.user_id
237 user_id = user.user_id
236 ip = '127.0.0.1/32'
238 ip = '127.0.0.1/32'
237 ip_range = '127.0.0.1 - 127.0.0.1'
239 ip_range = '127.0.0.1 - 127.0.0.1'
238 new_ip = UserModel().add_extra_ip(user_id, ip)
240 new_ip = UserModel().add_extra_ip(user_id, ip)
239 Session().commit()
241 Session().commit()
240 new_ip_id = new_ip.ip_id
242 new_ip_id = new_ip.ip_id
241
243
242 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
244 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
243 response.mustcontain(ip)
245 response.mustcontain(ip)
244 response.mustcontain(ip_range)
246 response.mustcontain(ip_range)
245
247
246 self.app.post(
248 self.app.post(
247 route_path('edit_user_ips_delete', user_id=user_id),
249 route_path('edit_user_ips_delete', user_id=user_id),
248 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
250 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
249
251
250 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
252 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
251 response.mustcontain('All IP addresses are allowed')
253 response.mustcontain('All IP addresses are allowed')
252 response.mustcontain(no=[ip])
254 response.mustcontain(no=[ip])
253 response.mustcontain(no=[ip_range])
255 response.mustcontain(no=[ip_range])
254
256
255 def test_emails(self):
257 def test_emails(self):
256 self.log_user()
258 self.log_user()
257 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
259 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
258 response = self.app.get(
260 response = self.app.get(
259 route_path('edit_user_emails', user_id=user.user_id))
261 route_path('edit_user_emails', user_id=user.user_id))
260 response.mustcontain('No additional emails specified')
262 response.mustcontain('No additional emails specified')
261
263
262 def test_emails_add(self, user_util):
264 def test_emails_add(self, user_util):
263 self.log_user()
265 self.log_user()
264 user = user_util.create_user()
266 user = user_util.create_user()
265 user_id = user.user_id
267 user_id = user.user_id
266
268
267 self.app.post(
269 self.app.post(
268 route_path('edit_user_emails_add', user_id=user_id),
270 route_path('edit_user_emails_add', user_id=user_id),
269 params={'new_email': 'example@rhodecode.com',
271 params={'new_email': 'example@rhodecode.com',
270 'csrf_token': self.csrf_token})
272 'csrf_token': self.csrf_token})
271
273
272 response = self.app.get(
274 response = self.app.get(
273 route_path('edit_user_emails', user_id=user_id))
275 route_path('edit_user_emails', user_id=user_id))
274 response.mustcontain('example@rhodecode.com')
276 response.mustcontain('example@rhodecode.com')
275
277
276 def test_emails_add_existing_email(self, user_util, user_regular):
278 def test_emails_add_existing_email(self, user_util, user_regular):
277 existing_email = user_regular.email
279 existing_email = user_regular.email
278
280
279 self.log_user()
281 self.log_user()
280 user = user_util.create_user()
282 user = user_util.create_user()
281 user_id = user.user_id
283 user_id = user.user_id
282
284
283 response = self.app.post(
285 response = self.app.post(
284 route_path('edit_user_emails_add', user_id=user_id),
286 route_path('edit_user_emails_add', user_id=user_id),
285 params={'new_email': existing_email,
287 params={'new_email': existing_email,
286 'csrf_token': self.csrf_token})
288 'csrf_token': self.csrf_token})
287 assert_session_flash(
289 assert_session_flash(
288 response, 'This e-mail address is already taken')
290 response, 'This e-mail address is already taken')
289
291
290 response = self.app.get(
292 response = self.app.get(
291 route_path('edit_user_emails', user_id=user_id))
293 route_path('edit_user_emails', user_id=user_id))
292 response.mustcontain(no=[existing_email])
294 response.mustcontain(no=[existing_email])
293
295
294 def test_emails_delete(self, user_util):
296 def test_emails_delete(self, user_util):
295 self.log_user()
297 self.log_user()
296 user = user_util.create_user()
298 user = user_util.create_user()
297 user_id = user.user_id
299 user_id = user.user_id
298
300
299 self.app.post(
301 self.app.post(
300 route_path('edit_user_emails_add', user_id=user_id),
302 route_path('edit_user_emails_add', user_id=user_id),
301 params={'new_email': 'example@rhodecode.com',
303 params={'new_email': 'example@rhodecode.com',
302 'csrf_token': self.csrf_token})
304 'csrf_token': self.csrf_token})
303
305
304 response = self.app.get(
306 response = self.app.get(
305 route_path('edit_user_emails', user_id=user_id))
307 route_path('edit_user_emails', user_id=user_id))
306 response.mustcontain('example@rhodecode.com')
308 response.mustcontain('example@rhodecode.com')
307
309
308 user_email = UserEmailMap.query()\
310 user_email = UserEmailMap.query()\
309 .filter(UserEmailMap.email == 'example@rhodecode.com') \
311 .filter(UserEmailMap.email == 'example@rhodecode.com') \
310 .filter(UserEmailMap.user_id == user_id)\
312 .filter(UserEmailMap.user_id == user_id)\
311 .one()
313 .one()
312
314
313 del_email_id = user_email.email_id
315 del_email_id = user_email.email_id
314 self.app.post(
316 self.app.post(
315 route_path('edit_user_emails_delete', user_id=user_id),
317 route_path('edit_user_emails_delete', user_id=user_id),
316 params={'del_email_id': del_email_id,
318 params={'del_email_id': del_email_id,
317 'csrf_token': self.csrf_token})
319 'csrf_token': self.csrf_token})
318
320
319 response = self.app.get(
321 response = self.app.get(
320 route_path('edit_user_emails', user_id=user_id))
322 route_path('edit_user_emails', user_id=user_id))
321 response.mustcontain(no=['example@rhodecode.com'])
323 response.mustcontain(no=['example@rhodecode.com'])
322
324
323 def test_create(self, request, xhr_header):
325 def test_create(self, request, xhr_header):
324 self.log_user()
326 self.log_user()
325 username = 'newtestuser'
327 username = 'newtestuser'
326 password = 'test12'
328 password = 'test12'
327 password_confirmation = password
329 password_confirmation = password
328 name = 'name'
330 name = 'name'
329 lastname = 'lastname'
331 lastname = 'lastname'
330 email = 'mail@mail.com'
332 email = 'mail@mail.com'
331
333
332 self.app.get(route_path('users_new'))
334 self.app.get(route_path('users_new'))
333
335
334 response = self.app.post(route_path('users_create'), params={
336 response = self.app.post(route_path('users_create'), params={
335 'username': username,
337 'username': username,
336 'password': password,
338 'password': password,
337 'description': 'mr CTO',
339 'description': 'mr CTO',
338 'password_confirmation': password_confirmation,
340 'password_confirmation': password_confirmation,
339 'firstname': name,
341 'firstname': name,
340 'active': True,
342 'active': True,
341 'lastname': lastname,
343 'lastname': lastname,
342 'extern_name': 'rhodecode',
344 'extern_name': 'rhodecode',
343 'extern_type': 'rhodecode',
345 'extern_type': 'rhodecode',
344 'email': email,
346 'email': email,
345 'csrf_token': self.csrf_token,
347 'csrf_token': self.csrf_token,
346 })
348 })
347 user_link = h.link_to(
349 user_link = h.link_to(
348 username,
350 username,
349 route_path(
351 route_path(
350 'user_edit', user_id=User.get_by_username(username).user_id))
352 'user_edit', user_id=User.get_by_username(username).user_id))
351 assert_session_flash(response, 'Created user %s' % (user_link,))
353 assert_session_flash(response, 'Created user %s' % (user_link,))
352
354
353 @request.addfinalizer
355 @request.addfinalizer
354 def cleanup():
356 def cleanup():
355 fixture.destroy_user(username)
357 fixture.destroy_user(username)
356 Session().commit()
358 Session().commit()
357
359
358 new_user = User.query().filter(User.username == username).one()
360 new_user = User.query().filter(User.username == username).one()
359
361
360 assert new_user.username == username
362 assert new_user.username == username
361 assert auth.check_password(password, new_user.password)
363 assert auth.check_password(password, new_user.password)
362 assert new_user.name == name
364 assert new_user.name == name
363 assert new_user.lastname == lastname
365 assert new_user.lastname == lastname
364 assert new_user.email == email
366 assert new_user.email == email
365
367
366 response = self.app.get(route_path('users_data'),
368 response = self.app.get(route_path('users_data'),
367 extra_environ=xhr_header)
369 extra_environ=xhr_header)
368 response.mustcontain(username)
370 response.mustcontain(username)
369
371
370 def test_create_err(self):
372 def test_create_err(self):
371 self.log_user()
373 self.log_user()
372 username = 'new_user'
374 username = 'new_user'
373 password = ''
375 password = ''
374 name = 'name'
376 name = 'name'
375 lastname = 'lastname'
377 lastname = 'lastname'
376 email = 'errmail.com'
378 email = 'errmail.com'
377
379
378 self.app.get(route_path('users_new'))
380 self.app.get(route_path('users_new'))
379
381
380 response = self.app.post(route_path('users_create'), params={
382 response = self.app.post(route_path('users_create'), params={
381 'username': username,
383 'username': username,
382 'password': password,
384 'password': password,
383 'name': name,
385 'name': name,
384 'active': False,
386 'active': False,
385 'lastname': lastname,
387 'lastname': lastname,
386 'description': 'mr CTO',
388 'description': 'mr CTO',
387 'email': email,
389 'email': email,
388 'csrf_token': self.csrf_token,
390 'csrf_token': self.csrf_token,
389 })
391 })
390
392
391 msg = u'Username "%(username)s" is forbidden'
393 msg = u'Username "%(username)s" is forbidden'
392 msg = h.html_escape(msg % {'username': 'new_user'})
394 msg = h.html_escape(msg % {'username': 'new_user'})
393 response.mustcontain('<span class="error-message">%s</span>' % msg)
395 response.mustcontain('<span class="error-message">%s</span>' % msg)
394 response.mustcontain(
396 response.mustcontain(
395 '<span class="error-message">Please enter a value</span>')
397 '<span class="error-message">Please enter a value</span>')
396 response.mustcontain(
398 response.mustcontain(
397 '<span class="error-message">An email address must contain a'
399 '<span class="error-message">An email address must contain a'
398 ' single @</span>')
400 ' single @</span>')
399
401
400 def get_user():
402 def get_user():
401 Session().query(User).filter(User.username == username).one()
403 Session().query(User).filter(User.username == username).one()
402
404
403 with pytest.raises(NoResultFound):
405 with pytest.raises(NoResultFound):
404 get_user()
406 get_user()
405
407
406 def test_new(self):
408 def test_new(self):
407 self.log_user()
409 self.log_user()
408 self.app.get(route_path('users_new'))
410 self.app.get(route_path('users_new'))
409
411
410 @pytest.mark.parametrize("name, attrs", [
412 @pytest.mark.parametrize("name, attrs", [
411 ('firstname', {'firstname': 'new_username'}),
413 ('firstname', {'firstname': 'new_username'}),
412 ('lastname', {'lastname': 'new_username'}),
414 ('lastname', {'lastname': 'new_username'}),
413 ('admin', {'admin': True}),
415 ('admin', {'admin': True}),
414 ('admin', {'admin': False}),
416 ('admin', {'admin': False}),
415 ('extern_type', {'extern_type': 'ldap'}),
417 ('extern_type', {'extern_type': 'ldap'}),
416 ('extern_type', {'extern_type': None}),
418 ('extern_type', {'extern_type': None}),
417 ('extern_name', {'extern_name': 'test'}),
419 ('extern_name', {'extern_name': 'test'}),
418 ('extern_name', {'extern_name': None}),
420 ('extern_name', {'extern_name': None}),
419 ('active', {'active': False}),
421 ('active', {'active': False}),
420 ('active', {'active': True}),
422 ('active', {'active': True}),
421 ('email', {'email': 'some@email.com'}),
423 ('email', {'email': 'some@email.com'}),
422 ('language', {'language': 'de'}),
424 ('language', {'language': 'de'}),
423 ('language', {'language': 'en'}),
425 ('language', {'language': 'en'}),
424 ('description', {'description': 'hello CTO'}),
426 ('description', {'description': 'hello CTO'}),
425 # ('new_password', {'new_password': 'foobar123',
427 # ('new_password', {'new_password': 'foobar123',
426 # 'password_confirmation': 'foobar123'})
428 # 'password_confirmation': 'foobar123'})
427 ])
429 ])
428 def test_update(self, name, attrs, user_util):
430 def test_update(self, name, attrs, user_util):
429 self.log_user()
431 self.log_user()
430 usr = user_util.create_user(
432 usr = user_util.create_user(
431 password='qweqwe',
433 password='qweqwe',
432 email='testme@rhodecode.org',
434 email='testme@rhodecode.org',
433 extern_type='rhodecode',
435 extern_type='rhodecode',
434 extern_name='xxx',
436 extern_name='xxx',
435 )
437 )
436 user_id = usr.user_id
438 user_id = usr.user_id
437 Session().commit()
439 Session().commit()
438
440
439 params = usr.get_api_data()
441 params = usr.get_api_data()
440 cur_lang = params['language'] or 'en'
442 cur_lang = params['language'] or 'en'
441 params.update({
443 params.update({
442 'password_confirmation': '',
444 'password_confirmation': '',
443 'new_password': '',
445 'new_password': '',
444 'language': cur_lang,
446 'language': cur_lang,
445 'csrf_token': self.csrf_token,
447 'csrf_token': self.csrf_token,
446 })
448 })
447 params.update({'new_password': ''})
449 params.update({'new_password': ''})
448 params.update(attrs)
450 params.update(attrs)
449 if name == 'email':
451 if name == 'email':
450 params['emails'] = [attrs['email']]
452 params['emails'] = [attrs['email']]
451 elif name == 'extern_type':
453 elif name == 'extern_type':
452 # cannot update this via form, expected value is original one
454 # cannot update this via form, expected value is original one
453 params['extern_type'] = "rhodecode"
455 params['extern_type'] = "rhodecode"
454 elif name == 'extern_name':
456 elif name == 'extern_name':
455 # cannot update this via form, expected value is original one
457 # cannot update this via form, expected value is original one
456 params['extern_name'] = 'xxx'
458 params['extern_name'] = 'xxx'
457 # special case since this user is not
459 # special case since this user is not
458 # logged in yet his data is not filled
460 # logged in yet his data is not filled
459 # so we use creation data
461 # so we use creation data
460
462
461 response = self.app.post(
463 response = self.app.post(
462 route_path('user_update', user_id=usr.user_id), params)
464 route_path('user_update', user_id=usr.user_id), params)
463 assert response.status_int == 302
465 assert response.status_int == 302
464 assert_session_flash(response, 'User updated successfully')
466 assert_session_flash(response, 'User updated successfully')
465
467
466 updated_user = User.get(user_id)
468 updated_user = User.get(user_id)
467 updated_params = updated_user.get_api_data()
469 updated_params = updated_user.get_api_data()
468 updated_params.update({'password_confirmation': ''})
470 updated_params.update({'password_confirmation': ''})
469 updated_params.update({'new_password': ''})
471 updated_params.update({'new_password': ''})
470
472
471 del params['csrf_token']
473 del params['csrf_token']
472 assert params == updated_params
474 assert params == updated_params
473
475
474 def test_update_and_migrate_password(
476 def test_update_and_migrate_password(
475 self, autologin_user, real_crypto_backend, user_util):
477 self, autologin_user, real_crypto_backend, user_util):
476
478
477 user = user_util.create_user()
479 user = user_util.create_user()
478 temp_user = user.username
480 temp_user = user.username
479 user.password = auth._RhodeCodeCryptoSha256().hash_create(
481 user.password = auth._RhodeCodeCryptoSha256().hash_create(
480 b'test123')
482 b'test123')
481 Session().add(user)
483 Session().add(user)
482 Session().commit()
484 Session().commit()
483
485
484 params = user.get_api_data()
486 params = user.get_api_data()
485
487
486 params.update({
488 params.update({
487 'password_confirmation': 'qweqwe123',
489 'password_confirmation': 'qweqwe123',
488 'new_password': 'qweqwe123',
490 'new_password': 'qweqwe123',
489 'language': 'en',
491 'language': 'en',
490 'csrf_token': autologin_user.csrf_token,
492 'csrf_token': autologin_user.csrf_token,
491 })
493 })
492
494
493 response = self.app.post(
495 response = self.app.post(
494 route_path('user_update', user_id=user.user_id), params)
496 route_path('user_update', user_id=user.user_id), params)
495 assert response.status_int == 302
497 assert response.status_int == 302
496 assert_session_flash(response, 'User updated successfully')
498 assert_session_flash(response, 'User updated successfully')
497
499
498 # new password should be bcrypted, after log-in and transfer
500 # new password should be bcrypted, after log-in and transfer
499 user = User.get_by_username(temp_user)
501 user = User.get_by_username(temp_user)
500 assert user.password.startswith('$')
502 assert user.password.startswith('$')
501
503
502 updated_user = User.get_by_username(temp_user)
504 updated_user = User.get_by_username(temp_user)
503 updated_params = updated_user.get_api_data()
505 updated_params = updated_user.get_api_data()
504 updated_params.update({'password_confirmation': 'qweqwe123'})
506 updated_params.update({'password_confirmation': 'qweqwe123'})
505 updated_params.update({'new_password': 'qweqwe123'})
507 updated_params.update({'new_password': 'qweqwe123'})
506
508
507 del params['csrf_token']
509 del params['csrf_token']
508 assert params == updated_params
510 assert params == updated_params
509
511
510 def test_delete(self):
512 def test_delete(self):
511 self.log_user()
513 self.log_user()
512 username = 'newtestuserdeleteme'
514 username = 'newtestuserdeleteme'
513
515
514 fixture.create_user(name=username)
516 fixture.create_user(name=username)
515
517
516 new_user = Session().query(User)\
518 new_user = Session().query(User)\
517 .filter(User.username == username).one()
519 .filter(User.username == username).one()
518 response = self.app.post(
520 response = self.app.post(
519 route_path('user_delete', user_id=new_user.user_id),
521 route_path('user_delete', user_id=new_user.user_id),
520 params={'csrf_token': self.csrf_token})
522 params={'csrf_token': self.csrf_token})
521
523
522 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
524 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
523
525
524 def test_delete_owner_of_repository(self, request, user_util):
526 def test_delete_owner_of_repository(self, request, user_util):
525 self.log_user()
527 self.log_user()
526 obj_name = 'test_repo'
528 obj_name = 'test_repo'
527 usr = user_util.create_user()
529 usr = user_util.create_user()
528 username = usr.username
530 username = usr.username
529 fixture.create_repo(obj_name, cur_user=usr.username)
531 fixture.create_repo(obj_name, cur_user=usr.username)
530
532
531 new_user = Session().query(User)\
533 new_user = Session().query(User)\
532 .filter(User.username == username).one()
534 .filter(User.username == username).one()
533 response = self.app.post(
535 response = self.app.post(
534 route_path('user_delete', user_id=new_user.user_id),
536 route_path('user_delete', user_id=new_user.user_id),
535 params={'csrf_token': self.csrf_token})
537 params={'csrf_token': self.csrf_token})
536
538
537 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
539 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
538 'Switch owners or remove those repositories:%s' % (username, obj_name)
540 'Switch owners or remove those repositories:%s' % (username, obj_name)
539 assert_session_flash(response, msg)
541 assert_session_flash(response, msg)
540 fixture.destroy_repo(obj_name)
542 fixture.destroy_repo(obj_name)
541
543
542 def test_delete_owner_of_repository_detaching(self, request, user_util):
544 def test_delete_owner_of_repository_detaching(self, request, user_util):
543 self.log_user()
545 self.log_user()
544 obj_name = 'test_repo'
546 obj_name = 'test_repo'
545 usr = user_util.create_user(auto_cleanup=False)
547 usr = user_util.create_user(auto_cleanup=False)
546 username = usr.username
548 username = usr.username
547 fixture.create_repo(obj_name, cur_user=usr.username)
549 fixture.create_repo(obj_name, cur_user=usr.username)
548 Session().commit()
550 Session().commit()
549
551
550 new_user = Session().query(User)\
552 new_user = Session().query(User)\
551 .filter(User.username == username).one()
553 .filter(User.username == username).one()
552 response = self.app.post(
554 response = self.app.post(
553 route_path('user_delete', user_id=new_user.user_id),
555 route_path('user_delete', user_id=new_user.user_id),
554 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
556 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
555
557
556 msg = 'Detached 1 repositories'
558 msg = 'Detached 1 repositories'
557 assert_session_flash(response, msg)
559 assert_session_flash(response, msg)
558 fixture.destroy_repo(obj_name)
560 fixture.destroy_repo(obj_name)
559
561
560 def test_delete_owner_of_repository_deleting(self, request, user_util):
562 def test_delete_owner_of_repository_deleting(self, request, user_util):
561 self.log_user()
563 self.log_user()
562 obj_name = 'test_repo'
564 obj_name = 'test_repo'
563 usr = user_util.create_user(auto_cleanup=False)
565 usr = user_util.create_user(auto_cleanup=False)
564 username = usr.username
566 username = usr.username
565 fixture.create_repo(obj_name, cur_user=usr.username)
567 fixture.create_repo(obj_name, cur_user=usr.username)
566
568
567 new_user = Session().query(User)\
569 new_user = Session().query(User)\
568 .filter(User.username == username).one()
570 .filter(User.username == username).one()
569 response = self.app.post(
571 response = self.app.post(
570 route_path('user_delete', user_id=new_user.user_id),
572 route_path('user_delete', user_id=new_user.user_id),
571 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
573 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
572
574
573 msg = 'Deleted 1 repositories'
575 msg = 'Deleted 1 repositories'
574 assert_session_flash(response, msg)
576 assert_session_flash(response, msg)
575
577
576 def test_delete_owner_of_repository_group(self, request, user_util):
578 def test_delete_owner_of_repository_group(self, request, user_util):
577 self.log_user()
579 self.log_user()
578 obj_name = 'test_group'
580 obj_name = 'test_group'
579 usr = user_util.create_user()
581 usr = user_util.create_user()
580 username = usr.username
582 username = usr.username
581 fixture.create_repo_group(obj_name, cur_user=usr.username)
583 fixture.create_repo_group(obj_name, cur_user=usr.username)
582
584
583 new_user = Session().query(User)\
585 new_user = Session().query(User)\
584 .filter(User.username == username).one()
586 .filter(User.username == username).one()
585 response = self.app.post(
587 response = self.app.post(
586 route_path('user_delete', user_id=new_user.user_id),
588 route_path('user_delete', user_id=new_user.user_id),
587 params={'csrf_token': self.csrf_token})
589 params={'csrf_token': self.csrf_token})
588
590
589 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
591 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
590 'Switch owners or remove those repository groups:%s' % (username, obj_name)
592 'Switch owners or remove those repository groups:%s' % (username, obj_name)
591 assert_session_flash(response, msg)
593 assert_session_flash(response, msg)
592 fixture.destroy_repo_group(obj_name)
594 fixture.destroy_repo_group(obj_name)
593
595
594 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
596 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
595 self.log_user()
597 self.log_user()
596 obj_name = 'test_group'
598 obj_name = 'test_group'
597 usr = user_util.create_user(auto_cleanup=False)
599 usr = user_util.create_user(auto_cleanup=False)
598 username = usr.username
600 username = usr.username
599 fixture.create_repo_group(obj_name, cur_user=usr.username)
601 fixture.create_repo_group(obj_name, cur_user=usr.username)
600
602
601 new_user = Session().query(User)\
603 new_user = Session().query(User)\
602 .filter(User.username == username).one()
604 .filter(User.username == username).one()
603 response = self.app.post(
605 response = self.app.post(
604 route_path('user_delete', user_id=new_user.user_id),
606 route_path('user_delete', user_id=new_user.user_id),
605 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
607 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
606
608
607 msg = 'Deleted 1 repository groups'
609 msg = 'Deleted 1 repository groups'
608 assert_session_flash(response, msg)
610 assert_session_flash(response, msg)
609
611
610 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
612 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
611 self.log_user()
613 self.log_user()
612 obj_name = 'test_group'
614 obj_name = 'test_group'
613 usr = user_util.create_user(auto_cleanup=False)
615 usr = user_util.create_user(auto_cleanup=False)
614 username = usr.username
616 username = usr.username
615 fixture.create_repo_group(obj_name, cur_user=usr.username)
617 fixture.create_repo_group(obj_name, cur_user=usr.username)
616
618
617 new_user = Session().query(User)\
619 new_user = Session().query(User)\
618 .filter(User.username == username).one()
620 .filter(User.username == username).one()
619 response = self.app.post(
621 response = self.app.post(
620 route_path('user_delete', user_id=new_user.user_id),
622 route_path('user_delete', user_id=new_user.user_id),
621 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
623 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
622
624
623 msg = 'Detached 1 repository groups'
625 msg = 'Detached 1 repository groups'
624 assert_session_flash(response, msg)
626 assert_session_flash(response, msg)
625 fixture.destroy_repo_group(obj_name)
627 fixture.destroy_repo_group(obj_name)
626
628
627 def test_delete_owner_of_user_group(self, request, user_util):
629 def test_delete_owner_of_user_group(self, request, user_util):
628 self.log_user()
630 self.log_user()
629 obj_name = 'test_user_group'
631 obj_name = 'test_user_group'
630 usr = user_util.create_user()
632 usr = user_util.create_user()
631 username = usr.username
633 username = usr.username
632 fixture.create_user_group(obj_name, cur_user=usr.username)
634 fixture.create_user_group(obj_name, cur_user=usr.username)
633
635
634 new_user = Session().query(User)\
636 new_user = Session().query(User)\
635 .filter(User.username == username).one()
637 .filter(User.username == username).one()
636 response = self.app.post(
638 response = self.app.post(
637 route_path('user_delete', user_id=new_user.user_id),
639 route_path('user_delete', user_id=new_user.user_id),
638 params={'csrf_token': self.csrf_token})
640 params={'csrf_token': self.csrf_token})
639
641
640 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
642 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
641 'Switch owners or remove those user groups:%s' % (username, obj_name)
643 'Switch owners or remove those user groups:%s' % (username, obj_name)
642 assert_session_flash(response, msg)
644 assert_session_flash(response, msg)
643 fixture.destroy_user_group(obj_name)
645 fixture.destroy_user_group(obj_name)
644
646
645 def test_delete_owner_of_user_group_detaching(self, request, user_util):
647 def test_delete_owner_of_user_group_detaching(self, request, user_util):
646 self.log_user()
648 self.log_user()
647 obj_name = 'test_user_group'
649 obj_name = 'test_user_group'
648 usr = user_util.create_user(auto_cleanup=False)
650 usr = user_util.create_user(auto_cleanup=False)
649 username = usr.username
651 username = usr.username
650 fixture.create_user_group(obj_name, cur_user=usr.username)
652 fixture.create_user_group(obj_name, cur_user=usr.username)
651
653
652 new_user = Session().query(User)\
654 new_user = Session().query(User)\
653 .filter(User.username == username).one()
655 .filter(User.username == username).one()
654 try:
656 try:
655 response = self.app.post(
657 response = self.app.post(
656 route_path('user_delete', user_id=new_user.user_id),
658 route_path('user_delete', user_id=new_user.user_id),
657 params={'user_user_groups': 'detach',
659 params={'user_user_groups': 'detach',
658 'csrf_token': self.csrf_token})
660 'csrf_token': self.csrf_token})
659
661
660 msg = 'Detached 1 user groups'
662 msg = 'Detached 1 user groups'
661 assert_session_flash(response, msg)
663 assert_session_flash(response, msg)
662 finally:
664 finally:
663 fixture.destroy_user_group(obj_name)
665 fixture.destroy_user_group(obj_name)
664
666
665 def test_delete_owner_of_user_group_deleting(self, request, user_util):
667 def test_delete_owner_of_user_group_deleting(self, request, user_util):
666 self.log_user()
668 self.log_user()
667 obj_name = 'test_user_group'
669 obj_name = 'test_user_group'
668 usr = user_util.create_user(auto_cleanup=False)
670 usr = user_util.create_user(auto_cleanup=False)
669 username = usr.username
671 username = usr.username
670 fixture.create_user_group(obj_name, cur_user=usr.username)
672 fixture.create_user_group(obj_name, cur_user=usr.username)
671
673
672 new_user = Session().query(User)\
674 new_user = Session().query(User)\
673 .filter(User.username == username).one()
675 .filter(User.username == username).one()
674 response = self.app.post(
676 response = self.app.post(
675 route_path('user_delete', user_id=new_user.user_id),
677 route_path('user_delete', user_id=new_user.user_id),
676 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
678 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
677
679
678 msg = 'Deleted 1 user groups'
680 msg = 'Deleted 1 user groups'
679 assert_session_flash(response, msg)
681 assert_session_flash(response, msg)
680
682
681 def test_edit(self, user_util):
683 def test_edit(self, user_util):
682 self.log_user()
684 self.log_user()
683 user = user_util.create_user()
685 user = user_util.create_user()
684 self.app.get(route_path('user_edit', user_id=user.user_id))
686 self.app.get(route_path('user_edit', user_id=user.user_id))
685
687
686 def test_edit_default_user_redirect(self):
688 def test_edit_default_user_redirect(self):
687 self.log_user()
689 self.log_user()
688 user = User.get_default_user()
690 user = User.get_default_user()
689 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
691 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
690
692
691 @pytest.mark.parametrize(
693 @pytest.mark.parametrize(
692 'repo_create, repo_create_write, user_group_create, repo_group_create,'
694 'repo_create, repo_create_write, user_group_create, repo_group_create,'
693 'fork_create, inherit_default_permissions, expect_error,'
695 'fork_create, inherit_default_permissions, expect_error,'
694 'expect_form_error', [
696 'expect_form_error', [
695 ('hg.create.none', 'hg.create.write_on_repogroup.false',
697 ('hg.create.none', 'hg.create.write_on_repogroup.false',
696 'hg.usergroup.create.false', 'hg.repogroup.create.false',
698 'hg.usergroup.create.false', 'hg.repogroup.create.false',
697 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
699 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
698 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
700 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
699 'hg.usergroup.create.false', 'hg.repogroup.create.false',
701 'hg.usergroup.create.false', 'hg.repogroup.create.false',
700 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
702 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
701 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
703 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
702 'hg.usergroup.create.true', 'hg.repogroup.create.true',
704 'hg.usergroup.create.true', 'hg.repogroup.create.true',
703 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
705 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
704 False),
706 False),
705 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
707 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
706 'hg.usergroup.create.true', 'hg.repogroup.create.true',
708 'hg.usergroup.create.true', 'hg.repogroup.create.true',
707 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
709 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
708 True),
710 True),
709 ('', '', '', '', '', '', True, False),
711 ('', '', '', '', '', '', True, False),
710 ])
712 ])
711 def test_global_perms_on_user(
713 def test_global_perms_on_user(
712 self, repo_create, repo_create_write, user_group_create,
714 self, repo_create, repo_create_write, user_group_create,
713 repo_group_create, fork_create, expect_error, expect_form_error,
715 repo_group_create, fork_create, expect_error, expect_form_error,
714 inherit_default_permissions, user_util):
716 inherit_default_permissions, user_util):
715 self.log_user()
717 self.log_user()
716 user = user_util.create_user()
718 user = user_util.create_user()
717 uid = user.user_id
719 uid = user.user_id
718
720
719 # ENABLE REPO CREATE ON A GROUP
721 # ENABLE REPO CREATE ON A GROUP
720 perm_params = {
722 perm_params = {
721 'inherit_default_permissions': False,
723 'inherit_default_permissions': False,
722 'default_repo_create': repo_create,
724 'default_repo_create': repo_create,
723 'default_repo_create_on_write': repo_create_write,
725 'default_repo_create_on_write': repo_create_write,
724 'default_user_group_create': user_group_create,
726 'default_user_group_create': user_group_create,
725 'default_repo_group_create': repo_group_create,
727 'default_repo_group_create': repo_group_create,
726 'default_fork_create': fork_create,
728 'default_fork_create': fork_create,
727 'default_inherit_default_permissions': inherit_default_permissions,
729 'default_inherit_default_permissions': inherit_default_permissions,
728 'csrf_token': self.csrf_token,
730 'csrf_token': self.csrf_token,
729 }
731 }
730 response = self.app.post(
732 response = self.app.post(
731 route_path('user_edit_global_perms_update', user_id=uid),
733 route_path('user_edit_global_perms_update', user_id=uid),
732 params=perm_params)
734 params=perm_params)
733
735
734 if expect_form_error:
736 if expect_form_error:
735 assert response.status_int == 200
737 assert response.status_int == 200
736 response.mustcontain('Value must be one of')
738 response.mustcontain('Value must be one of')
737 else:
739 else:
738 if expect_error:
740 if expect_error:
739 msg = 'An error occurred during permissions saving'
741 msg = 'An error occurred during permissions saving'
740 else:
742 else:
741 msg = 'User global permissions updated successfully'
743 msg = 'User global permissions updated successfully'
742 ug = User.get(uid)
744 ug = User.get(uid)
743 del perm_params['inherit_default_permissions']
745 del perm_params['inherit_default_permissions']
744 del perm_params['csrf_token']
746 del perm_params['csrf_token']
745 assert perm_params == ug.get_default_perms()
747 assert perm_params == ug.get_default_perms()
746 assert_session_flash(response, msg)
748 assert_session_flash(response, msg)
747
749
748 def test_global_permissions_initial_values(self, user_util):
750 def test_global_permissions_initial_values(self, user_util):
749 self.log_user()
751 self.log_user()
750 user = user_util.create_user()
752 user = user_util.create_user()
751 uid = user.user_id
753 uid = user.user_id
752 response = self.app.get(
754 response = self.app.get(
753 route_path('user_edit_global_perms', user_id=uid))
755 route_path('user_edit_global_perms', user_id=uid))
754 default_user = User.get_default_user()
756 default_user = User.get_default_user()
755 default_permissions = default_user.get_default_perms()
757 default_permissions = default_user.get_default_perms()
756 assert_response = response.assert_response()
758 assert_response = response.assert_response()
757 expected_permissions = (
759 expected_permissions = (
758 'default_repo_create', 'default_repo_create_on_write',
760 'default_repo_create', 'default_repo_create_on_write',
759 'default_fork_create', 'default_repo_group_create',
761 'default_fork_create', 'default_repo_group_create',
760 'default_user_group_create', 'default_inherit_default_permissions')
762 'default_user_group_create', 'default_inherit_default_permissions')
761 for permission in expected_permissions:
763 for permission in expected_permissions:
762 css_selector = '[name={}][checked=checked]'.format(permission)
764 css_selector = '[name={}][checked=checked]'.format(permission)
763 element = assert_response.get_element(css_selector)
765 element = assert_response.get_element(css_selector)
764 assert element.value == default_permissions[permission]
766 assert element.value == default_permissions[permission]
765
767
766 def test_perms_summary_page(self):
768 def test_perms_summary_page(self):
767 user = self.log_user()
769 user = self.log_user()
768 response = self.app.get(
770 response = self.app.get(
769 route_path('edit_user_perms_summary', user_id=user['user_id']))
771 route_path('edit_user_perms_summary', user_id=user['user_id']))
770 for repo in Repository.query().all():
772 for repo in Repository.query().all():
771 response.mustcontain(repo.repo_name)
773 response.mustcontain(repo.repo_name)
772
774
773 def test_perms_summary_page_json(self):
775 def test_perms_summary_page_json(self):
774 user = self.log_user()
776 user = self.log_user()
775 response = self.app.get(
777 response = self.app.get(
776 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
778 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
777 for repo in Repository.query().all():
779 for repo in Repository.query().all():
778 response.mustcontain(repo.repo_name)
780 response.mustcontain(repo.repo_name)
779
781
780 def test_audit_log_page(self):
782 def test_audit_log_page(self):
781 user = self.log_user()
783 user = self.log_user()
782 self.app.get(
784 self.app.get(
783 route_path('edit_user_audit_logs', user_id=user['user_id']))
785 route_path('edit_user_audit_logs', user_id=user['user_id']))
784
786
785 def test_audit_log_page_download(self):
787 def test_audit_log_page_download(self):
786 user = self.log_user()
788 user = self.log_user()
787 user_id = user['user_id']
789 user_id = user['user_id']
788 response = self.app.get(
790 response = self.app.get(
789 route_path('edit_user_audit_logs_download', user_id=user_id))
791 route_path('edit_user_audit_logs_download', user_id=user_id))
790
792
791 assert response.content_disposition == \
793 assert response.content_disposition == \
792 'attachment; filename=user_{}_audit_logs.json'.format(user_id)
794 'attachment; filename=user_{}_audit_logs.json'.format(user_id)
793 assert response.content_type == "application/json"
795 assert response.content_type == "application/json"
@@ -1,175 +1,177 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.model.db import User, UserSshKeys
22 from rhodecode.model.db import User, UserSshKeys
23
23
24 from rhodecode.tests import TestController, assert_session_flash
24 from rhodecode.tests import TestController, assert_session_flash
25 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
26
26
27 fixture = Fixture()
27 fixture = Fixture()
28
28
29
29
30 def route_path(name, params=None, **kwargs):
30 def route_path(name, params=None, **kwargs):
31 import urllib.request, urllib.parse, urllib.error
31 import urllib.request
32 import urllib.parse
33 import urllib.error
32 from rhodecode.apps._base import ADMIN_PREFIX
34 from rhodecode.apps._base import ADMIN_PREFIX
33
35
34 base_url = {
36 base_url = {
35 'edit_user_ssh_keys':
37 'edit_user_ssh_keys':
36 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys',
38 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys',
37 'edit_user_ssh_keys_generate_keypair':
39 'edit_user_ssh_keys_generate_keypair':
38 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate',
40 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate',
39 'edit_user_ssh_keys_add':
41 'edit_user_ssh_keys_add':
40 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new',
42 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new',
41 'edit_user_ssh_keys_delete':
43 'edit_user_ssh_keys_delete':
42 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete',
44 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete',
43
45
44 }[name].format(**kwargs)
46 }[name].format(**kwargs)
45
47
46 if params:
48 if params:
47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 return base_url
50 return base_url
49
51
50
52
51 class TestAdminUsersSshKeysView(TestController):
53 class TestAdminUsersSshKeysView(TestController):
52 INVALID_KEY = """\
54 INVALID_KEY = """\
53 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
55 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
54 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
56 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
55 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
57 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
56 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
58 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
57 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
59 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
58 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
60 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
59 your_email@example.com
61 your_email@example.com
60 """
62 """
61 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
63 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
62 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
64 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
63 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
65 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
64 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
66 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
65 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
67 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
66 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
68 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
67 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
69 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
68 'your_email@example.com'
70 'your_email@example.com'
69 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
71 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
70
72
71 def test_ssh_keys_default_user(self):
73 def test_ssh_keys_default_user(self):
72 self.log_user()
74 self.log_user()
73 user = User.get_default_user()
75 user = User.get_default_user()
74 self.app.get(
76 self.app.get(
75 route_path('edit_user_ssh_keys', user_id=user.user_id),
77 route_path('edit_user_ssh_keys', user_id=user.user_id),
76 status=302)
78 status=302)
77
79
78 def test_add_ssh_key_error(self, user_util):
80 def test_add_ssh_key_error(self, user_util):
79 self.log_user()
81 self.log_user()
80 user = user_util.create_user()
82 user = user_util.create_user()
81 user_id = user.user_id
83 user_id = user.user_id
82
84
83 key_data = self.INVALID_KEY
85 key_data = self.INVALID_KEY
84
86
85 desc = 'MY SSH KEY'
87 desc = 'MY SSH KEY'
86 response = self.app.post(
88 response = self.app.post(
87 route_path('edit_user_ssh_keys_add', user_id=user_id),
89 route_path('edit_user_ssh_keys_add', user_id=user_id),
88 {'description': desc, 'key_data': key_data,
90 {'description': desc, 'key_data': key_data,
89 'csrf_token': self.csrf_token})
91 'csrf_token': self.csrf_token})
90 assert_session_flash(response, 'An error occurred during ssh '
92 assert_session_flash(response, 'An error occurred during ssh '
91 'key saving: Unable to decode the key')
93 'key saving: Unable to decode the key')
92
94
93 def test_ssh_key_duplicate(self, user_util):
95 def test_ssh_key_duplicate(self, user_util):
94 self.log_user()
96 self.log_user()
95 user = user_util.create_user()
97 user = user_util.create_user()
96 user_id = user.user_id
98 user_id = user.user_id
97
99
98 key_data = self.VALID_KEY
100 key_data = self.VALID_KEY
99
101
100 desc = 'MY SSH KEY'
102 desc = 'MY SSH KEY'
101 response = self.app.post(
103 response = self.app.post(
102 route_path('edit_user_ssh_keys_add', user_id=user_id),
104 route_path('edit_user_ssh_keys_add', user_id=user_id),
103 {'description': desc, 'key_data': key_data,
105 {'description': desc, 'key_data': key_data,
104 'csrf_token': self.csrf_token})
106 'csrf_token': self.csrf_token})
105 assert_session_flash(response, 'Ssh Key successfully created')
107 assert_session_flash(response, 'Ssh Key successfully created')
106 response.follow() # flush session flash
108 response.follow() # flush session flash
107
109
108 # add the same key AGAIN
110 # add the same key AGAIN
109 desc = 'MY SSH KEY'
111 desc = 'MY SSH KEY'
110 response = self.app.post(
112 response = self.app.post(
111 route_path('edit_user_ssh_keys_add', user_id=user_id),
113 route_path('edit_user_ssh_keys_add', user_id=user_id),
112 {'description': desc, 'key_data': key_data,
114 {'description': desc, 'key_data': key_data,
113 'csrf_token': self.csrf_token})
115 'csrf_token': self.csrf_token})
114
116
115 err = 'Such key with fingerprint `{}` already exists, ' \
117 err = 'Such key with fingerprint `{}` already exists, ' \
116 'please use a different one'.format(self.FINGERPRINT)
118 'please use a different one'.format(self.FINGERPRINT)
117 assert_session_flash(response, 'An error occurred during ssh key '
119 assert_session_flash(response, 'An error occurred during ssh key '
118 'saving: {}'.format(err))
120 'saving: {}'.format(err))
119
121
120 def test_add_ssh_key(self, user_util):
122 def test_add_ssh_key(self, user_util):
121 self.log_user()
123 self.log_user()
122 user = user_util.create_user()
124 user = user_util.create_user()
123 user_id = user.user_id
125 user_id = user.user_id
124
126
125 key_data = self.VALID_KEY
127 key_data = self.VALID_KEY
126
128
127 desc = 'MY SSH KEY'
129 desc = 'MY SSH KEY'
128 response = self.app.post(
130 response = self.app.post(
129 route_path('edit_user_ssh_keys_add', user_id=user_id),
131 route_path('edit_user_ssh_keys_add', user_id=user_id),
130 {'description': desc, 'key_data': key_data,
132 {'description': desc, 'key_data': key_data,
131 'csrf_token': self.csrf_token})
133 'csrf_token': self.csrf_token})
132 assert_session_flash(response, 'Ssh Key successfully created')
134 assert_session_flash(response, 'Ssh Key successfully created')
133
135
134 response = response.follow()
136 response = response.follow()
135 response.mustcontain(desc)
137 response.mustcontain(desc)
136
138
137 def test_delete_ssh_key(self, user_util):
139 def test_delete_ssh_key(self, user_util):
138 self.log_user()
140 self.log_user()
139 user = user_util.create_user()
141 user = user_util.create_user()
140 user_id = user.user_id
142 user_id = user.user_id
141
143
142 key_data = self.VALID_KEY
144 key_data = self.VALID_KEY
143
145
144 desc = 'MY SSH KEY'
146 desc = 'MY SSH KEY'
145 response = self.app.post(
147 response = self.app.post(
146 route_path('edit_user_ssh_keys_add', user_id=user_id),
148 route_path('edit_user_ssh_keys_add', user_id=user_id),
147 {'description': desc, 'key_data': key_data,
149 {'description': desc, 'key_data': key_data,
148 'csrf_token': self.csrf_token})
150 'csrf_token': self.csrf_token})
149 assert_session_flash(response, 'Ssh Key successfully created')
151 assert_session_flash(response, 'Ssh Key successfully created')
150 response = response.follow() # flush the Session flash
152 response = response.follow() # flush the Session flash
151
153
152 # now delete our key
154 # now delete our key
153 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
155 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
154 assert 1 == len(keys)
156 assert 1 == len(keys)
155
157
156 response = self.app.post(
158 response = self.app.post(
157 route_path('edit_user_ssh_keys_delete', user_id=user_id),
159 route_path('edit_user_ssh_keys_delete', user_id=user_id),
158 {'del_ssh_key': keys[0].ssh_key_id,
160 {'del_ssh_key': keys[0].ssh_key_id,
159 'csrf_token': self.csrf_token})
161 'csrf_token': self.csrf_token})
160
162
161 assert_session_flash(response, 'Ssh key successfully deleted')
163 assert_session_flash(response, 'Ssh key successfully deleted')
162 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
164 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
163 assert 0 == len(keys)
165 assert 0 == len(keys)
164
166
165 def test_generate_keypair(self, user_util):
167 def test_generate_keypair(self, user_util):
166 self.log_user()
168 self.log_user()
167 user = user_util.create_user()
169 user = user_util.create_user()
168 user_id = user.user_id
170 user_id = user.user_id
169
171
170 response = self.app.get(
172 response = self.app.get(
171 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
173 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
172
174
173 response.mustcontain('Private key')
175 response.mustcontain('Private key')
174 response.mustcontain('Public key')
176 response.mustcontain('Public key')
175 response.mustcontain('-----BEGIN PRIVATE KEY-----')
177 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,260 +1,262 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 import os
19 import os
20 import pytest
20 import pytest
21
21
22 from rhodecode.lib.ext_json import json
22 from rhodecode.lib.ext_json import json
23 from rhodecode.model.auth_token import AuthTokenModel
23 from rhodecode.model.auth_token import AuthTokenModel
24 from rhodecode.model.db import Session, FileStore, Repository, User
24 from rhodecode.model.db import Session, FileStore, Repository, User
25 from rhodecode.tests import TestController
25 from rhodecode.tests import TestController
26 from rhodecode.apps.file_store import utils, config_keys
26 from rhodecode.apps.file_store import utils, config_keys
27
27
28
28
29 def route_path(name, params=None, **kwargs):
29 def route_path(name, params=None, **kwargs):
30 import urllib.request, urllib.parse, urllib.error
30 import urllib.request
31 import urllib.parse
32 import urllib.error
31
33
32 base_url = {
34 base_url = {
33 'upload_file': '/_file_store/upload',
35 'upload_file': '/_file_store/upload',
34 'download_file': '/_file_store/download/{fid}',
36 'download_file': '/_file_store/download/{fid}',
35 'download_file_by_token': '/_file_store/token-download/{_auth_token}/{fid}'
37 'download_file_by_token': '/_file_store/token-download/{_auth_token}/{fid}'
36
38
37 }[name].format(**kwargs)
39 }[name].format(**kwargs)
38
40
39 if params:
41 if params:
40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 return base_url
43 return base_url
42
44
43
45
44 class TestFileStoreViews(TestController):
46 class TestFileStoreViews(TestController):
45
47
46 @pytest.mark.parametrize("fid, content, exists", [
48 @pytest.mark.parametrize("fid, content, exists", [
47 ('abcde-0.jpg', "xxxxx", True),
49 ('abcde-0.jpg', "xxxxx", True),
48 ('abcde-0.exe', "1234567", True),
50 ('abcde-0.exe', "1234567", True),
49 ('abcde-0.jpg', "xxxxx", False),
51 ('abcde-0.jpg', "xxxxx", False),
50 ])
52 ])
51 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
53 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
52 user = self.log_user()
54 user = self.log_user()
53 user_id = user['user_id']
55 user_id = user['user_id']
54 repo_id = user_util.create_repo().repo_id
56 repo_id = user_util.create_repo().repo_id
55 store_path = self.app._pyramid_settings[config_keys.store_path]
57 store_path = self.app._pyramid_settings[config_keys.store_path]
56 store_uid = fid
58 store_uid = fid
57
59
58 if exists:
60 if exists:
59 status = 200
61 status = 200
60 store = utils.get_file_storage({config_keys.store_path: store_path})
62 store = utils.get_file_storage({config_keys.store_path: store_path})
61 filesystem_file = os.path.join(str(tmpdir), fid)
63 filesystem_file = os.path.join(str(tmpdir), fid)
62 with open(filesystem_file, 'wb') as f:
64 with open(filesystem_file, 'wt') as f:
63 f.write(content)
65 f.write(content)
64
66
65 with open(filesystem_file, 'rb') as f:
67 with open(filesystem_file, 'rb') as f:
66 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
68 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
67
69
68 entry = FileStore.create(
70 entry = FileStore.create(
69 file_uid=store_uid, filename=metadata["filename"],
71 file_uid=store_uid, filename=metadata["filename"],
70 file_hash=metadata["sha256"], file_size=metadata["size"],
72 file_hash=metadata["sha256"], file_size=metadata["size"],
71 file_display_name='file_display_name',
73 file_display_name='file_display_name',
72 file_description='repo artifact `{}`'.format(metadata["filename"]),
74 file_description='repo artifact `{}`'.format(metadata["filename"]),
73 check_acl=True, user_id=user_id,
75 check_acl=True, user_id=user_id,
74 scope_repo_id=repo_id
76 scope_repo_id=repo_id
75 )
77 )
76 Session().add(entry)
78 Session().add(entry)
77 Session().commit()
79 Session().commit()
78
80
79 else:
81 else:
80 status = 404
82 status = 404
81
83
82 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
84 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
83
85
84 if exists:
86 if exists:
85 assert response.text == content
87 assert response.text == content
86 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
88 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
87 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
89 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
88 assert os.path.exists(metadata_file)
90 assert os.path.exists(metadata_file)
89 with open(metadata_file, 'rb') as f:
91 with open(metadata_file, 'rb') as f:
90 json_data = json.loads(f.read())
92 json_data = json.loads(f.read())
91
93
92 assert json_data
94 assert json_data
93 assert 'size' in json_data
95 assert 'size' in json_data
94
96
95 def test_upload_files_without_content_to_store(self):
97 def test_upload_files_without_content_to_store(self):
96 self.log_user()
98 self.log_user()
97 response = self.app.post(
99 response = self.app.post(
98 route_path('upload_file'),
100 route_path('upload_file'),
99 params={'csrf_token': self.csrf_token},
101 params={'csrf_token': self.csrf_token},
100 status=200)
102 status=200)
101
103
102 assert response.json == {
104 assert response.json == {
103 u'error': u'store_file data field is missing',
105 u'error': u'store_file data field is missing',
104 u'access_path': None,
106 u'access_path': None,
105 u'store_fid': None}
107 u'store_fid': None}
106
108
107 def test_upload_files_bogus_content_to_store(self):
109 def test_upload_files_bogus_content_to_store(self):
108 self.log_user()
110 self.log_user()
109 response = self.app.post(
111 response = self.app.post(
110 route_path('upload_file'),
112 route_path('upload_file'),
111 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
113 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
112 status=200)
114 status=200)
113
115
114 assert response.json == {
116 assert response.json == {
115 u'error': u'filename cannot be read from the data field',
117 u'error': u'filename cannot be read from the data field',
116 u'access_path': None,
118 u'access_path': None,
117 u'store_fid': None}
119 u'store_fid': None}
118
120
119 def test_upload_content_to_store(self):
121 def test_upload_content_to_store(self):
120 self.log_user()
122 self.log_user()
121 response = self.app.post(
123 response = self.app.post(
122 route_path('upload_file'),
124 route_path('upload_file'),
123 upload_files=[('store_file', 'myfile.txt', 'SOME CONTENT')],
125 upload_files=[('store_file', b'myfile.txt', b'SOME CONTENT')],
124 params={'csrf_token': self.csrf_token},
126 params={'csrf_token': self.csrf_token},
125 status=200)
127 status=200)
126
128
127 assert response.json['store_fid']
129 assert response.json['store_fid']
128
130
129 @pytest.fixture()
131 @pytest.fixture()
130 def create_artifact_factory(self, tmpdir):
132 def create_artifact_factory(self, tmpdir):
131 def factory(user_id, content):
133 def factory(user_id, content):
132 store_path = self.app._pyramid_settings[config_keys.store_path]
134 store_path = self.app._pyramid_settings[config_keys.store_path]
133 store = utils.get_file_storage({config_keys.store_path: store_path})
135 store = utils.get_file_storage({config_keys.store_path: store_path})
134 fid = 'example.txt'
136 fid = 'example.txt'
135
137
136 filesystem_file = os.path.join(str(tmpdir), fid)
138 filesystem_file = os.path.join(str(tmpdir), fid)
137 with open(filesystem_file, 'wb') as f:
139 with open(filesystem_file, 'wt') as f:
138 f.write(content)
140 f.write(content)
139
141
140 with open(filesystem_file, 'rb') as f:
142 with open(filesystem_file, 'rb') as f:
141 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
143 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
142
144
143 entry = FileStore.create(
145 entry = FileStore.create(
144 file_uid=store_uid, filename=metadata["filename"],
146 file_uid=store_uid, filename=metadata["filename"],
145 file_hash=metadata["sha256"], file_size=metadata["size"],
147 file_hash=metadata["sha256"], file_size=metadata["size"],
146 file_display_name='file_display_name',
148 file_display_name='file_display_name',
147 file_description='repo artifact `{}`'.format(metadata["filename"]),
149 file_description='repo artifact `{}`'.format(metadata["filename"]),
148 check_acl=True, user_id=user_id,
150 check_acl=True, user_id=user_id,
149 )
151 )
150 Session().add(entry)
152 Session().add(entry)
151 Session().commit()
153 Session().commit()
152 return entry
154 return entry
153 return factory
155 return factory
154
156
155 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
157 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
156 user = self.log_user()
158 user = self.log_user()
157 user_id = user['user_id']
159 user_id = user['user_id']
158 content = 'HELLO MY NAME IS ARTIFACT !'
160 content = 'HELLO MY NAME IS ARTIFACT !'
159
161
160 artifact = create_artifact_factory(user_id, content)
162 artifact = create_artifact_factory(user_id, content)
161 file_uid = artifact.file_uid
163 file_uid = artifact.file_uid
162 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
164 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
163 assert response.text == content
165 assert response.text == content
164
166
165 # log-in to new user and test download again
167 # log-in to new user and test download again
166 user = user_util.create_user(password='qweqwe')
168 user = user_util.create_user(password='qweqwe')
167 self.log_user(user.username, 'qweqwe')
169 self.log_user(user.username, 'qweqwe')
168 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
170 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
169 assert response.text == content
171 assert response.text == content
170
172
171 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
173 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
172 user = self.log_user()
174 user = self.log_user()
173 user_id = user['user_id']
175 user_id = user['user_id']
174 content = 'HELLO MY NAME IS ARTIFACT !'
176 content = 'HELLO MY NAME IS ARTIFACT !'
175
177
176 artifact = create_artifact_factory(user_id, content)
178 artifact = create_artifact_factory(user_id, content)
177 # bind to repo
179 # bind to repo
178 repo = user_util.create_repo()
180 repo = user_util.create_repo()
179 repo_id = repo.repo_id
181 repo_id = repo.repo_id
180 artifact.scope_repo_id = repo_id
182 artifact.scope_repo_id = repo_id
181 Session().add(artifact)
183 Session().add(artifact)
182 Session().commit()
184 Session().commit()
183
185
184 file_uid = artifact.file_uid
186 file_uid = artifact.file_uid
185 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
187 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
186 assert response.text == content
188 assert response.text == content
187
189
188 # log-in to new user and test download again
190 # log-in to new user and test download again
189 user = user_util.create_user(password='qweqwe')
191 user = user_util.create_user(password='qweqwe')
190 self.log_user(user.username, 'qweqwe')
192 self.log_user(user.username, 'qweqwe')
191 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
193 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
192 assert response.text == content
194 assert response.text == content
193
195
194 # forbid user the rights to repo
196 # forbid user the rights to repo
195 repo = Repository.get(repo_id)
197 repo = Repository.get(repo_id)
196 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
198 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
197 self.app.get(route_path('download_file', fid=file_uid), status=404)
199 self.app.get(route_path('download_file', fid=file_uid), status=404)
198
200
199 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
201 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
200 user = self.log_user()
202 user = self.log_user()
201 user_id = user['user_id']
203 user_id = user['user_id']
202 content = 'HELLO MY NAME IS ARTIFACT !'
204 content = 'HELLO MY NAME IS ARTIFACT !'
203
205
204 artifact = create_artifact_factory(user_id, content)
206 artifact = create_artifact_factory(user_id, content)
205 # bind to user
207 # bind to user
206 user = user_util.create_user(password='qweqwe')
208 user = user_util.create_user(password='qweqwe')
207
209
208 artifact.scope_user_id = user.user_id
210 artifact.scope_user_id = user.user_id
209 Session().add(artifact)
211 Session().add(artifact)
210 Session().commit()
212 Session().commit()
211
213
212 # artifact creator doesn't have access since it's bind to another user
214 # artifact creator doesn't have access since it's bind to another user
213 file_uid = artifact.file_uid
215 file_uid = artifact.file_uid
214 self.app.get(route_path('download_file', fid=file_uid), status=404)
216 self.app.get(route_path('download_file', fid=file_uid), status=404)
215
217
216 # log-in to new user and test download again, should be ok since we're bind to this artifact
218 # log-in to new user and test download again, should be ok since we're bind to this artifact
217 self.log_user(user.username, 'qweqwe')
219 self.log_user(user.username, 'qweqwe')
218 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
220 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
219 assert response.text == content
221 assert response.text == content
220
222
221 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
223 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
222 user_id = User.get_first_super_admin().user_id
224 user_id = User.get_first_super_admin().user_id
223 content = 'HELLO MY NAME IS ARTIFACT !'
225 content = 'HELLO MY NAME IS ARTIFACT !'
224
226
225 artifact = create_artifact_factory(user_id, content)
227 artifact = create_artifact_factory(user_id, content)
226 # bind to repo
228 # bind to repo
227 repo = user_util.create_repo()
229 repo = user_util.create_repo()
228 repo_id = repo.repo_id
230 repo_id = repo.repo_id
229 artifact.scope_repo_id = repo_id
231 artifact.scope_repo_id = repo_id
230 Session().add(artifact)
232 Session().add(artifact)
231 Session().commit()
233 Session().commit()
232
234
233 file_uid = artifact.file_uid
235 file_uid = artifact.file_uid
234 self.app.get(route_path('download_file_by_token',
236 self.app.get(route_path('download_file_by_token',
235 _auth_token='bogus', fid=file_uid), status=302)
237 _auth_token='bogus', fid=file_uid), status=302)
236
238
237 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
239 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
238 user = User.get_first_super_admin()
240 user = User.get_first_super_admin()
239 AuthTokenModel().create(user, 'test artifact token',
241 AuthTokenModel().create(user, 'test artifact token',
240 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
242 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
241
243
242 user = User.get_first_super_admin()
244 user = User.get_first_super_admin()
243 artifact_token = user.artifact_token
245 artifact_token = user.artifact_token
244
246
245 user_id = User.get_first_super_admin().user_id
247 user_id = User.get_first_super_admin().user_id
246 content = 'HELLO MY NAME IS ARTIFACT !'
248 content = 'HELLO MY NAME IS ARTIFACT !'
247
249
248 artifact = create_artifact_factory(user_id, content)
250 artifact = create_artifact_factory(user_id, content)
249 # bind to repo
251 # bind to repo
250 repo = user_util.create_repo()
252 repo = user_util.create_repo()
251 repo_id = repo.repo_id
253 repo_id = repo.repo_id
252 artifact.scope_repo_id = repo_id
254 artifact.scope_repo_id = repo_id
253 Session().add(artifact)
255 Session().add(artifact)
254 Session().commit()
256 Session().commit()
255
257
256 file_uid = artifact.file_uid
258 file_uid = artifact.file_uid
257 response = self.app.get(
259 response = self.app.get(
258 route_path('download_file_by_token',
260 route_path('download_file_by_token',
259 _auth_token=artifact_token, fid=file_uid), status=200)
261 _auth_token=artifact_token, fid=file_uid), status=200)
260 assert response.text == content
262 assert response.text == content
@@ -1,390 +1,390 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import mock
20 import mock
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.model.db import User, Gist
24 from rhodecode.model.db import User, Gist
25 from rhodecode.model.gist import GistModel
25 from rhodecode.model.gist import GistModel
26 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
27 from rhodecode.tests import (
27 from rhodecode.tests import (
28 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
28 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
29 TestController, assert_session_flash)
29 TestController, assert_session_flash)
30
30
31
31
32 def route_path(name, params=None, **kwargs):
32 def route_path(name, params=None, **kwargs):
33 import urllib.request, urllib.parse, urllib.error
33 import urllib.parse
34 import urllib.error
34 from rhodecode.apps._base import ADMIN_PREFIX
35 from rhodecode.apps._base import ADMIN_PREFIX
35
36
36 base_url = {
37 base_url = {
37 'gists_show': ADMIN_PREFIX + '/gists',
38 'gists_show': ADMIN_PREFIX + '/gists',
38 'gists_new': ADMIN_PREFIX + '/gists/new',
39 'gists_new': ADMIN_PREFIX + '/gists/new',
39 'gists_create': ADMIN_PREFIX + '/gists/create',
40 'gists_create': ADMIN_PREFIX + '/gists/create',
40 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
41 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
41 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
42 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
42 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
43 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
43 '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',
44 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
45 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
45 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}',
46 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}',
46 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}',
47 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}',
47 '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}',
48
49
49 }[name].format(**kwargs)
50 }[name].format(**kwargs)
50
51
51 if params:
52 if params:
52 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
53 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
53 return base_url
54 return base_url
54
55
55
56
56 class GistUtility(object):
57 class GistUtility(object):
57
58
58 def __init__(self):
59 def __init__(self):
59 self._gist_ids = []
60 self._gist_ids = []
60
61
61 def __call__(
62 def __call__(
62 self, f_name, content='some gist', lifetime=-1,
63 self, f_name: bytes, content: bytes = b'some gist', lifetime=-1,
63 description='gist-desc', gist_type='public',
64 description='gist-desc', gist_type='public',
64 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
65 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
65 gist_mapping = {
66 gist_mapping = {
66 f_name: {'content': content}
67 f_name: {'content': content}
67 }
68 }
68 user = User.get_by_username(owner)
69 user = User.get_by_username(owner)
69 gist = GistModel().create(
70 gist = GistModel().create(
70 description, owner=user, gist_mapping=gist_mapping,
71 description, owner=user, gist_mapping=gist_mapping,
71 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
72 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
72 Session().commit()
73 Session().commit()
73 self._gist_ids.append(gist.gist_id)
74 self._gist_ids.append(gist.gist_id)
74 return gist
75 return gist
75
76
76 def cleanup(self):
77 def cleanup(self):
77 for gist_id in self._gist_ids:
78 for gist_id in self._gist_ids:
78 gist = Gist.get(gist_id)
79 gist = Gist.get(gist_id)
79 if gist:
80 if gist:
80 Session().delete(gist)
81 Session().delete(gist)
81
82
82 Session().commit()
83 Session().commit()
83
84
84
85
85 @pytest.fixture()
86 @pytest.fixture()
86 def create_gist(request):
87 def create_gist(request):
87 gist_utility = GistUtility()
88 gist_utility = GistUtility()
88 request.addfinalizer(gist_utility.cleanup)
89 request.addfinalizer(gist_utility.cleanup)
89 return gist_utility
90 return gist_utility
90
91
91
92
92 class TestGistsController(TestController):
93 class TestGistsController(TestController):
93
94
94 def test_index_empty(self, create_gist):
95 def test_index_empty(self, create_gist):
95 self.log_user()
96 self.log_user()
96 response = self.app.get(route_path('gists_show'))
97 response = self.app.get(route_path('gists_show'))
97 response.mustcontain('data: [],')
98 response.mustcontain('var gist_data = [];')
98
99
99 def test_index(self, create_gist):
100 def test_index(self, create_gist):
100 self.log_user()
101 self.log_user()
101 g1 = create_gist('gist1')
102 g1 = create_gist(b'gist1')
102 g2 = create_gist('gist2', lifetime=1400)
103 g2 = create_gist(b'gist2', lifetime=1400)
103 g3 = create_gist('gist3', description='gist3-desc')
104 g3 = create_gist(b'gist3', description='gist3-desc')
104 g4 = create_gist('gist4', gist_type='private').gist_access_id
105 g4 = create_gist(b'gist4', gist_type='private').gist_access_id
105 response = self.app.get(route_path('gists_show'))
106 response = self.app.get(route_path('gists_show'))
106
107
107 response.mustcontain(g1.gist_access_id)
108 response.mustcontain(g1.gist_access_id)
108 response.mustcontain(g2.gist_access_id)
109 response.mustcontain(g2.gist_access_id)
109 response.mustcontain(g3.gist_access_id)
110 response.mustcontain(g3.gist_access_id)
110 response.mustcontain('gist3-desc')
111 response.mustcontain('gist3-desc')
111 response.mustcontain(no=[g4])
112 response.mustcontain(no=[g4])
112
113
113 # Expiration information should be visible
114 # Expiration information should be visible
114 expires_tag = '%s' % h.age_component(
115 expires_tag = str(h.age_component(h.time_to_utcdatetime(g2.gist_expires)))
115 h.time_to_utcdatetime(g2.gist_expires))
116 response.mustcontain(expires_tag.replace('"', '\\"'))
116 response.mustcontain(expires_tag.replace('"', '\\"'))
117
117
118 def test_index_private_gists(self, create_gist):
118 def test_index_private_gists(self, create_gist):
119 self.log_user()
119 self.log_user()
120 gist = create_gist('gist5', gist_type='private')
120 gist = create_gist(b'gist5', gist_type='private')
121 response = self.app.get(route_path('gists_show', params=dict(private=1)))
121 response = self.app.get(route_path('gists_show', params=dict(private=1)))
122
122
123 # and privates
123 # and privates
124 response.mustcontain(gist.gist_access_id)
124 response.mustcontain(gist.gist_access_id)
125
125
126 def test_index_show_all(self, create_gist):
126 def test_index_show_all(self, create_gist):
127 self.log_user()
127 self.log_user()
128 create_gist('gist1')
128 create_gist(b'gist1')
129 create_gist('gist2', lifetime=1400)
129 create_gist(b'gist2', lifetime=1400)
130 create_gist('gist3', description='gist3-desc')
130 create_gist(b'gist3', description='gist3-desc')
131 create_gist('gist4', gist_type='private')
131 create_gist(b'gist4', gist_type='private')
132
132
133 response = self.app.get(route_path('gists_show', params=dict(all=1)))
133 response = self.app.get(route_path('gists_show', params=dict(all=1)))
134
134
135 assert len(GistModel.get_all()) == 4
135 assert len(GistModel.get_all()) == 4
136 # and privates
136 # and privates
137 for gist in GistModel.get_all():
137 for gist in GistModel.get_all():
138 response.mustcontain(gist.gist_access_id)
138 response.mustcontain(gist.gist_access_id)
139
139
140 def test_index_show_all_hidden_from_regular(self, create_gist):
140 def test_index_show_all_hidden_from_regular(self, create_gist):
141 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
141 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
142 create_gist('gist2', gist_type='private')
142 create_gist(b'gist2', gist_type='private')
143 create_gist('gist3', gist_type='private')
143 create_gist(b'gist3', gist_type='private')
144 create_gist('gist4', gist_type='private')
144 create_gist(b'gist4', gist_type='private')
145
145
146 response = self.app.get(route_path('gists_show', params=dict(all=1)))
146 response = self.app.get(route_path('gists_show', params=dict(all=1)))
147
147
148 assert len(GistModel.get_all()) == 3
148 assert len(GistModel.get_all()) == 3
149 # since we don't have access to private in this view, we
149 # since we don't have access to private in this view, we
150 # should see nothing
150 # should see nothing
151 for gist in GistModel.get_all():
151 for gist in GistModel.get_all():
152 response.mustcontain(no=[gist.gist_access_id])
152 response.mustcontain(no=[gist.gist_access_id])
153
153
154 def test_create(self):
154 def test_create(self):
155 self.log_user()
155 self.log_user()
156 response = self.app.post(
156 response = self.app.post(
157 route_path('gists_create'),
157 route_path('gists_create'),
158 params={'lifetime': -1,
158 params={'lifetime': -1,
159 'content': 'gist test',
159 'content': 'gist test',
160 'filename': 'foo',
160 'filename': 'foo',
161 'gist_type': 'public',
161 'gist_type': 'public',
162 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
162 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
163 'csrf_token': self.csrf_token},
163 'csrf_token': self.csrf_token},
164 status=302)
164 status=302)
165 response = response.follow()
165 response = response.follow()
166 response.mustcontain('added file: foo')
166 response.mustcontain('added file: foo')
167 response.mustcontain('gist test')
167 response.mustcontain('gist test')
168
168
169 def test_create_with_path_with_dirs(self):
169 def test_create_with_path_with_dirs(self):
170 self.log_user()
170 self.log_user()
171 response = self.app.post(
171 response = self.app.post(
172 route_path('gists_create'),
172 route_path('gists_create'),
173 params={'lifetime': -1,
173 params={'lifetime': -1,
174 'content': 'gist test',
174 'content': 'gist test',
175 'filename': '/home/foo',
175 'filename': '/home/foo',
176 'gist_type': 'public',
176 'gist_type': 'public',
177 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
177 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
178 'csrf_token': self.csrf_token},
178 'csrf_token': self.csrf_token},
179 status=200)
179 status=200)
180 response.mustcontain('Filename /home/foo cannot be inside a directory')
180 response.mustcontain('Filename /home/foo cannot be inside a directory')
181
181
182 def test_access_expired_gist(self, create_gist):
182 def test_access_expired_gist(self, create_gist):
183 self.log_user()
183 self.log_user()
184 gist = create_gist('never-see-me')
184 gist = create_gist(b'never-see-me')
185 gist.gist_expires = 0 # 1970
185 gist.gist_expires = 0 # 1970
186 Session().add(gist)
186 Session().add(gist)
187 Session().commit()
187 Session().commit()
188
188
189 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
189 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
190 status=404)
190 status=404)
191
191
192 def test_create_private(self):
192 def test_create_private(self):
193 self.log_user()
193 self.log_user()
194 response = self.app.post(
194 response = self.app.post(
195 route_path('gists_create'),
195 route_path('gists_create'),
196 params={'lifetime': -1,
196 params={'lifetime': -1,
197 'content': 'private gist test',
197 'content': 'private gist test',
198 'filename': 'private-foo',
198 'filename': 'private-foo',
199 'gist_type': 'private',
199 'gist_type': 'private',
200 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
200 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
201 'csrf_token': self.csrf_token},
201 'csrf_token': self.csrf_token},
202 status=302)
202 status=302)
203 response = response.follow()
203 response = response.follow()
204 response.mustcontain('added file: private-foo<')
204 response.mustcontain('added file: private-foo<')
205 response.mustcontain('private gist test')
205 response.mustcontain('private gist test')
206 response.mustcontain('Private Gist')
206 response.mustcontain('Private Gist')
207 # Make sure private gists are not indexed by robots
207 # Make sure private gists are not indexed by robots
208 response.mustcontain(
208 response.mustcontain(
209 '<meta name="robots" content="noindex, nofollow">')
209 '<meta name="robots" content="noindex, nofollow">')
210
210
211 def test_create_private_acl_private(self):
211 def test_create_private_acl_private(self):
212 self.log_user()
212 self.log_user()
213 response = self.app.post(
213 response = self.app.post(
214 route_path('gists_create'),
214 route_path('gists_create'),
215 params={'lifetime': -1,
215 params={'lifetime': -1,
216 'content': 'private gist test',
216 'content': 'private gist test',
217 'filename': 'private-foo',
217 'filename': 'private-foo',
218 'gist_type': 'private',
218 'gist_type': 'private',
219 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
219 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
220 'csrf_token': self.csrf_token},
220 'csrf_token': self.csrf_token},
221 status=302)
221 status=302)
222 response = response.follow()
222 response = response.follow()
223 response.mustcontain('added file: private-foo<')
223 response.mustcontain('added file: private-foo<')
224 response.mustcontain('private gist test')
224 response.mustcontain('private gist test')
225 response.mustcontain('Private Gist')
225 response.mustcontain('Private Gist')
226 # Make sure private gists are not indexed by robots
226 # Make sure private gists are not indexed by robots
227 response.mustcontain(
227 response.mustcontain(
228 '<meta name="robots" content="noindex, nofollow">')
228 '<meta name="robots" content="noindex, nofollow">')
229
229
230 def test_create_with_description(self):
230 def test_create_with_description(self):
231 self.log_user()
231 self.log_user()
232 response = self.app.post(
232 response = self.app.post(
233 route_path('gists_create'),
233 route_path('gists_create'),
234 params={'lifetime': -1,
234 params={'lifetime': -1,
235 'content': 'gist test',
235 'content': 'gist test',
236 'filename': 'foo-desc',
236 'filename': 'foo-desc',
237 'description': 'gist-desc',
237 'description': 'gist-desc',
238 'gist_type': 'public',
238 'gist_type': 'public',
239 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
239 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
240 'csrf_token': self.csrf_token},
240 'csrf_token': self.csrf_token},
241 status=302)
241 status=302)
242 response = response.follow()
242 response = response.follow()
243 response.mustcontain('added file: foo-desc')
243 response.mustcontain('added file: foo-desc')
244 response.mustcontain('gist test')
244 response.mustcontain('gist test')
245 response.mustcontain('gist-desc')
245 response.mustcontain('gist-desc')
246
246
247 def test_create_public_with_anonymous_access(self):
247 def test_create_public_with_anonymous_access(self):
248 self.log_user()
248 self.log_user()
249 params = {
249 params = {
250 'lifetime': -1,
250 'lifetime': -1,
251 'content': 'gist test',
251 'content': 'gist test',
252 'filename': 'foo-desc',
252 'filename': 'foo-desc',
253 'description': 'gist-desc',
253 'description': 'gist-desc',
254 'gist_type': 'public',
254 'gist_type': 'public',
255 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
255 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
256 'csrf_token': self.csrf_token
256 'csrf_token': self.csrf_token
257 }
257 }
258 response = self.app.post(
258 response = self.app.post(
259 route_path('gists_create'), params=params, status=302)
259 route_path('gists_create'), params=params, status=302)
260 self.logout_user()
260 self.logout_user()
261 response = response.follow()
261 response = response.follow()
262 response.mustcontain('added file: foo-desc')
262 response.mustcontain('added file: foo-desc')
263 response.mustcontain('gist test')
263 response.mustcontain('gist test')
264 response.mustcontain('gist-desc')
264 response.mustcontain('gist-desc')
265
265
266 def test_new(self):
266 def test_new(self):
267 self.log_user()
267 self.log_user()
268 self.app.get(route_path('gists_new'))
268 self.app.get(route_path('gists_new'))
269
269
270 def test_delete(self, create_gist):
270 def test_delete(self, create_gist):
271 self.log_user()
271 self.log_user()
272 gist = create_gist('delete-me')
272 gist = create_gist(b'delete-me')
273 response = self.app.post(
273 response = self.app.post(
274 route_path('gist_delete', gist_id=gist.gist_id),
274 route_path('gist_delete', gist_id=gist.gist_id),
275 params={'csrf_token': self.csrf_token})
275 params={'csrf_token': self.csrf_token})
276 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
276 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
277
277
278 def test_delete_normal_user_his_gist(self, create_gist):
278 def test_delete_normal_user_his_gist(self, create_gist):
279 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
279 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
280 gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
280 gist = create_gist(b'delete-me', owner=TEST_USER_REGULAR_LOGIN)
281
281
282 response = self.app.post(
282 response = self.app.post(
283 route_path('gist_delete', gist_id=gist.gist_id),
283 route_path('gist_delete', gist_id=gist.gist_id),
284 params={'csrf_token': self.csrf_token})
284 params={'csrf_token': self.csrf_token})
285 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
285 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
286
286
287 def test_delete_normal_user_not_his_own_gist(self, create_gist):
287 def test_delete_normal_user_not_his_own_gist(self, create_gist):
288 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
288 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
289 gist = create_gist('delete-me-2')
289 gist = create_gist(b'delete-me-2')
290
290
291 self.app.post(
291 self.app.post(
292 route_path('gist_delete', gist_id=gist.gist_id),
292 route_path('gist_delete', gist_id=gist.gist_id),
293 params={'csrf_token': self.csrf_token}, status=404)
293 params={'csrf_token': self.csrf_token}, status=404)
294
294
295 def test_show(self, create_gist):
295 def test_show(self, create_gist):
296 gist = create_gist('gist-show-me')
296 gist = create_gist(b'gist-show-me')
297 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
297 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
298
298
299 response.mustcontain('added file: gist-show-me<')
299 response.mustcontain('added file: gist-show-me<')
300
300
301 assert_response = response.assert_response()
301 assert_response = response.assert_response()
302 assert_response.element_equals_to(
302 assert_response.element_equals_to(
303 'div.rc-user span.user',
303 'div.rc-user span.user',
304 '<a href="/_profiles/test_admin">test_admin</a>')
304 '<a href="/_profiles/test_admin">test_admin</a>')
305
305
306 response.mustcontain('gist-desc')
306 response.mustcontain('gist-desc')
307
307
308 def test_show_without_hg(self, create_gist):
308 def test_show_without_hg(self, create_gist):
309 with mock.patch(
309 with mock.patch(
310 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
310 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
311 gist = create_gist('gist-show-me-again')
311 gist = create_gist(b'gist-show-me-again')
312 self.app.get(
312 self.app.get(
313 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
313 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
314
314
315 def test_show_acl_private(self, create_gist):
315 def test_show_acl_private(self, create_gist):
316 gist = create_gist('gist-show-me-only-when-im-logged-in',
316 gist = create_gist(b'gist-show-me-only-when-im-logged-in',
317 acl_level=Gist.ACL_LEVEL_PRIVATE)
317 acl_level=Gist.ACL_LEVEL_PRIVATE)
318 self.app.get(
318 self.app.get(
319 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
319 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
320
320
321 # now we log-in we should see thi gist
321 # now we log-in we should see thi gist
322 self.log_user()
322 self.log_user()
323 response = self.app.get(
323 response = self.app.get(
324 route_path('gist_show', gist_id=gist.gist_access_id))
324 route_path('gist_show', gist_id=gist.gist_access_id))
325 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
325 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
326
326
327 assert_response = response.assert_response()
327 assert_response = response.assert_response()
328 assert_response.element_equals_to(
328 assert_response.element_equals_to(
329 'div.rc-user span.user',
329 'div.rc-user span.user',
330 '<a href="/_profiles/test_admin">test_admin</a>')
330 '<a href="/_profiles/test_admin">test_admin</a>')
331 response.mustcontain('gist-desc')
331 response.mustcontain('gist-desc')
332
332
333 def test_show_as_raw(self, create_gist):
333 def test_show_as_raw(self, create_gist):
334 gist = create_gist('gist-show-me', content='GIST CONTENT')
334 gist = create_gist(b'gist-show-me', content=b'GIST CONTENT')
335 response = self.app.get(
335 response = self.app.get(
336 route_path('gist_show_formatted',
336 route_path('gist_show_formatted',
337 gist_id=gist.gist_access_id, revision='tip',
337 gist_id=gist.gist_access_id, revision='tip',
338 format='raw'))
338 format='raw'))
339 assert response.text == 'GIST CONTENT'
339 assert response.text == 'GIST CONTENT'
340
340
341 def test_show_as_raw_individual_file(self, create_gist):
341 def test_show_as_raw_individual_file(self, create_gist):
342 gist = create_gist('gist-show-me-raw', content='GIST BODY')
342 gist = create_gist(b'gist-show-me-raw', content=b'GIST BODY')
343 response = self.app.get(
343 response = self.app.get(
344 route_path('gist_show_formatted_path',
344 route_path('gist_show_formatted_path',
345 gist_id=gist.gist_access_id, format='raw',
345 gist_id=gist.gist_access_id, format='raw',
346 revision='tip', f_path='gist-show-me-raw'))
346 revision='tip', f_path='gist-show-me-raw'))
347 assert response.text == 'GIST BODY'
347 assert response.text == 'GIST BODY'
348
348
349 def test_edit_page(self, create_gist):
349 def test_edit_page(self, create_gist):
350 self.log_user()
350 self.log_user()
351 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
351 gist = create_gist(b'gist-for-edit', content=b'GIST EDIT BODY')
352 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
352 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
353 response.mustcontain('GIST EDIT BODY')
353 response.mustcontain('GIST EDIT BODY')
354
354
355 def test_edit_page_non_logged_user(self, create_gist):
355 def test_edit_page_non_logged_user(self, create_gist):
356 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
356 gist = create_gist(b'gist-for-edit', content=b'GIST EDIT BODY')
357 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
357 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
358 status=302)
358 status=302)
359
359
360 def test_edit_normal_user_his_gist(self, create_gist):
360 def test_edit_normal_user_his_gist(self, create_gist):
361 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
361 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
362 gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
362 gist = create_gist(b'gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
363 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
363 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
364 status=200))
364 status=200))
365
365
366 def test_edit_normal_user_not_his_own_gist(self, create_gist):
366 def test_edit_normal_user_not_his_own_gist(self, create_gist):
367 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
367 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
368 gist = create_gist('delete-me')
368 gist = create_gist(b'delete-me')
369 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
369 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
370 status=404)
370 status=404)
371
371
372 def test_user_first_name_is_escaped(self, user_util, create_gist):
372 def test_user_first_name_is_escaped(self, user_util, create_gist):
373 xss_atack_string = '"><script>alert(\'First Name\')</script>'
373 xss_atack_string = '"><script>alert(\'First Name\')</script>'
374 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
374 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
375 password = 'test'
375 password = 'test'
376 user = user_util.create_user(
376 user = user_util.create_user(
377 firstname=xss_atack_string, password=password)
377 firstname=xss_atack_string, password=password)
378 create_gist('gist', gist_type='public', owner=user.username)
378 create_gist(b'gist', gist_type='public', owner=user.username)
379 response = self.app.get(route_path('gists_show'))
379 response = self.app.get(route_path('gists_show'))
380 response.mustcontain(xss_escaped_string)
380 response.mustcontain(xss_escaped_string)
381
381
382 def test_user_last_name_is_escaped(self, user_util, create_gist):
382 def test_user_last_name_is_escaped(self, user_util, create_gist):
383 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
383 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
384 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
384 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
385 password = 'test'
385 password = 'test'
386 user = user_util.create_user(
386 user = user_util.create_user(
387 lastname=xss_atack_string, password=password)
387 lastname=xss_atack_string, password=password)
388 create_gist('gist', gist_type='public', owner=user.username)
388 create_gist(b'gist', gist_type='public', owner=user.username)
389 response = self.app.get(route_path('gists_show'))
389 response = self.app.get(route_path('gists_show'))
390 response.mustcontain(xss_escaped_string)
390 response.mustcontain(xss_escaped_string)
@@ -1,95 +1,97 b''
1
1
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
22
23 from . import assert_and_get_repo_list_content
21 from . import assert_and_get_repo_list_content
24 from rhodecode.tests import TestController
22 from rhodecode.tests import TestController
25 from rhodecode.tests.fixture import Fixture
23 from rhodecode.tests.fixture import Fixture
26 from rhodecode.model.db import Repository
24 from rhodecode.model.db import Repository
25 from rhodecode.lib.ext_json import json
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.request, urllib.parse, urllib.error
32 import urllib.request
33 import urllib.parse
34 import urllib.error
33
35
34 base_url = {
36 base_url = {
35 'repo_list_data': '/_repos',
37 'repo_list_data': '/_repos',
36 }[name].format(**kwargs)
38 }[name].format(**kwargs)
37
39
38 if params:
40 if params:
39 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 return base_url
42 return base_url
41
43
42
44
43 class TestRepoListData(TestController):
45 class TestRepoListData(TestController):
44
46
45 def test_returns_list_of_repos_and_groups(self, xhr_header):
47 def test_returns_list_of_repos_and_groups(self, xhr_header):
46 self.log_user()
48 self.log_user()
47
49
48 response = self.app.get(
50 response = self.app.get(
49 route_path('repo_list_data'),
51 route_path('repo_list_data'),
50 extra_environ=xhr_header, status=200)
52 extra_environ=xhr_header, status=200)
51 result = json.loads(response.body)['results']
53 result = json.loads(response.body)['results']
52
54
53 repos = assert_and_get_repo_list_content(result)
55 repos = assert_and_get_repo_list_content(result)
54
56
55 assert len(repos) == len(Repository.get_all())
57 assert len(repos) == len(Repository.get_all())
56
58
57 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
59 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
58 self.log_user()
60 self.log_user()
59
61
60 response = self.app.get(
62 response = self.app.get(
61 route_path('repo_list_data'),
63 route_path('repo_list_data'),
62 params={'query': 'vcs_test_git'},
64 params={'query': 'vcs_test_git'},
63 extra_environ=xhr_header, status=200)
65 extra_environ=xhr_header, status=200)
64 result = json.loads(response.body)['results']
66 result = json.loads(response.body)['results']
65
67
66 repos = assert_and_get_repo_list_content(result)
68 repos = assert_and_get_repo_list_content(result)
67
69
68 assert len(repos) == len(Repository.query().filter(
70 assert len(repos) == len(Repository.query().filter(
69 Repository.repo_name.ilike('%vcs_test_git%')).all())
71 Repository.repo_name.ilike('%vcs_test_git%')).all())
70
72
71 def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header):
73 def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header):
72 self.log_user()
74 self.log_user()
73
75
74 response = self.app.get(
76 response = self.app.get(
75 route_path('repo_list_data'),
77 route_path('repo_list_data'),
76 params={'query': 'vcs_test_git', 'repo_type': 'git'},
78 params={'query': 'vcs_test_git', 'repo_type': 'git'},
77 extra_environ=xhr_header, status=200)
79 extra_environ=xhr_header, status=200)
78 result = json.loads(response.body)['results']
80 result = json.loads(response.body)['results']
79
81
80 repos = assert_and_get_repo_list_content(result)
82 repos = assert_and_get_repo_list_content(result)
81
83
82 assert len(repos) == len(Repository.query().filter(
84 assert len(repos) == len(Repository.query().filter(
83 Repository.repo_name.ilike('%vcs_test_git%')).all())
85 Repository.repo_name.ilike('%vcs_test_git%')).all())
84
86
85 def test_returns_list_of_repos_non_ascii_query(self, xhr_header):
87 def test_returns_list_of_repos_non_ascii_query(self, xhr_header):
86 self.log_user()
88 self.log_user()
87 response = self.app.get(
89 response = self.app.get(
88 route_path('repo_list_data'),
90 route_path('repo_list_data'),
89 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'},
91 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'},
90 extra_environ=xhr_header, status=200)
92 extra_environ=xhr_header, status=200)
91 result = json.loads(response.body)['results']
93 result = json.loads(response.body)['results']
92
94
93 repos = assert_and_get_repo_list_content(result)
95 repos = assert_and_get_repo_list_content(result)
94
96
95 assert len(repos) == 0
97 assert len(repos) == 0
@@ -1,112 +1,112 b''
1
1
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
21 import json
22 import pytest
20 import pytest
23
21
24 from rhodecode.tests import TestController
22 from rhodecode.tests import TestController
25 from rhodecode.tests.fixture import Fixture
23 from rhodecode.tests.fixture import Fixture
26
24 from rhodecode.lib.ext_json import json
27
25
28 fixture = Fixture()
26 fixture = Fixture()
29
27
30
28
31 def route_path(name, params=None, **kwargs):
29 def route_path(name, params=None, **kwargs):
32 import urllib.request, urllib.parse, urllib.error
30 import urllib.request
31 import urllib.parse
32 import 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.parse.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,116 +1,118 b''
1
1
2 # Copyright (C) 2016-2020 RhodeCode GmbH
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # -*- coding: utf-8 -*-
20
2
21 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
22 #
4 #
23 # 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
24 # 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
25 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
26 #
8 #
27 # 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,
28 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
29 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 # GNU General Public License for more details.
12 # GNU General Public License for more details.
31 #
13 #
32 # 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
33 # 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/>.
34 #
16 #
35 # 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
36 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
37 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
38
20
39 import json
21
22 # Copyright (C) 2016-2020 RhodeCode GmbH
23 #
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
26 # (only), as published by the Free Software Foundation.
27 #
28 # This program is distributed in the hope that it will be useful,
29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 # GNU General Public License for more details.
32 #
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/>.
35 #
36 # This program is dual-licensed. If you wish to learn more about the
37 # RhodeCode Enterprise Edition, including its added features, Support services,
38 # and proprietary license terms, please see https://rhodecode.com/licenses/
40
39
41 import pytest
40 import pytest
42
41
43 from rhodecode.tests import TestController
42 from rhodecode.tests import TestController
44 from rhodecode.tests.fixture import Fixture
43 from rhodecode.tests.fixture import Fixture
44 from rhodecode.lib.ext_json import json
45
45
46
46
47 fixture = Fixture()
47 fixture = Fixture()
48
48
49
49
50 def route_path(name, params=None, **kwargs):
50 def route_path(name, params=None, **kwargs):
51 import urllib.request, urllib.parse, urllib.error
51 import urllib.request
52 import urllib.parse
53 import urllib.error
52
54
53 base_url = {
55 base_url = {
54 'user_autocomplete_data': '/_users',
56 'user_autocomplete_data': '/_users',
55 'user_group_autocomplete_data': '/_user_groups'
57 'user_group_autocomplete_data': '/_user_groups'
56 }[name].format(**kwargs)
58 }[name].format(**kwargs)
57
59
58 if params:
60 if params:
59 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
61 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
60 return base_url
62 return base_url
61
63
62
64
63 class TestUserGroupAutocompleteData(TestController):
65 class TestUserGroupAutocompleteData(TestController):
64
66
65 def test_returns_list_of_user_groups(self, user_util, xhr_header):
67 def test_returns_list_of_user_groups(self, user_util, xhr_header):
66 self.log_user()
68 self.log_user()
67 user_group = user_util.create_user_group(active=True)
69 user_group = user_util.create_user_group(active=True)
68 user_group_name = user_group.users_group_name
70 user_group_name = user_group.users_group_name
69 response = self.app.get(
71 response = self.app.get(
70 route_path('user_group_autocomplete_data'),
72 route_path('user_group_autocomplete_data'),
71 extra_environ=xhr_header, status=200)
73 extra_environ=xhr_header, status=200)
72 result = json.loads(response.body)
74 result = json.loads(response.body)
73 values = [suggestion['value'] for suggestion in result['suggestions']]
75 values = [suggestion['value'] for suggestion in result['suggestions']]
74 assert user_group_name in values
76 assert user_group_name in values
75
77
76 def test_returns_inactive_user_groups_when_active_flag_sent(
78 def test_returns_inactive_user_groups_when_active_flag_sent(
77 self, user_util, xhr_header):
79 self, user_util, xhr_header):
78 self.log_user()
80 self.log_user()
79 user_group = user_util.create_user_group(active=False)
81 user_group = user_util.create_user_group(active=False)
80 user_group_name = user_group.users_group_name
82 user_group_name = user_group.users_group_name
81
83
82 response = self.app.get(
84 response = self.app.get(
83 route_path('user_group_autocomplete_data',
85 route_path('user_group_autocomplete_data',
84 params=dict(active='0')),
86 params=dict(active='0')),
85 extra_environ=xhr_header, status=200)
87 extra_environ=xhr_header, status=200)
86 result = json.loads(response.body)
88 result = json.loads(response.body)
87 values = [suggestion['value'] for suggestion in result['suggestions']]
89 values = [suggestion['value'] for suggestion in result['suggestions']]
88 assert user_group_name in values
90 assert user_group_name in values
89
91
90 response = self.app.get(
92 response = self.app.get(
91 route_path('user_group_autocomplete_data',
93 route_path('user_group_autocomplete_data',
92 params=dict(active='1')),
94 params=dict(active='1')),
93 extra_environ=xhr_header, status=200)
95 extra_environ=xhr_header, status=200)
94 result = json.loads(response.body)
96 result = json.loads(response.body)
95 values = [suggestion['value'] for suggestion in result['suggestions']]
97 values = [suggestion['value'] for suggestion in result['suggestions']]
96 assert user_group_name not in values
98 assert user_group_name not in values
97
99
98 @pytest.mark.parametrize('query, count', [
100 @pytest.mark.parametrize('query, count', [
99 ('hello1', 0),
101 ('hello1', 0),
100 ('dev', 1),
102 ('dev', 1),
101 ])
103 ])
102 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count):
104 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count):
103 self.log_user()
105 self.log_user()
104
106
105 user_util._test_name = 'dev-test'
107 user_util._test_name = 'dev-test'
106 user_util.create_user_group()
108 user_util.create_user_group()
107
109
108 response = self.app.get(
110 response = self.app.get(
109 route_path('user_group_autocomplete_data',
111 route_path('user_group_autocomplete_data',
110 params=dict(user_groups='true',
112 params=dict(user_groups='true',
111 query=query)),
113 query=query)),
112 extra_environ=xhr_header, status=200)
114 extra_environ=xhr_header, status=200)
113
115
114 result = json.loads(response.body)
116 result = json.loads(response.body)
115
117
116 assert len(result['suggestions']) == count
118 assert len(result['suggestions']) == count
@@ -1,178 +1,178 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20
20
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.model.db import Repository, RepoGroup, User
24 from rhodecode.model.db import Repository, RepoGroup, User
25 from rhodecode.model.meta import Session
25 from rhodecode.model.meta import Session
26 from rhodecode.model.repo import RepoModel
26 from rhodecode.model.repo import RepoModel
27 from rhodecode.model.repo_group import RepoGroupModel
27 from rhodecode.model.repo_group import RepoGroupModel
28 from rhodecode.model.settings import SettingsModel
28 from rhodecode.model.settings import SettingsModel
29 from rhodecode.tests import TestController
29 from rhodecode.tests import TestController
30 from rhodecode.tests.fixture import Fixture
30 from rhodecode.tests.fixture import Fixture
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32
32
33 fixture = Fixture()
33 fixture = Fixture()
34
34
35
35
36 def route_path(name, **kwargs):
36 def route_path(name, **kwargs):
37 return {
37 return {
38 'home': '/',
38 'home': '/',
39 'main_page_repos_data': '/_home_repos',
39 'main_page_repos_data': '/_home_repos',
40 'main_page_repo_groups_data': '/_home_repo_groups',
40 'main_page_repo_groups_data': '/_home_repo_groups',
41 'repo_group_home': '/{repo_group_name}'
41 'repo_group_home': '/{repo_group_name}'
42 }[name].format(**kwargs)
42 }[name].format(**kwargs)
43
43
44
44
45 class TestHomeController(TestController):
45 class TestHomeController(TestController):
46
46
47 def test_index(self):
47 def test_index(self):
48 self.log_user()
48 self.log_user()
49 response = self.app.get(route_path('home'))
49 response = self.app.get(route_path('home'))
50 # if global permission is set
50 # if global permission is set
51 response.mustcontain('New Repository')
51 response.mustcontain('New Repository')
52
52
53 def test_index_grid_repos(self, xhr_header):
53 def test_index_grid_repos(self, xhr_header):
54 self.log_user()
54 self.log_user()
55 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
55 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
56 # search for objects inside the JavaScript JSON
56 # search for objects inside the JavaScript JSON
57 for obj in Repository.getAll():
57 for obj in Repository.getAll():
58 response.mustcontain('<a href=\\"/{}\\">'.format(obj.repo_name))
58 response.mustcontain('<a href=\\"/{}\\">'.format(obj.repo_name))
59
59
60 def test_index_grid_repo_groups(self, xhr_header):
60 def test_index_grid_repo_groups(self, xhr_header):
61 self.log_user()
61 self.log_user()
62 response = self.app.get(route_path('main_page_repo_groups_data'),
62 response = self.app.get(route_path('main_page_repo_groups_data'),
63 extra_environ=xhr_header,)
63 extra_environ=xhr_header,)
64
64
65 # search for objects inside the JavaScript JSON
65 # search for objects inside the JavaScript JSON
66 for obj in RepoGroup.getAll():
66 for obj in RepoGroup.getAll():
67 response.mustcontain('<a href=\\"/{}\\">'.format(obj.group_name))
67 response.mustcontain('<a href=\\"/{}\\">'.format(obj.group_name))
68
68
69 def test_index_grid_repo_groups_without_access(self, xhr_header, user_util):
69 def test_index_grid_repo_groups_without_access(self, xhr_header, user_util):
70 user = user_util.create_user(password='qweqwe')
70 user = user_util.create_user(password='qweqwe')
71 group_ok = user_util.create_repo_group(owner=user)
71 group_ok = user_util.create_repo_group(owner=user)
72 group_id_ok = group_ok.group_id
72 group_id_ok = group_ok.group_id
73
73
74 group_forbidden = user_util.create_repo_group(owner=User.get_first_super_admin())
74 group_forbidden = user_util.create_repo_group(owner=User.get_first_super_admin())
75 group_id_forbidden = group_forbidden.group_id
75 group_id_forbidden = group_forbidden.group_id
76
76
77 user_util.grant_user_permission_to_repo_group(group_forbidden, user, 'group.none')
77 user_util.grant_user_permission_to_repo_group(group_forbidden, user, 'group.none')
78 self.log_user(user.username, 'qweqwe')
78 self.log_user(user.username, 'qweqwe')
79
79
80 self.app.get(route_path('main_page_repo_groups_data'),
80 self.app.get(route_path('main_page_repo_groups_data'),
81 extra_environ=xhr_header,
81 extra_environ=xhr_header,
82 params={'repo_group_id': group_id_ok}, status=200)
82 params={'repo_group_id': group_id_ok}, status=200)
83
83
84 self.app.get(route_path('main_page_repo_groups_data'),
84 self.app.get(route_path('main_page_repo_groups_data'),
85 extra_environ=xhr_header,
85 extra_environ=xhr_header,
86 params={'repo_group_id': group_id_forbidden}, status=404)
86 params={'repo_group_id': group_id_forbidden}, status=404)
87
87
88 def test_index_contains_statics_with_ver(self):
88 def test_index_contains_statics_with_ver(self):
89 from rhodecode.lib.base import calculate_version_hash
89 from rhodecode.lib.base import calculate_version_hash
90
90
91 self.log_user()
91 self.log_user()
92 response = self.app.get(route_path('home'))
92 response = self.app.get(route_path('home'))
93
93
94 rhodecode_version_hash = calculate_version_hash(
94 rhodecode_version_hash = calculate_version_hash(
95 {'beaker.session.secret': 'test-rc-uytcxaz'})
95 {'beaker.session.secret': 'test-rc-uytcxaz'})
96 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
96 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
97 response.mustcontain('scripts.min.js?ver={0}'.format(rhodecode_version_hash))
97 response.mustcontain('scripts.min.js?ver={0}'.format(rhodecode_version_hash))
98
98
99 def test_index_contains_backend_specific_details(self, backend, xhr_header):
99 def test_index_contains_backend_specific_details(self, backend, xhr_header):
100 self.log_user()
100 self.log_user()
101 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
101 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
102 tip = backend.repo.get_commit().raw_id
102 tip = backend.repo.get_commit().raw_id
103
103
104 # html in javascript variable:
104 # html in javascript variable:
105 response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
105 response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
106 response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
106 response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
107
107
108 response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
108 response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
109 response.mustcontain("""Added a symlink""")
109 response.mustcontain("""Added a symlink""")
110
110
111 def test_index_with_anonymous_access_disabled(self):
111 def test_index_with_anonymous_access_disabled(self):
112 with fixture.anon_access(False):
112 with fixture.anon_access(False):
113 response = self.app.get(route_path('home'), status=302)
113 response = self.app.get(route_path('home'), status=302)
114 assert 'login' in response.location
114 assert 'login' in response.location
115
115
116 def test_index_page_on_groups_with_wrong_group_id(self, autologin_user, xhr_header):
116 def test_index_page_on_groups_with_wrong_group_id(self, autologin_user, xhr_header):
117 group_id = 918123
117 group_id = 918123
118 self.app.get(
118 self.app.get(
119 route_path('main_page_repo_groups_data'),
119 route_path('main_page_repo_groups_data'),
120 params={'repo_group_id': group_id},
120 params={'repo_group_id': group_id},
121 status=404, extra_environ=xhr_header)
121 status=404, extra_environ=xhr_header)
122
122
123 def test_index_page_on_groups(self, autologin_user, user_util, xhr_header):
123 def test_index_page_on_groups(self, autologin_user, user_util, xhr_header):
124 gr = user_util.create_repo_group()
124 gr = user_util.create_repo_group()
125 repo = user_util.create_repo(parent=gr)
125 repo = user_util.create_repo(parent=gr)
126 repo_name = repo.repo_name
126 repo_name = repo.repo_name
127 group_id = gr.group_id
127 group_id = gr.group_id
128
128
129 response = self.app.get(route_path(
129 response = self.app.get(route_path(
130 'repo_group_home', repo_group_name=gr.group_name))
130 'repo_group_home', repo_group_name=gr.group_name))
131 response.mustcontain('d.repo_group_id = {}'.format(group_id))
131 response.mustcontain('d.repo_group_id = {}'.format(group_id))
132
132
133 response = self.app.get(
133 response = self.app.get(
134 route_path('main_page_repos_data'),
134 route_path('main_page_repos_data'),
135 params={'repo_group_id': group_id},
135 params={'repo_group_id': group_id},
136 extra_environ=xhr_header,)
136 extra_environ=xhr_header,)
137 response.mustcontain(repo_name)
137 response.mustcontain(repo_name)
138
138
139 def test_index_page_on_group_with_trailing_slash(self, autologin_user, user_util, xhr_header):
139 def test_index_page_on_group_with_trailing_slash(self, autologin_user, user_util, xhr_header):
140 gr = user_util.create_repo_group()
140 gr = user_util.create_repo_group()
141 repo = user_util.create_repo(parent=gr)
141 repo = user_util.create_repo(parent=gr)
142 repo_name = repo.repo_name
142 repo_name = repo.repo_name
143 group_id = gr.group_id
143 group_id = gr.group_id
144
144
145 response = self.app.get(route_path(
145 response = self.app.get(route_path(
146 'repo_group_home', repo_group_name=gr.group_name+'/'))
146 'repo_group_home', repo_group_name=gr.group_name+'/'))
147 response.mustcontain('d.repo_group_id = {}'.format(group_id))
147 response.mustcontain('d.repo_group_id = {}'.format(group_id))
148
148
149 response = self.app.get(
149 response = self.app.get(
150 route_path('main_page_repos_data'),
150 route_path('main_page_repos_data'),
151 params={'repo_group_id': group_id},
151 params={'repo_group_id': group_id},
152 extra_environ=xhr_header, )
152 extra_environ=xhr_header, )
153 response.mustcontain(repo_name)
153 response.mustcontain(repo_name)
154
154
155 @pytest.mark.parametrize("name, state", [
155 @pytest.mark.parametrize("name, state", [
156 ('Disabled', False),
156 ('Disabled', False),
157 ('Enabled', True),
157 ('Enabled', True),
158 ])
158 ])
159 def test_index_show_version(self, autologin_user, name, state):
159 def test_index_show_version(self, autologin_user, name, state):
160 version_string = 'RhodeCode %s' % rhodecode.__version__
160 version_string = 'RhodeCode %s' % rhodecode.__version__
161
161
162 sett = SettingsModel().create_or_update_setting(
162 sett = SettingsModel().create_or_update_setting(
163 'show_version', state, 'bool')
163 'show_version', state, 'bool')
164 Session().add(sett)
164 Session().add(sett)
165 Session().commit()
165 Session().commit()
166 SettingsModel().invalidate_settings_cache()
166 SettingsModel().invalidate_settings_cache(hard=True)
167
167
168 response = self.app.get(route_path('home'))
168 response = self.app.get(route_path('home'))
169 if state is True:
169 if state is True:
170 response.mustcontain(version_string)
170 response.mustcontain(version_string)
171 if state is False:
171 if state is False:
172 response.mustcontain(no=[version_string])
172 response.mustcontain(no=[version_string])
173
173
174 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
174 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
175 response = self.app.get(route_path('home'))
175 response = self.app.get(route_path('home'))
176 assert_response = response.assert_response()
176 assert_response = response.assert_response()
177 element = assert_response.get_element('.logout [name=csrf_token]')
177 element = assert_response.get_element('.logout [name=csrf_token]')
178 assert element.value == csrf_token
178 assert element.value == csrf_token
@@ -1,105 +1,107 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import datetime
20 import datetime
21
21
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.tests import TestController
25 from rhodecode.tests import TestController
26 from rhodecode.model.db import UserFollowing, Repository
26 from rhodecode.model.db import UserFollowing, Repository
27
27
28
28
29 def route_path(name, params=None, **kwargs):
29 def route_path(name, params=None, **kwargs):
30 import urllib.request, urllib.parse, urllib.error
30 import urllib.request
31 import urllib.parse
32 import urllib.error
31
33
32 base_url = {
34 base_url = {
33 'journal': ADMIN_PREFIX + '/journal',
35 'journal': ADMIN_PREFIX + '/journal',
34 'journal_rss': ADMIN_PREFIX + '/journal/rss',
36 'journal_rss': ADMIN_PREFIX + '/journal/rss',
35 'journal_atom': ADMIN_PREFIX + '/journal/atom',
37 'journal_atom': ADMIN_PREFIX + '/journal/atom',
36 'journal_public': ADMIN_PREFIX + '/public_journal',
38 'journal_public': ADMIN_PREFIX + '/public_journal',
37 'journal_public_atom': ADMIN_PREFIX + '/public_journal/atom',
39 'journal_public_atom': ADMIN_PREFIX + '/public_journal/atom',
38 'journal_public_atom_old': ADMIN_PREFIX + '/public_journal_atom',
40 'journal_public_atom_old': ADMIN_PREFIX + '/public_journal_atom',
39 'journal_public_rss': ADMIN_PREFIX + '/public_journal/rss',
41 'journal_public_rss': ADMIN_PREFIX + '/public_journal/rss',
40 'journal_public_rss_old': ADMIN_PREFIX + '/public_journal_rss',
42 'journal_public_rss_old': ADMIN_PREFIX + '/public_journal_rss',
41 'toggle_following': ADMIN_PREFIX + '/toggle_following',
43 'toggle_following': ADMIN_PREFIX + '/toggle_following',
42 }[name].format(**kwargs)
44 }[name].format(**kwargs)
43
45
44 if params:
46 if params:
45 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 return base_url
48 return base_url
47
49
48
50
49 class TestJournalViews(TestController):
51 class TestJournalViews(TestController):
50
52
51 def test_journal(self):
53 def test_journal(self):
52 self.log_user()
54 self.log_user()
53 response = self.app.get(route_path('journal'))
55 response = self.app.get(route_path('journal'))
54 # response.mustcontain(
56 # response.mustcontain(
55 # """<div class="journal_day">%s</div>""" % datetime.date.today())
57 # """<div class="journal_day">%s</div>""" % datetime.date.today())
56
58
57 @pytest.mark.parametrize("feed_type, content_type", [
59 @pytest.mark.parametrize("feed_type, content_type", [
58 ('rss', "application/rss+xml"),
60 ('rss', "application/rss+xml"),
59 ('atom', "application/atom+xml")
61 ('atom', "application/atom+xml")
60 ])
62 ])
61 def test_journal_feed(self, feed_type, content_type):
63 def test_journal_feed(self, feed_type, content_type):
62 self.log_user()
64 self.log_user()
63 response = self.app.get(
65 response = self.app.get(
64 route_path(
66 route_path(
65 'journal_{}'.format(feed_type)),
67 'journal_{}'.format(feed_type)),
66 status=200)
68 status=200)
67
69
68 assert response.content_type == content_type
70 assert response.content_type == content_type
69
71
70 def test_toggle_following_repository(self, backend):
72 def test_toggle_following_repository(self, backend):
71 user = self.log_user()
73 user = self.log_user()
72 repo = Repository.get_by_repo_name(backend.repo_name)
74 repo = Repository.get_by_repo_name(backend.repo_name)
73 repo_id = repo.repo_id
75 repo_id = repo.repo_id
74 self.app.post(
76 self.app.post(
75 route_path('toggle_following'), {'follows_repo_id': repo_id,
77 route_path('toggle_following'), {'follows_repo_id': repo_id,
76 'csrf_token': self.csrf_token})
78 'csrf_token': self.csrf_token})
77
79
78 followings = UserFollowing.query()\
80 followings = UserFollowing.query()\
79 .filter(UserFollowing.user_id == user['user_id'])\
81 .filter(UserFollowing.user_id == user['user_id'])\
80 .filter(UserFollowing.follows_repo_id == repo_id).all()
82 .filter(UserFollowing.follows_repo_id == repo_id).all()
81
83
82 assert len(followings) == 0
84 assert len(followings) == 0
83
85
84 self.app.post(
86 self.app.post(
85 route_path('toggle_following'), {'follows_repo_id': repo_id,
87 route_path('toggle_following'), {'follows_repo_id': repo_id,
86 'csrf_token': self.csrf_token})
88 'csrf_token': self.csrf_token})
87
89
88 followings = UserFollowing.query()\
90 followings = UserFollowing.query()\
89 .filter(UserFollowing.user_id == user['user_id'])\
91 .filter(UserFollowing.user_id == user['user_id'])\
90 .filter(UserFollowing.follows_repo_id == repo_id).all()
92 .filter(UserFollowing.follows_repo_id == repo_id).all()
91
93
92 assert len(followings) == 1
94 assert len(followings) == 1
93
95
94 @pytest.mark.parametrize("feed_type, content_type", [
96 @pytest.mark.parametrize("feed_type, content_type", [
95 ('rss', "application/rss+xml"),
97 ('rss', "application/rss+xml"),
96 ('atom', "application/atom+xml")
98 ('atom', "application/atom+xml")
97 ])
99 ])
98 def test_public_journal_feed(self, feed_type, content_type):
100 def test_public_journal_feed(self, feed_type, content_type):
99 self.log_user()
101 self.log_user()
100 response = self.app.get(
102 response = self.app.get(
101 route_path(
103 route_path(
102 'journal_public_{}'.format(feed_type)),
104 'journal_public_{}'.format(feed_type)),
103 status=200)
105 status=200)
104
106
105 assert response.content_type == content_type
107 assert response.content_type == content_type
@@ -1,579 +1,608 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import urllib.parse
20 import urllib.parse
21
21
22 import mock
22 import mock
23 import pytest
23 import pytest
24
24
25 from rhodecode.tests import (
25 from rhodecode.tests import (
26 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
26 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
27 no_newline_id_generator)
27 no_newline_id_generator)
28 from rhodecode.tests.fixture import Fixture
28 from rhodecode.tests.fixture import Fixture
29 from rhodecode.lib.auth import check_password
29 from rhodecode.lib.auth import check_password
30 from rhodecode.lib import helpers as h
30 from rhodecode.lib import helpers as h
31 from rhodecode.model.auth_token import AuthTokenModel
31 from rhodecode.model.auth_token import AuthTokenModel
32 from rhodecode.model.db import User, Notification, UserApiKeys
32 from rhodecode.model.db import User, Notification, UserApiKeys
33 from rhodecode.model.meta import Session
33 from rhodecode.model.meta import Session
34
34
35 fixture = Fixture()
35 fixture = Fixture()
36
36
37 whitelist_view = ['RepoCommitsView:repo_commit_raw']
37 whitelist_view = ['RepoCommitsView:repo_commit_raw']
38
38
39
39
40 def route_path(name, params=None, **kwargs):
40 def route_path(name, params=None, **kwargs):
41 import urllib.request, urllib.parse, urllib.error
41 import urllib.request
42 import urllib.parse
43 import urllib.error
42 from rhodecode.apps._base import ADMIN_PREFIX
44 from rhodecode.apps._base import ADMIN_PREFIX
43
45
44 base_url = {
46 base_url = {
45 'login': ADMIN_PREFIX + '/login',
47 'login': ADMIN_PREFIX + '/login',
46 'logout': ADMIN_PREFIX + '/logout',
48 'logout': ADMIN_PREFIX + '/logout',
47 'register': ADMIN_PREFIX + '/register',
49 'register': ADMIN_PREFIX + '/register',
48 'reset_password':
50 'reset_password':
49 ADMIN_PREFIX + '/password_reset',
51 ADMIN_PREFIX + '/password_reset',
50 'reset_password_confirmation':
52 'reset_password_confirmation':
51 ADMIN_PREFIX + '/password_reset_confirmation',
53 ADMIN_PREFIX + '/password_reset_confirmation',
52
54
53 'admin_permissions_application':
55 'admin_permissions_application':
54 ADMIN_PREFIX + '/permissions/application',
56 ADMIN_PREFIX + '/permissions/application',
55 'admin_permissions_application_update':
57 'admin_permissions_application_update':
56 ADMIN_PREFIX + '/permissions/application/update',
58 ADMIN_PREFIX + '/permissions/application/update',
57
59
58 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
60 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
59
61
60 }[name].format(**kwargs)
62 }[name].format(**kwargs)
61
63
62 if params:
64 if params:
63 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
65 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
64 return base_url
66 return base_url
65
67
66
68
67 @pytest.mark.usefixtures('app')
69 @pytest.mark.usefixtures('app')
68 class TestLoginController(object):
70 class TestLoginController(object):
69 destroy_users = set()
71 destroy_users = set()
70
72
71 @classmethod
73 @classmethod
72 def teardown_class(cls):
74 def teardown_class(cls):
73 fixture.destroy_users(cls.destroy_users)
75 fixture.destroy_users(cls.destroy_users)
74
76
75 def teardown_method(self, method):
77 def teardown_method(self, method):
76 for n in Notification.query().all():
78 for n in Notification.query().all():
77 Session().delete(n)
79 Session().delete(n)
78
80
79 Session().commit()
81 Session().commit()
80 assert Notification.query().all() == []
82 assert Notification.query().all() == []
81
83
82 def test_index(self):
84 def test_index(self):
83 response = self.app.get(route_path('login'))
85 response = self.app.get(route_path('login'))
84 assert response.status == '200 OK'
86 assert response.status == '200 OK'
85 # Test response...
87 # Test response...
86
88
87 def test_login_admin_ok(self):
89 def test_login_admin_ok(self):
88 response = self.app.post(route_path('login'),
90 response = self.app.post(route_path('login'),
89 {'username': 'test_admin',
91 {'username': 'test_admin',
90 'password': 'test12'}, status=302)
92 'password': 'test12'}, status=302)
91 response = response.follow()
93 response = response.follow()
92 session = response.get_session_from_response()
94 session = response.get_session_from_response()
93 username = session['rhodecode_user'].get('username')
95 username = session['rhodecode_user'].get('username')
94 assert username == 'test_admin'
96 assert username == 'test_admin'
95 response.mustcontain('logout')
97 response.mustcontain('logout')
96
98
97 def test_login_regular_ok(self):
99 def test_login_regular_ok(self):
98 response = self.app.post(route_path('login'),
100 response = self.app.post(route_path('login'),
99 {'username': 'test_regular',
101 {'username': 'test_regular',
100 'password': 'test12'}, status=302)
102 'password': 'test12'}, status=302)
101
103
102 response = response.follow()
104 response = response.follow()
103 session = response.get_session_from_response()
105 session = response.get_session_from_response()
104 username = session['rhodecode_user'].get('username')
106 username = session['rhodecode_user'].get('username')
105 assert username == 'test_regular'
107 assert username == 'test_regular'
106 response.mustcontain('logout')
108 response.mustcontain('logout')
107
109
108 def test_login_regular_forbidden_when_super_admin_restriction(self):
110 def test_login_regular_forbidden_when_super_admin_restriction(self):
109 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
111 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
110 with fixture.auth_restriction(self.app._pyramid_registry,
112 with fixture.auth_restriction(self.app._pyramid_registry,
111 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
113 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
112 response = self.app.post(route_path('login'),
114 response = self.app.post(route_path('login'),
113 {'username': 'test_regular',
115 {'username': 'test_regular',
114 'password': 'test12'})
116 'password': 'test12'})
115
117
116 response.mustcontain('invalid user name')
118 response.mustcontain('invalid user name')
117 response.mustcontain('invalid password')
119 response.mustcontain('invalid password')
118
120
119 def test_login_regular_forbidden_when_scope_restriction(self):
121 def test_login_regular_forbidden_when_scope_restriction(self):
120 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
122 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
121 with fixture.scope_restriction(self.app._pyramid_registry,
123 with fixture.scope_restriction(self.app._pyramid_registry,
122 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
124 RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
123 response = self.app.post(route_path('login'),
125 response = self.app.post(route_path('login'),
124 {'username': 'test_regular',
126 {'username': 'test_regular',
125 'password': 'test12'})
127 'password': 'test12'})
126
128
127 response.mustcontain('invalid user name')
129 response.mustcontain('invalid user name')
128 response.mustcontain('invalid password')
130 response.mustcontain('invalid password')
129
131
130 def test_login_ok_came_from(self):
132 def test_login_ok_came_from(self):
131 test_came_from = '/_admin/users?branch=stable'
133 test_came_from = '/_admin/users?branch=stable'
132 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
134 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
133 response = self.app.post(
135 response = self.app.post(
134 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
136 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
135
137
136 assert 'branch=stable' in response.location
138 assert 'branch=stable' in response.location
137 response = response.follow()
139 response = response.follow()
138
140
139 assert response.status == '200 OK'
141 assert response.status == '200 OK'
140 response.mustcontain('Users administration')
142 response.mustcontain('Users administration')
141
143
142 def test_redirect_to_login_with_get_args(self):
144 def test_redirect_to_login_with_get_args(self):
143 with fixture.anon_access(False):
145 with fixture.anon_access(False):
144 kwargs = {'branch': 'stable'}
146 kwargs = {'branch': 'stable'}
145 response = self.app.get(
147 response = self.app.get(
146 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
148 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
147 status=302)
149 status=302)
148
150
149 response_query = urllib.parse.parse_qsl(response.location)
151 response_query = urllib.parse.parse_qsl(response.location)
150 assert 'branch=stable' in response_query[0][1]
152 assert 'branch=stable' in response_query[0][1]
151
153
152 def test_login_form_with_get_args(self):
154 def test_login_form_with_get_args(self):
153 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
155 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
154 response = self.app.get(_url)
156 response = self.app.get(_url)
155 assert 'branch%3Dstable' in response.form.action
157 assert 'branch%3Dstable' in response.form.action
156
158
157 @pytest.mark.parametrize("url_came_from", [
159 @pytest.mark.parametrize("url_came_from", [
158 'data:text/html,<script>window.alert("xss")</script>',
160 'data:text/html,<script>window.alert("xss")</script>',
159 'mailto:test@rhodecode.org',
161 'mailto:test@rhodecode.org',
160 'file:///etc/passwd',
162 'file:///etc/passwd',
161 'ftp://some.ftp.server',
163 'ftp://some.ftp.server',
162 'http://other.domain',
164 'http://other.domain',
163 '/\r\nX-Forwarded-Host: http://example.org',
164 ], ids=no_newline_id_generator)
165 ], ids=no_newline_id_generator)
165 def test_login_bad_came_froms(self, url_came_from):
166 def test_login_bad_came_froms(self, url_came_from):
166 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
167 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
167 response = self.app.post(
168 response = self.app.post(
168 _url,
169 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
169 {'username': 'test_admin', 'password': 'test12'})
170 assert response.status == '302 Found'
170 assert response.status == '302 Found'
171 response = response.follow()
171 response = response.follow()
172 assert response.status == '200 OK'
172 assert response.status == '200 OK'
173 assert response.request.path == '/'
173 assert response.request.path == '/'
174
174
175 @pytest.mark.xfail(reason="newline params changed behaviour in python3")
176 @pytest.mark.parametrize("url_came_from", [
177 '/\r\nX-Forwarded-Host: \rhttp://example.org',
178 ], ids=no_newline_id_generator)
179 def test_login_bad_came_froms_404(self, url_came_from):
180 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
181 response = self.app.post(
182 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
183
184 response = response.follow()
185 assert response.status == '404 Not Found'
186
175 def test_login_short_password(self):
187 def test_login_short_password(self):
176 response = self.app.post(route_path('login'),
188 response = self.app.post(route_path('login'),
177 {'username': 'test_admin',
189 {'username': 'test_admin',
178 'password': 'as'})
190 'password': 'as'})
179 assert response.status == '200 OK'
191 assert response.status == '200 OK'
180
192
181 response.mustcontain('Enter 3 characters or more')
193 response.mustcontain('Enter 3 characters or more')
182
194
183 def test_login_wrong_non_ascii_password(self, user_regular):
195 def test_login_wrong_non_ascii_password(self, user_regular):
184 response = self.app.post(
196 response = self.app.post(
185 route_path('login'),
197 route_path('login'),
186 {'username': user_regular.username,
198 {'username': user_regular.username,
187 'password': u'invalid-non-asci\xe4'.encode('utf8')})
199 'password': 'invalid-non-asci\xe4'.encode('utf8')})
188
200
189 response.mustcontain('invalid user name')
201 response.mustcontain('invalid user name')
190 response.mustcontain('invalid password')
202 response.mustcontain('invalid password')
191
203
192 def test_login_with_non_ascii_password(self, user_util):
204 def test_login_with_non_ascii_password(self, user_util):
193 password = u'valid-non-ascii\xe4'
205 password = u'valid-non-ascii\xe4'
194 user = user_util.create_user(password=password)
206 user = user_util.create_user(password=password)
195 response = self.app.post(
207 response = self.app.post(
196 route_path('login'),
208 route_path('login'),
197 {'username': user.username,
209 {'username': user.username,
198 'password': password})
210 'password': password})
199 assert response.status_code == 302
211 assert response.status_code == 302
200
212
201 def test_login_wrong_username_password(self):
213 def test_login_wrong_username_password(self):
202 response = self.app.post(route_path('login'),
214 response = self.app.post(route_path('login'),
203 {'username': 'error',
215 {'username': 'error',
204 'password': 'test12'})
216 'password': 'test12'})
205
217
206 response.mustcontain('invalid user name')
218 response.mustcontain('invalid user name')
207 response.mustcontain('invalid password')
219 response.mustcontain('invalid password')
208
220
209 def test_login_admin_ok_password_migration(self, real_crypto_backend):
221 def test_login_admin_ok_password_migration(self, real_crypto_backend):
210 from rhodecode.lib import auth
222 from rhodecode.lib import auth
211
223
212 # create new user, with sha256 password
224 # create new user, with sha256 password
213 temp_user = 'test_admin_sha256'
225 temp_user = 'test_admin_sha256'
214 user = fixture.create_user(temp_user)
226 user = fixture.create_user(temp_user)
215 user.password = auth._RhodeCodeCryptoSha256().hash_create(
227 user.password = auth._RhodeCodeCryptoSha256().hash_create(
216 b'test123')
228 b'test123')
217 Session().add(user)
229 Session().add(user)
218 Session().commit()
230 Session().commit()
219 self.destroy_users.add(temp_user)
231 self.destroy_users.add(temp_user)
220 response = self.app.post(route_path('login'),
232 response = self.app.post(route_path('login'),
221 {'username': temp_user,
233 {'username': temp_user,
222 'password': 'test123'}, status=302)
234 'password': 'test123'}, status=302)
223
235
224 response = response.follow()
236 response = response.follow()
225 session = response.get_session_from_response()
237 session = response.get_session_from_response()
226 username = session['rhodecode_user'].get('username')
238 username = session['rhodecode_user'].get('username')
227 assert username == temp_user
239 assert username == temp_user
228 response.mustcontain('logout')
240 response.mustcontain('logout')
229
241
230 # new password should be bcrypted, after log-in and transfer
242 # new password should be bcrypted, after log-in and transfer
231 user = User.get_by_username(temp_user)
243 user = User.get_by_username(temp_user)
232 assert user.password.startswith('$')
244 assert user.password.startswith('$')
233
245
234 # REGISTRATIONS
246 # REGISTRATIONS
235 def test_register(self):
247 def test_register(self):
236 response = self.app.get(route_path('register'))
248 response = self.app.get(route_path('register'))
237 response.mustcontain('Create an Account')
249 response.mustcontain('Create an Account')
238
250
239 def test_register_err_same_username(self):
251 def test_register_err_same_username(self):
240 uname = 'test_admin'
252 uname = 'test_admin'
241 response = self.app.post(
253 response = self.app.post(
242 route_path('register'),
254 route_path('register'),
243 {
255 {
244 'username': uname,
256 'username': uname,
245 'password': 'test12',
257 'password': 'test12',
246 'password_confirmation': 'test12',
258 'password_confirmation': 'test12',
247 'email': 'goodmail@domain.com',
259 'email': 'goodmail@domain.com',
248 'firstname': 'test',
260 'firstname': 'test',
249 'lastname': 'test'
261 'lastname': 'test'
250 }
262 }
251 )
263 )
252
264
253 assertr = response.assert_response()
265 assertr = response.assert_response()
254 msg = 'Username "%(username)s" already exists'
266 msg = 'Username "%(username)s" already exists'
255 msg = msg % {'username': uname}
267 msg = msg % {'username': uname}
256 assertr.element_contains('#username+.error-message', msg)
268 assertr.element_contains('#username+.error-message', msg)
257
269
258 def test_register_err_same_email(self):
270 def test_register_err_same_email(self):
259 response = self.app.post(
271 response = self.app.post(
260 route_path('register'),
272 route_path('register'),
261 {
273 {
262 'username': 'test_admin_0',
274 'username': 'test_admin_0',
263 'password': 'test12',
275 'password': 'test12',
264 'password_confirmation': 'test12',
276 'password_confirmation': 'test12',
265 'email': 'test_admin@mail.com',
277 'email': 'test_admin@mail.com',
266 'firstname': 'test',
278 'firstname': 'test',
267 'lastname': 'test'
279 'lastname': 'test'
268 }
280 }
269 )
281 )
270
282
271 assertr = response.assert_response()
283 assertr = response.assert_response()
272 msg = u'This e-mail address is already taken'
284 msg = u'This e-mail address is already taken'
273 assertr.element_contains('#email+.error-message', msg)
285 assertr.element_contains('#email+.error-message', msg)
274
286
275 def test_register_err_same_email_case_sensitive(self):
287 def test_register_err_same_email_case_sensitive(self):
276 response = self.app.post(
288 response = self.app.post(
277 route_path('register'),
289 route_path('register'),
278 {
290 {
279 'username': 'test_admin_1',
291 'username': 'test_admin_1',
280 'password': 'test12',
292 'password': 'test12',
281 'password_confirmation': 'test12',
293 'password_confirmation': 'test12',
282 'email': 'TesT_Admin@mail.COM',
294 'email': 'TesT_Admin@mail.COM',
283 'firstname': 'test',
295 'firstname': 'test',
284 'lastname': 'test'
296 'lastname': 'test'
285 }
297 }
286 )
298 )
287 assertr = response.assert_response()
299 assertr = response.assert_response()
288 msg = u'This e-mail address is already taken'
300 msg = u'This e-mail address is already taken'
289 assertr.element_contains('#email+.error-message', msg)
301 assertr.element_contains('#email+.error-message', msg)
290
302
291 def test_register_err_wrong_data(self):
303 def test_register_err_wrong_data(self):
292 response = self.app.post(
304 response = self.app.post(
293 route_path('register'),
305 route_path('register'),
294 {
306 {
295 'username': 'xs',
307 'username': 'xs',
296 'password': 'test',
308 'password': 'test',
297 'password_confirmation': 'test',
309 'password_confirmation': 'test',
298 'email': 'goodmailm',
310 'email': 'goodmailm',
299 'firstname': 'test',
311 'firstname': 'test',
300 'lastname': 'test'
312 'lastname': 'test'
301 }
313 }
302 )
314 )
303 assert response.status == '200 OK'
315 assert response.status == '200 OK'
304 response.mustcontain('An email address must contain a single @')
316 response.mustcontain('An email address must contain a single @')
305 response.mustcontain('Enter a value 6 characters long or more')
317 response.mustcontain('Enter a value 6 characters long or more')
306
318
307 def test_register_err_username(self):
319 def test_register_err_username(self):
308 response = self.app.post(
320 response = self.app.post(
309 route_path('register'),
321 route_path('register'),
310 {
322 {
311 'username': 'error user',
323 'username': 'error user',
312 'password': 'test12',
324 'password': 'test12',
313 'password_confirmation': 'test12',
325 'password_confirmation': 'test12',
314 'email': 'goodmailm',
326 'email': 'goodmailm',
315 'firstname': 'test',
327 'firstname': 'test',
316 'lastname': 'test'
328 'lastname': 'test'
317 }
329 }
318 )
330 )
319
331
320 response.mustcontain('An email address must contain a single @')
332 response.mustcontain('An email address must contain a single @')
321 response.mustcontain(
333 response.mustcontain(
322 'Username may only contain '
334 'Username may only contain '
323 'alphanumeric characters underscores, '
335 'alphanumeric characters underscores, '
324 'periods or dashes and must begin with '
336 'periods or dashes and must begin with '
325 'alphanumeric character')
337 'alphanumeric character')
326
338
327 def test_register_err_case_sensitive(self):
339 def test_register_err_case_sensitive(self):
328 usr = 'Test_Admin'
340 usr = 'Test_Admin'
329 response = self.app.post(
341 response = self.app.post(
330 route_path('register'),
342 route_path('register'),
331 {
343 {
332 'username': usr,
344 'username': usr,
333 'password': 'test12',
345 'password': 'test12',
334 'password_confirmation': 'test12',
346 'password_confirmation': 'test12',
335 'email': 'goodmailm',
347 'email': 'goodmailm',
336 'firstname': 'test',
348 'firstname': 'test',
337 'lastname': 'test'
349 'lastname': 'test'
338 }
350 }
339 )
351 )
340
352
341 assertr = response.assert_response()
353 assertr = response.assert_response()
342 msg = u'Username "%(username)s" already exists'
354 msg = u'Username "%(username)s" already exists'
343 msg = msg % {'username': usr}
355 msg = msg % {'username': usr}
344 assertr.element_contains('#username+.error-message', msg)
356 assertr.element_contains('#username+.error-message', msg)
345
357
346 def test_register_special_chars(self):
358 def test_register_special_chars(self):
347 response = self.app.post(
359 response = self.app.post(
348 route_path('register'),
360 route_path('register'),
349 {
361 {
350 'username': 'xxxaxn',
362 'username': 'xxxaxn',
351 'password': 'ąćźżąśśśś',
363 'password': 'ąćźżąśśśś',
352 'password_confirmation': 'ąćźżąśśśś',
364 'password_confirmation': 'ąćźżąśśśś',
353 'email': 'goodmailm@test.plx',
365 'email': 'goodmailm@test.plx',
354 'firstname': 'test',
366 'firstname': 'test',
355 'lastname': 'test'
367 'lastname': 'test'
356 }
368 }
357 )
369 )
358
370
359 msg = u'Invalid characters (non-ascii) in password'
371 msg = u'Invalid characters (non-ascii) in password'
360 response.mustcontain(msg)
372 response.mustcontain(msg)
361
373
362 def test_register_password_mismatch(self):
374 def test_register_password_mismatch(self):
363 response = self.app.post(
375 response = self.app.post(
364 route_path('register'),
376 route_path('register'),
365 {
377 {
366 'username': 'xs',
378 'username': 'xs',
367 'password': '123qwe',
379 'password': '123qwe',
368 'password_confirmation': 'qwe123',
380 'password_confirmation': 'qwe123',
369 'email': 'goodmailm@test.plxa',
381 'email': 'goodmailm@test.plxa',
370 'firstname': 'test',
382 'firstname': 'test',
371 'lastname': 'test'
383 'lastname': 'test'
372 }
384 }
373 )
385 )
374 msg = u'Passwords do not match'
386 msg = u'Passwords do not match'
375 response.mustcontain(msg)
387 response.mustcontain(msg)
376
388
377 def test_register_ok(self):
389 def test_register_ok(self):
378 username = 'test_regular4'
390 username = 'test_regular4'
379 password = 'qweqwe'
391 password = 'qweqwe'
380 email = 'marcin@test.com'
392 email = 'marcin@test.com'
381 name = 'testname'
393 name = 'testname'
382 lastname = 'testlastname'
394 lastname = 'testlastname'
383
395
384 # this initializes a session
396 # this initializes a session
385 response = self.app.get(route_path('register'))
397 response = self.app.get(route_path('register'))
386 response.mustcontain('Create an Account')
398 response.mustcontain('Create an Account')
387
399
388
400
389 response = self.app.post(
401 response = self.app.post(
390 route_path('register'),
402 route_path('register'),
391 {
403 {
392 'username': username,
404 'username': username,
393 'password': password,
405 'password': password,
394 'password_confirmation': password,
406 'password_confirmation': password,
395 'email': email,
407 'email': email,
396 'firstname': name,
408 'firstname': name,
397 'lastname': lastname,
409 'lastname': lastname,
398 'admin': True
410 'admin': True
399 },
411 },
400 status=302
412 status=302
401 ) # This should be overridden
413 ) # This should be overridden
402
414
403 assert_session_flash(
415 assert_session_flash(
404 response, 'You have successfully registered with RhodeCode. You can log-in now.')
416 response, 'You have successfully registered with RhodeCode. You can log-in now.')
405
417
406 ret = Session().query(User).filter(
418 ret = Session().query(User).filter(
407 User.username == 'test_regular4').one()
419 User.username == 'test_regular4').one()
408 assert ret.username == username
420 assert ret.username == username
409 assert check_password(password, ret.password)
421 assert check_password(password, ret.password)
410 assert ret.email == email
422 assert ret.email == email
411 assert ret.name == name
423 assert ret.name == name
412 assert ret.lastname == lastname
424 assert ret.lastname == lastname
413 assert ret.auth_tokens is not None
425 assert ret.auth_tokens is not None
414 assert not ret.admin
426 assert not ret.admin
415
427
416 def test_forgot_password_wrong_mail(self):
428 def test_forgot_password_wrong_mail(self):
417 bad_email = 'marcin@wrongmail.org'
429 bad_email = 'marcin@wrongmail.org'
418 # this initializes a session
430 # this initializes a session
419 self.app.get(route_path('reset_password'))
431 self.app.get(route_path('reset_password'))
420
432
421 response = self.app.post(
433 response = self.app.post(
422 route_path('reset_password'), {'email': bad_email, }
434 route_path('reset_password'), {'email': bad_email, }
423 )
435 )
424 assert_session_flash(response,
436 assert_session_flash(response,
425 'If such email exists, a password reset link was sent to it.')
437 'If such email exists, a password reset link was sent to it.')
426
438
427 def test_forgot_password(self, user_util):
439 def test_forgot_password(self, user_util):
428 # this initializes a session
440 # this initializes a session
429 self.app.get(route_path('reset_password'))
441 self.app.get(route_path('reset_password'))
430
442
431 user = user_util.create_user()
443 user = user_util.create_user()
432 user_id = user.user_id
444 user_id = user.user_id
433 email = user.email
445 email = user.email
434
446
435 response = self.app.post(route_path('reset_password'), {'email': email, })
447 response = self.app.post(route_path('reset_password'), {'email': email, })
436
448
437 assert_session_flash(response,
449 assert_session_flash(response,
438 'If such email exists, a password reset link was sent to it.')
450 'If such email exists, a password reset link was sent to it.')
439
451
440 # BAD KEY
452 # BAD KEY
441 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
453 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
442 response = self.app.get(confirm_url, status=302)
454 response = self.app.get(confirm_url, status=302)
443 assert response.location.endswith(route_path('reset_password'))
455 assert response.location.endswith(route_path('reset_password'))
444 assert_session_flash(response, 'Given reset token is invalid')
456 assert_session_flash(response, 'Given reset token is invalid')
445
457
446 response.follow() # cleanup flash
458 response.follow() # cleanup flash
447
459
448 # GOOD KEY
460 # GOOD KEY
449 key = UserApiKeys.query()\
461 key = UserApiKeys.query()\
450 .filter(UserApiKeys.user_id == user_id)\
462 .filter(UserApiKeys.user_id == user_id)\
451 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
463 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
452 .first()
464 .first()
453
465
454 assert key
466 assert key
455
467
456 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
468 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
457 response = self.app.get(confirm_url)
469 response = self.app.get(confirm_url)
458 assert response.status == '302 Found'
470 assert response.status == '302 Found'
459 assert response.location.endswith(route_path('login'))
471 assert response.location.endswith(route_path('login'))
460
472
461 assert_session_flash(
473 assert_session_flash(
462 response,
474 response,
463 'Your password reset was successful, '
475 'Your password reset was successful, '
464 'a new password has been sent to your email')
476 'a new password has been sent to your email')
465
477
466 response.follow()
478 response.follow()
467
479
468 def _get_api_whitelist(self, values=None):
480 def _get_api_whitelist(self, values=None):
469 config = {'api_access_controllers_whitelist': values or []}
481 config = {'api_access_controllers_whitelist': values or []}
470 return config
482 return config
471
483
472 @pytest.mark.parametrize("test_name, auth_token", [
484 @pytest.mark.parametrize("test_name, auth_token", [
473 ('none', None),
485 ('none', None),
474 ('empty_string', ''),
486 ('empty_string', ''),
475 ('fake_number', '123456'),
487 ('fake_number', '123456'),
476 ('proper_auth_token', None)
488 ('proper_auth_token', None)
477 ])
489 ])
478 def test_access_not_whitelisted_page_via_auth_token(
490 def test_access_not_whitelisted_page_via_auth_token(
479 self, test_name, auth_token, user_admin):
491 self, test_name, auth_token, user_admin):
480
492
481 whitelist = self._get_api_whitelist([])
493 whitelist = self._get_api_whitelist([])
482 with mock.patch.dict('rhodecode.CONFIG', whitelist):
494 with mock.patch.dict('rhodecode.CONFIG', whitelist):
483 assert [] == whitelist['api_access_controllers_whitelist']
495 assert [] == whitelist['api_access_controllers_whitelist']
484 if test_name == 'proper_auth_token':
496 if test_name == 'proper_auth_token':
485 # use builtin if api_key is None
497 # use builtin if api_key is None
486 auth_token = user_admin.api_key
498 auth_token = user_admin.api_key
487
499
488 with fixture.anon_access(False):
500 with fixture.anon_access(False):
501 # webtest uses linter to check if response is bytes,
502 # and we use memoryview here as a wrapper, quick turn-off
503 self.app.lint = False
504
489 self.app.get(
505 self.app.get(
490 route_path('repo_commit_raw',
506 route_path('repo_commit_raw',
491 repo_name=HG_REPO, commit_id='tip',
507 repo_name=HG_REPO, commit_id='tip',
492 params=dict(api_key=auth_token)),
508 params=dict(api_key=auth_token)),
493 status=302)
509 status=302)
494
510
495 @pytest.mark.parametrize("test_name, auth_token, code", [
511 @pytest.mark.parametrize("test_name, auth_token, code", [
496 ('none', None, 302),
512 ('none', None, 302),
497 ('empty_string', '', 302),
513 ('empty_string', '', 302),
498 ('fake_number', '123456', 302),
514 ('fake_number', '123456', 302),
499 ('proper_auth_token', None, 200)
515 ('proper_auth_token', None, 200)
500 ])
516 ])
501 def test_access_whitelisted_page_via_auth_token(
517 def test_access_whitelisted_page_via_auth_token(
502 self, test_name, auth_token, code, user_admin):
518 self, test_name, auth_token, code, user_admin):
503
519
504 whitelist = self._get_api_whitelist(whitelist_view)
520 whitelist = self._get_api_whitelist(whitelist_view)
505
521
506 with mock.patch.dict('rhodecode.CONFIG', whitelist):
522 with mock.patch.dict('rhodecode.CONFIG', whitelist):
507 assert whitelist_view == whitelist['api_access_controllers_whitelist']
523 assert whitelist_view == whitelist['api_access_controllers_whitelist']
508
524
509 if test_name == 'proper_auth_token':
525 if test_name == 'proper_auth_token':
510 auth_token = user_admin.api_key
526 auth_token = user_admin.api_key
511 assert auth_token
527 assert auth_token
512
528
513 with fixture.anon_access(False):
529 with fixture.anon_access(False):
530 # webtest uses linter to check if response is bytes,
531 # and we use memoryview here as a wrapper, quick turn-off
532 self.app.lint = False
514 self.app.get(
533 self.app.get(
515 route_path('repo_commit_raw',
534 route_path('repo_commit_raw',
516 repo_name=HG_REPO, commit_id='tip',
535 repo_name=HG_REPO, commit_id='tip',
517 params=dict(api_key=auth_token)),
536 params=dict(api_key=auth_token)),
518 status=code)
537 status=code)
519
538
520 @pytest.mark.parametrize("test_name, auth_token, code", [
539 @pytest.mark.parametrize("test_name, auth_token, code", [
521 ('proper_auth_token', None, 200),
540 ('proper_auth_token', None, 200),
522 ('wrong_auth_token', '123456', 302),
541 ('wrong_auth_token', '123456', 302),
523 ])
542 ])
524 def test_access_whitelisted_page_via_auth_token_bound_to_token(
543 def test_access_whitelisted_page_via_auth_token_bound_to_token(
525 self, test_name, auth_token, code, user_admin):
544 self, test_name, auth_token, code, user_admin):
526
545
527 expected_token = auth_token
546 expected_token = auth_token
528 if test_name == 'proper_auth_token':
547 if test_name == 'proper_auth_token':
529 auth_token = user_admin.api_key
548 auth_token = user_admin.api_key
530 expected_token = auth_token
549 expected_token = auth_token
531 assert auth_token
550 assert auth_token
532
551
533 whitelist = self._get_api_whitelist([
552 whitelist = self._get_api_whitelist([
534 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
553 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
535
554
536 with mock.patch.dict('rhodecode.CONFIG', whitelist):
555 with mock.patch.dict('rhodecode.CONFIG', whitelist):
537
556
538 with fixture.anon_access(False):
557 with fixture.anon_access(False):
558 # webtest uses linter to check if response is bytes,
559 # and we use memoryview here as a wrapper, quick turn-off
560 self.app.lint = False
561
539 self.app.get(
562 self.app.get(
540 route_path('repo_commit_raw',
563 route_path('repo_commit_raw',
541 repo_name=HG_REPO, commit_id='tip',
564 repo_name=HG_REPO, commit_id='tip',
542 params=dict(api_key=auth_token)),
565 params=dict(api_key=auth_token)),
543 status=code)
566 status=code)
544
567
545 def test_access_page_via_extra_auth_token(self):
568 def test_access_page_via_extra_auth_token(self):
546 whitelist = self._get_api_whitelist(whitelist_view)
569 whitelist = self._get_api_whitelist(whitelist_view)
547 with mock.patch.dict('rhodecode.CONFIG', whitelist):
570 with mock.patch.dict('rhodecode.CONFIG', whitelist):
548 assert whitelist_view == \
571 assert whitelist_view == \
549 whitelist['api_access_controllers_whitelist']
572 whitelist['api_access_controllers_whitelist']
550
573
551 new_auth_token = AuthTokenModel().create(
574 new_auth_token = AuthTokenModel().create(
552 TEST_USER_ADMIN_LOGIN, 'test')
575 TEST_USER_ADMIN_LOGIN, 'test')
553 Session().commit()
576 Session().commit()
554 with fixture.anon_access(False):
577 with fixture.anon_access(False):
578 # webtest uses linter to check if response is bytes,
579 # and we use memoryview here as a wrapper, quick turn-off
580 self.app.lint = False
555 self.app.get(
581 self.app.get(
556 route_path('repo_commit_raw',
582 route_path('repo_commit_raw',
557 repo_name=HG_REPO, commit_id='tip',
583 repo_name=HG_REPO, commit_id='tip',
558 params=dict(api_key=new_auth_token.api_key)),
584 params=dict(api_key=new_auth_token.api_key)),
559 status=200)
585 status=200)
560
586
561 def test_access_page_via_expired_auth_token(self):
587 def test_access_page_via_expired_auth_token(self):
562 whitelist = self._get_api_whitelist(whitelist_view)
588 whitelist = self._get_api_whitelist(whitelist_view)
563 with mock.patch.dict('rhodecode.CONFIG', whitelist):
589 with mock.patch.dict('rhodecode.CONFIG', whitelist):
564 assert whitelist_view == \
590 assert whitelist_view == \
565 whitelist['api_access_controllers_whitelist']
591 whitelist['api_access_controllers_whitelist']
566
592
567 new_auth_token = AuthTokenModel().create(
593 new_auth_token = AuthTokenModel().create(
568 TEST_USER_ADMIN_LOGIN, 'test')
594 TEST_USER_ADMIN_LOGIN, 'test')
569 Session().commit()
595 Session().commit()
570 # patch the api key and make it expired
596 # patch the api key and make it expired
571 new_auth_token.expires = 0
597 new_auth_token.expires = 0
572 Session().add(new_auth_token)
598 Session().add(new_auth_token)
573 Session().commit()
599 Session().commit()
574 with fixture.anon_access(False):
600 with fixture.anon_access(False):
601 # webtest uses linter to check if response is bytes,
602 # and we use memoryview here as a wrapper, quick turn-off
603 self.app.lint = False
575 self.app.get(
604 self.app.get(
576 route_path('repo_commit_raw',
605 route_path('repo_commit_raw',
577 repo_name=HG_REPO, commit_id='tip',
606 repo_name=HG_REPO, commit_id='tip',
578 params=dict(api_key=new_auth_token.api_key)),
607 params=dict(api_key=new_auth_token.api_key)),
579 status=302)
608 status=302)
@@ -1,117 +1,119 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.lib import helpers as h
22 from rhodecode.lib import helpers as h
23 from rhodecode.tests import (
23 from rhodecode.tests import (
24 TestController, clear_cache_regions,
24 TestController, clear_cache_regions,
25 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
25 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
26 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.fixture import Fixture
27 from rhodecode.tests.utils import AssertResponse
27 from rhodecode.tests.utils import AssertResponse
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.request, urllib.parse, urllib.error
33 import urllib.request
34 import urllib.parse
35 import urllib.error
34 from rhodecode.apps._base import ADMIN_PREFIX
36 from rhodecode.apps._base import ADMIN_PREFIX
35
37
36 base_url = {
38 base_url = {
37 'login': ADMIN_PREFIX + '/login',
39 'login': ADMIN_PREFIX + '/login',
38 'logout': ADMIN_PREFIX + '/logout',
40 'logout': ADMIN_PREFIX + '/logout',
39 'register': ADMIN_PREFIX + '/register',
41 'register': ADMIN_PREFIX + '/register',
40 'reset_password':
42 'reset_password':
41 ADMIN_PREFIX + '/password_reset',
43 ADMIN_PREFIX + '/password_reset',
42 'reset_password_confirmation':
44 'reset_password_confirmation':
43 ADMIN_PREFIX + '/password_reset_confirmation',
45 ADMIN_PREFIX + '/password_reset_confirmation',
44
46
45 'admin_permissions_application':
47 'admin_permissions_application':
46 ADMIN_PREFIX + '/permissions/application',
48 ADMIN_PREFIX + '/permissions/application',
47 'admin_permissions_application_update':
49 'admin_permissions_application_update':
48 ADMIN_PREFIX + '/permissions/application/update',
50 ADMIN_PREFIX + '/permissions/application/update',
49 }[name].format(**kwargs)
51 }[name].format(**kwargs)
50
52
51 if params:
53 if params:
52 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
54 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
53 return base_url
55 return base_url
54
56
55
57
56 class TestPasswordReset(TestController):
58 class TestPasswordReset(TestController):
57
59
58 @pytest.mark.parametrize(
60 @pytest.mark.parametrize(
59 'pwd_reset_setting, show_link, show_reset', [
61 'pwd_reset_setting, show_link, show_reset', [
60 ('hg.password_reset.enabled', True, True),
62 ('hg.password_reset.enabled', True, True),
61 ('hg.password_reset.hidden', False, True),
63 ('hg.password_reset.hidden', False, True),
62 ('hg.password_reset.disabled', False, False),
64 ('hg.password_reset.disabled', False, False),
63 ])
65 ])
64 def test_password_reset_settings(
66 def test_password_reset_settings(
65 self, pwd_reset_setting, show_link, show_reset):
67 self, pwd_reset_setting, show_link, show_reset):
66 clear_cache_regions()
68 clear_cache_regions()
67 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
69 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
68 params = {
70 params = {
69 'csrf_token': self.csrf_token,
71 'csrf_token': self.csrf_token,
70 'anonymous': 'True',
72 'anonymous': 'True',
71 'default_register': 'hg.register.auto_activate',
73 'default_register': 'hg.register.auto_activate',
72 'default_register_message': '',
74 'default_register_message': '',
73 'default_password_reset': pwd_reset_setting,
75 'default_password_reset': pwd_reset_setting,
74 'default_extern_activate': 'hg.extern_activate.auto',
76 'default_extern_activate': 'hg.extern_activate.auto',
75 }
77 }
76 resp = self.app.post(
78 resp = self.app.post(
77 route_path('admin_permissions_application_update'), params=params)
79 route_path('admin_permissions_application_update'), params=params)
78 self.logout_user()
80 self.logout_user()
79
81
80 login_page = self.app.get(route_path('login'))
82 login_page = self.app.get(route_path('login'))
81 asr_login = AssertResponse(login_page)
83 asr_login = AssertResponse(login_page)
82
84
83 if show_link:
85 if show_link:
84 asr_login.one_element_exists('a.pwd_reset')
86 asr_login.one_element_exists('a.pwd_reset')
85 else:
87 else:
86 asr_login.no_element_exists('a.pwd_reset')
88 asr_login.no_element_exists('a.pwd_reset')
87
89
88 response = self.app.get(route_path('reset_password'))
90 response = self.app.get(route_path('reset_password'))
89
91
90 assert_response = response.assert_response()
92 assert_response = response.assert_response()
91 if show_reset:
93 if show_reset:
92 response.mustcontain('Send password reset email')
94 response.mustcontain('Send password reset email')
93 assert_response.one_element_exists('#email')
95 assert_response.one_element_exists('#email')
94 assert_response.one_element_exists('#send')
96 assert_response.one_element_exists('#send')
95 else:
97 else:
96 response.mustcontain('Password reset is disabled.')
98 response.mustcontain('Password reset is disabled.')
97 assert_response.no_element_exists('#email')
99 assert_response.no_element_exists('#email')
98 assert_response.no_element_exists('#send')
100 assert_response.no_element_exists('#send')
99
101
100 def test_password_form_disabled(self):
102 def test_password_form_disabled(self):
101 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
103 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
102 params = {
104 params = {
103 'csrf_token': self.csrf_token,
105 'csrf_token': self.csrf_token,
104 'anonymous': 'True',
106 'anonymous': 'True',
105 'default_register': 'hg.register.auto_activate',
107 'default_register': 'hg.register.auto_activate',
106 'default_register_message': '',
108 'default_register_message': '',
107 'default_password_reset': 'hg.password_reset.disabled',
109 'default_password_reset': 'hg.password_reset.disabled',
108 'default_extern_activate': 'hg.extern_activate.auto',
110 'default_extern_activate': 'hg.extern_activate.auto',
109 }
111 }
110 self.app.post(route_path('admin_permissions_application_update'), params=params)
112 self.app.post(route_path('admin_permissions_application_update'), params=params)
111 self.logout_user()
113 self.logout_user()
112
114
113 response = self.app.post(
115 response = self.app.post(
114 route_path('reset_password'), {'email': 'lisa@rhodecode.com',}
116 route_path('reset_password'), {'email': 'lisa@rhodecode.com',}
115 )
117 )
116 response = response.follow()
118 response = response.follow()
117 response.mustcontain('Password reset is disabled.')
119 response.mustcontain('Password reset is disabled.')
@@ -1,205 +1,208 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.apps._base import ADMIN_PREFIX
22 from rhodecode.apps._base import ADMIN_PREFIX
23 from rhodecode.tests import (
23 from rhodecode.tests import (
24 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
24 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
25 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
25 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
26 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.fixture import Fixture
27
27
28 from rhodecode.model.db import Notification, User
28 from rhodecode.model.db import Notification, User
29 from rhodecode.model.user import UserModel
29 from rhodecode.model.user import UserModel
30 from rhodecode.model.notification import NotificationModel
30 from rhodecode.model.notification import NotificationModel
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.request, urllib.parse, urllib.error
37 import urllib.request
38 import urllib.parse
39 import urllib.error
38 from rhodecode.apps._base import ADMIN_PREFIX
40 from rhodecode.apps._base import ADMIN_PREFIX
39
41
40 base_url = {
42 base_url = {
41 'notifications_show_all': ADMIN_PREFIX + '/notifications',
43 'notifications_show_all': ADMIN_PREFIX + '/notifications',
42 'notifications_mark_all_read': ADMIN_PREFIX + '/notifications_mark_all_read',
44 'notifications_mark_all_read': ADMIN_PREFIX + '/notifications_mark_all_read',
43 'notifications_show': ADMIN_PREFIX + '/notifications/{notification_id}',
45 'notifications_show': ADMIN_PREFIX + '/notifications/{notification_id}',
44 'notifications_update': ADMIN_PREFIX + '/notifications/{notification_id}/update',
46 'notifications_update': ADMIN_PREFIX + '/notifications/{notification_id}/update',
45 'notifications_delete': ADMIN_PREFIX + '/notifications/{notification_id}/delete',
47 'notifications_delete': ADMIN_PREFIX + '/notifications/{notification_id}/delete',
46
48
47 }[name].format(**kwargs)
49 }[name].format(**kwargs)
48
50
49 if params:
51 if params:
50 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
52 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
51 return base_url
53 return base_url
52
54
53
55
54 class TestNotificationsController(TestController):
56 class TestNotificationsController(TestController):
55
57
56 def teardown_method(self, method):
58 def teardown_method(self, method):
57 for n in Notification.query().all():
59 for n in Notification.query().all():
58 inst = Notification.get(n.notification_id)
60 inst = Notification.get(n.notification_id)
59 Session().delete(inst)
61 Session().delete(inst)
60 Session().commit()
62 Session().commit()
61
63
62 def test_mark_all_read(self, user_util):
64 def test_mark_all_read(self, user_util):
63 user = user_util.create_user(password='qweqwe')
65 user = user_util.create_user(password='qweqwe')
64 self.log_user(user.username, 'qweqwe')
66 self.log_user(user.username, 'qweqwe')
65
67
66 self.app.post(
68 self.app.post(
67 route_path('notifications_mark_all_read'), status=302,
69 route_path('notifications_mark_all_read'), status=302,
68 params={'csrf_token': self.csrf_token}
70 params={'csrf_token': self.csrf_token}
69 )
71 )
70
72
71 def test_show_all(self, user_util):
73 def test_show_all(self, user_util):
72 user = user_util.create_user(password='qweqwe')
74 user = user_util.create_user(password='qweqwe')
73 user_id = user.user_id
75 user_id = user.user_id
74 self.log_user(user.username, 'qweqwe')
76 self.log_user(user.username, 'qweqwe')
75
77
76 response = self.app.get(
78 response = self.app.get(
77 route_path('notifications_show_all', params={'type': 'all'}))
79 route_path('notifications_show_all', params={'type': 'all'}))
78 response.mustcontain(
80 response.mustcontain(
79 '<div class="table">No notifications here yet</div>')
81 '<div class="table">No notifications here yet</div>')
80
82
81 notification = NotificationModel().create(
83 notification = NotificationModel().create(
82 created_by=user_id, notification_subject=u'test_notification_1',
84 created_by=user_id, notification_subject=u'test_notification_1',
83 notification_body=u'notification_1', recipients=[user_id])
85 notification_body=u'notification_1', recipients=[user_id])
84 Session().commit()
86 Session().commit()
85 notification_id = notification.notification_id
87 notification_id = notification.notification_id
86
88
87 response = self.app.get(route_path('notifications_show_all',
89 response = self.app.get(route_path('notifications_show_all',
88 params={'type': 'all'}))
90 params={'type': 'all'}))
89 response.mustcontain('id="notification_%s"' % notification_id)
91 response.mustcontain('id="notification_%s"' % notification_id)
90
92
91 def test_show_unread(self, user_util):
93 def test_show_unread(self, user_util):
92 user = user_util.create_user(password='qweqwe')
94 user = user_util.create_user(password='qweqwe')
93 user_id = user.user_id
95 user_id = user.user_id
94 self.log_user(user.username, 'qweqwe')
96 self.log_user(user.username, 'qweqwe')
95
97
96 response = self.app.get(route_path('notifications_show_all'))
98 response = self.app.get(route_path('notifications_show_all'))
97 response.mustcontain(
99 response.mustcontain(
98 '<div class="table">No notifications here yet</div>')
100 '<div class="table">No notifications here yet</div>')
99
101
100 notification = NotificationModel().create(
102 notification = NotificationModel().create(
101 created_by=user_id, notification_subject=u'test_notification_1',
103 created_by=user_id, notification_subject=u'test_notification_1',
102 notification_body=u'notification_1', recipients=[user_id])
104 notification_body=u'notification_1', recipients=[user_id])
103
105
104 # mark the USER notification as unread
106 # mark the USER notification as unread
105 user_notification = NotificationModel().get_user_notification(
107 user_notification = NotificationModel().get_user_notification(
106 user_id, notification)
108 user_id, notification)
107 user_notification.read = False
109 user_notification.read = False
108
110
109 Session().commit()
111 Session().commit()
110 notification_id = notification.notification_id
112 notification_id = notification.notification_id
111
113
112 response = self.app.get(route_path('notifications_show_all'))
114 response = self.app.get(route_path('notifications_show_all'))
113 response.mustcontain('id="notification_%s"' % notification_id)
115 response.mustcontain('id="notification_%s"' % notification_id)
114 response.mustcontain('<div class="desc unread')
116 response.mustcontain('<div class="desc unread')
115
117
116 @pytest.mark.parametrize('user,password', [
118 @pytest.mark.parametrize('user,password', [
117 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
119 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
118 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
120 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
119 ])
121 ])
120 def test_delete(self, user, password, user_util):
122 def test_delete(self, user, password, user_util):
121 self.log_user(user, password)
123 self.log_user(user, password)
122 cur_user = self._get_logged_user()
124 cur_user = self._get_logged_user()
123
125
124 u1 = user_util.create_user()
126 u1 = user_util.create_user()
125 u2 = user_util.create_user()
127 u2 = user_util.create_user()
126
128
127 # make notifications
129 # make notifications
128 notification = NotificationModel().create(
130 notification = NotificationModel().create(
129 created_by=cur_user, notification_subject=u'test',
131 created_by=cur_user, notification_subject=u'test',
130 notification_body=u'hi there', recipients=[cur_user, u1, u2])
132 notification_body=u'hi there', recipients=[cur_user, u1, u2])
131 Session().commit()
133 Session().commit()
132 u1 = User.get(u1.user_id)
134 u1 = User.get(u1.user_id)
133 u2 = User.get(u2.user_id)
135 u2 = User.get(u2.user_id)
134
136
135 # check DB
137 # check DB
136 get_notif = lambda un: [x.notification for x in un]
138 def get_notif(un):
139 return [x.notification for x in un]
137 assert get_notif(cur_user.notifications) == [notification]
140 assert get_notif(cur_user.notifications) == [notification]
138 assert get_notif(u1.notifications) == [notification]
141 assert get_notif(u1.notifications) == [notification]
139 assert get_notif(u2.notifications) == [notification]
142 assert get_notif(u2.notifications) == [notification]
140 cur_usr_id = cur_user.user_id
143 cur_usr_id = cur_user.user_id
141
144
142 response = self.app.post(
145 response = self.app.post(
143 route_path('notifications_delete',
146 route_path('notifications_delete',
144 notification_id=notification.notification_id),
147 notification_id=notification.notification_id),
145 params={'csrf_token': self.csrf_token})
148 params={'csrf_token': self.csrf_token})
146 assert response.json == 'ok'
149 assert response.json == 'ok'
147
150
148 cur_user = User.get(cur_usr_id)
151 cur_user = User.get(cur_usr_id)
149 assert cur_user.notifications == []
152 assert cur_user.notifications == []
150
153
151 @pytest.mark.parametrize('user,password', [
154 @pytest.mark.parametrize('user,password', [
152 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
155 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
153 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
156 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
154 ])
157 ])
155 def test_show(self, user, password, user_util):
158 def test_show(self, user, password, user_util):
156 self.log_user(user, password)
159 self.log_user(user, password)
157 cur_user = self._get_logged_user()
160 cur_user = self._get_logged_user()
158 u1 = user_util.create_user()
161 u1 = user_util.create_user()
159 u2 = user_util.create_user()
162 u2 = user_util.create_user()
160
163
161 subject = u'test'
164 subject = u'test'
162 notif_body = u'hi there'
165 notif_body = u'hi there'
163 notification = NotificationModel().create(
166 notification = NotificationModel().create(
164 created_by=cur_user, notification_subject=subject,
167 created_by=cur_user, notification_subject=subject,
165 notification_body=notif_body, recipients=[cur_user, u1, u2])
168 notification_body=notif_body, recipients=[cur_user, u1, u2])
166 Session().commit()
169 Session().commit()
167
170
168 response = self.app.get(
171 response = self.app.get(
169 route_path('notifications_show',
172 route_path('notifications_show',
170 notification_id=notification.notification_id))
173 notification_id=notification.notification_id))
171
174
172 response.mustcontain(subject)
175 response.mustcontain(subject)
173 response.mustcontain(notif_body)
176 response.mustcontain(notif_body)
174
177
175 @pytest.mark.parametrize('user,password', [
178 @pytest.mark.parametrize('user,password', [
176 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
179 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
177 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
180 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
178 ])
181 ])
179 def test_update(self, user, password, user_util):
182 def test_update(self, user, password, user_util):
180 self.log_user(user, password)
183 self.log_user(user, password)
181 cur_user = self._get_logged_user()
184 cur_user = self._get_logged_user()
182 u1 = user_util.create_user()
185 u1 = user_util.create_user()
183 u2 = user_util.create_user()
186 u2 = user_util.create_user()
184
187
185 # make notifications
188 # make notifications
186 recipients = [cur_user, u1, u2]
189 recipients = [cur_user, u1, u2]
187 notification = NotificationModel().create(
190 notification = NotificationModel().create(
188 created_by=cur_user, notification_subject=u'test',
191 created_by=cur_user, notification_subject=u'test',
189 notification_body=u'hi there', recipients=recipients)
192 notification_body=u'hi there', recipients=recipients)
190 Session().commit()
193 Session().commit()
191
194
192 for u_obj in recipients:
195 for u_obj in recipients:
193 # if it's current user, he has his message already read
196 # if it's current user, he has his message already read
194 read = u_obj.username == user
197 read = u_obj.username == user
195 assert len(u_obj.notifications) == 1
198 assert len(u_obj.notifications) == 1
196 assert u_obj.notifications[0].read == read
199 assert u_obj.notifications[0].read == read
197
200
198 response = self.app.post(
201 response = self.app.post(
199 route_path('notifications_update',
202 route_path('notifications_update',
200 notification_id=notification.notification_id),
203 notification_id=notification.notification_id),
201 params={'csrf_token': self.csrf_token})
204 params={'csrf_token': self.csrf_token})
202 assert response.json == 'ok'
205 assert response.json == 'ok'
203
206
204 cur_user = self._get_logged_user()
207 cur_user = self._get_logged_user()
205 assert True is cur_user.notifications[0].read
208 assert True is cur_user.notifications[0].read
@@ -1,162 +1,164 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.model.db import User, UserSshKeys
22 from rhodecode.model.db import User, UserSshKeys
23
23
24 from rhodecode.tests import TestController, assert_session_flash
24 from rhodecode.tests import TestController, assert_session_flash
25 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
26
26
27 fixture = Fixture()
27 fixture = Fixture()
28
28
29
29
30 def route_path(name, params=None, **kwargs):
30 def route_path(name, params=None, **kwargs):
31 import urllib.request, urllib.parse, urllib.error
31 import urllib.request
32 import urllib.parse
33 import urllib.error
32 from rhodecode.apps._base import ADMIN_PREFIX
34 from rhodecode.apps._base import ADMIN_PREFIX
33
35
34 base_url = {
36 base_url = {
35 'my_account_ssh_keys':
37 'my_account_ssh_keys':
36 ADMIN_PREFIX + '/my_account/ssh_keys',
38 ADMIN_PREFIX + '/my_account/ssh_keys',
37 'my_account_ssh_keys_generate':
39 'my_account_ssh_keys_generate':
38 ADMIN_PREFIX + '/my_account/ssh_keys/generate',
40 ADMIN_PREFIX + '/my_account/ssh_keys/generate',
39 'my_account_ssh_keys_add':
41 'my_account_ssh_keys_add':
40 ADMIN_PREFIX + '/my_account/ssh_keys/new',
42 ADMIN_PREFIX + '/my_account/ssh_keys/new',
41 'my_account_ssh_keys_delete':
43 'my_account_ssh_keys_delete':
42 ADMIN_PREFIX + '/my_account/ssh_keys/delete',
44 ADMIN_PREFIX + '/my_account/ssh_keys/delete',
43 }[name].format(**kwargs)
45 }[name].format(**kwargs)
44
46
45 if params:
47 if params:
46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
47 return base_url
49 return base_url
48
50
49
51
50 class TestMyAccountSshKeysView(TestController):
52 class TestMyAccountSshKeysView(TestController):
51 INVALID_KEY = """\
53 INVALID_KEY = """\
52 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
54 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
53 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
55 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
54 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
56 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
55 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
57 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
56 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
58 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
57 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
59 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
58 your_email@example.com
60 your_email@example.com
59 """
61 """
60 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
62 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
61 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
63 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
62 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
64 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
63 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
65 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
64 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
66 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
65 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
67 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
66 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
68 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
67 'your_email@example.com'
69 'your_email@example.com'
68 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'
69
71
70 def test_add_ssh_key_error(self, user_util):
72 def test_add_ssh_key_error(self, user_util):
71 user = user_util.create_user(password='qweqwe')
73 user = user_util.create_user(password='qweqwe')
72 self.log_user(user.username, 'qweqwe')
74 self.log_user(user.username, 'qweqwe')
73
75
74 key_data = self.INVALID_KEY
76 key_data = self.INVALID_KEY
75
77
76 desc = 'MY SSH KEY'
78 desc = 'MY SSH KEY'
77 response = self.app.post(
79 response = self.app.post(
78 route_path('my_account_ssh_keys_add'),
80 route_path('my_account_ssh_keys_add'),
79 {'description': desc, 'key_data': key_data,
81 {'description': desc, 'key_data': key_data,
80 'csrf_token': self.csrf_token})
82 'csrf_token': self.csrf_token})
81 assert_session_flash(response, 'An error occurred during ssh '
83 assert_session_flash(response, 'An error occurred during ssh '
82 'key saving: Unable to decode the key')
84 'key saving: Unable to decode the key')
83
85
84 def test_ssh_key_duplicate(self, user_util):
86 def test_ssh_key_duplicate(self, user_util):
85 user = user_util.create_user(password='qweqwe')
87 user = user_util.create_user(password='qweqwe')
86 self.log_user(user.username, 'qweqwe')
88 self.log_user(user.username, 'qweqwe')
87 key_data = self.VALID_KEY
89 key_data = self.VALID_KEY
88
90
89 desc = 'MY SSH KEY'
91 desc = 'MY SSH KEY'
90 response = self.app.post(
92 response = self.app.post(
91 route_path('my_account_ssh_keys_add'),
93 route_path('my_account_ssh_keys_add'),
92 {'description': desc, 'key_data': key_data,
94 {'description': desc, 'key_data': key_data,
93 'csrf_token': self.csrf_token})
95 'csrf_token': self.csrf_token})
94 assert_session_flash(response, 'Ssh Key successfully created')
96 assert_session_flash(response, 'Ssh Key successfully created')
95 response.follow() # flush session flash
97 response.follow() # flush session flash
96
98
97 # add the same key AGAIN
99 # add the same key AGAIN
98 desc = 'MY SSH KEY'
100 desc = 'MY SSH KEY'
99 response = self.app.post(
101 response = self.app.post(
100 route_path('my_account_ssh_keys_add'),
102 route_path('my_account_ssh_keys_add'),
101 {'description': desc, 'key_data': key_data,
103 {'description': desc, 'key_data': key_data,
102 'csrf_token': self.csrf_token})
104 'csrf_token': self.csrf_token})
103
105
104 err = 'Such key with fingerprint `{}` already exists, ' \
106 err = 'Such key with fingerprint `{}` already exists, ' \
105 'please use a different one'.format(self.FINGERPRINT)
107 'please use a different one'.format(self.FINGERPRINT)
106 assert_session_flash(response, 'An error occurred during ssh key '
108 assert_session_flash(response, 'An error occurred during ssh key '
107 'saving: {}'.format(err))
109 'saving: {}'.format(err))
108
110
109 def test_add_ssh_key(self, user_util):
111 def test_add_ssh_key(self, user_util):
110 user = user_util.create_user(password='qweqwe')
112 user = user_util.create_user(password='qweqwe')
111 self.log_user(user.username, 'qweqwe')
113 self.log_user(user.username, 'qweqwe')
112
114
113 key_data = self.VALID_KEY
115 key_data = self.VALID_KEY
114
116
115 desc = 'MY SSH KEY'
117 desc = 'MY SSH KEY'
116 response = self.app.post(
118 response = self.app.post(
117 route_path('my_account_ssh_keys_add'),
119 route_path('my_account_ssh_keys_add'),
118 {'description': desc, 'key_data': key_data,
120 {'description': desc, 'key_data': key_data,
119 'csrf_token': self.csrf_token})
121 'csrf_token': self.csrf_token})
120 assert_session_flash(response, 'Ssh Key successfully created')
122 assert_session_flash(response, 'Ssh Key successfully created')
121
123
122 response = response.follow()
124 response = response.follow()
123 response.mustcontain(desc)
125 response.mustcontain(desc)
124
126
125 def test_delete_ssh_key(self, user_util):
127 def test_delete_ssh_key(self, user_util):
126 user = user_util.create_user(password='qweqwe')
128 user = user_util.create_user(password='qweqwe')
127 user_id = user.user_id
129 user_id = user.user_id
128 self.log_user(user.username, 'qweqwe')
130 self.log_user(user.username, 'qweqwe')
129
131
130 key_data = self.VALID_KEY
132 key_data = self.VALID_KEY
131
133
132 desc = 'MY SSH KEY'
134 desc = 'MY SSH KEY'
133 response = self.app.post(
135 response = self.app.post(
134 route_path('my_account_ssh_keys_add'),
136 route_path('my_account_ssh_keys_add'),
135 {'description': desc, 'key_data': key_data,
137 {'description': desc, 'key_data': key_data,
136 'csrf_token': self.csrf_token})
138 'csrf_token': self.csrf_token})
137 assert_session_flash(response, 'Ssh Key successfully created')
139 assert_session_flash(response, 'Ssh Key successfully created')
138 response = response.follow() # flush the Session flash
140 response = response.follow() # flush the Session flash
139
141
140 # now delete our key
142 # now delete our key
141 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
143 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
142 assert 1 == len(keys)
144 assert 1 == len(keys)
143
145
144 response = self.app.post(
146 response = self.app.post(
145 route_path('my_account_ssh_keys_delete'),
147 route_path('my_account_ssh_keys_delete'),
146 {'del_ssh_key': keys[0].ssh_key_id,
148 {'del_ssh_key': keys[0].ssh_key_id,
147 'csrf_token': self.csrf_token})
149 'csrf_token': self.csrf_token})
148
150
149 assert_session_flash(response, 'Ssh key successfully deleted')
151 assert_session_flash(response, 'Ssh key successfully deleted')
150 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
152 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
151 assert 0 == len(keys)
153 assert 0 == len(keys)
152
154
153 def test_generate_keypair(self, user_util):
155 def test_generate_keypair(self, user_util):
154 user = user_util.create_user(password='qweqwe')
156 user = user_util.create_user(password='qweqwe')
155 self.log_user(user.username, 'qweqwe')
157 self.log_user(user.username, 'qweqwe')
156
158
157 response = self.app.get(
159 response = self.app.get(
158 route_path('my_account_ssh_keys_generate'))
160 route_path('my_account_ssh_keys_generate'))
159
161
160 response.mustcontain('Private key')
162 response.mustcontain('Private key')
161 response.mustcontain('Public key')
163 response.mustcontain('Public key')
162 response.mustcontain('-----BEGIN PRIVATE KEY-----')
164 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,88 +1,90 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.tests import assert_session_flash
22 from rhodecode.tests import assert_session_flash
23
23
24
24
25 def route_path(name, params=None, **kwargs):
25 def route_path(name, params=None, **kwargs):
26 import urllib.request, urllib.parse, urllib.error
26 import urllib.request
27 import urllib.parse
28 import urllib.error
27
29
28 base_url = {
30 base_url = {
29 'edit_repo_group_advanced':
31 'edit_repo_group_advanced':
30 '/{repo_group_name}/_settings/advanced',
32 '/{repo_group_name}/_settings/advanced',
31 'edit_repo_group_advanced_delete':
33 'edit_repo_group_advanced_delete':
32 '/{repo_group_name}/_settings/advanced/delete',
34 '/{repo_group_name}/_settings/advanced/delete',
33 }[name].format(**kwargs)
35 }[name].format(**kwargs)
34
36
35 if params:
37 if params:
36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
37 return base_url
39 return base_url
38
40
39
41
40 @pytest.mark.usefixtures("app")
42 @pytest.mark.usefixtures("app")
41 class TestRepoGroupsAdvancedView(object):
43 class TestRepoGroupsAdvancedView(object):
42
44
43 @pytest.mark.parametrize('repo_group_name', [
45 @pytest.mark.parametrize('repo_group_name', [
44 'gro',
46 'gro',
45 '12345',
47 '12345',
46 ])
48 ])
47 def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
49 def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
48 user_util._test_name = repo_group_name
50 user_util._test_name = repo_group_name
49 gr = user_util.create_repo_group()
51 gr = user_util.create_repo_group()
50 self.app.get(
52 self.app.get(
51 route_path('edit_repo_group_advanced',
53 route_path('edit_repo_group_advanced',
52 repo_group_name=gr.group_name))
54 repo_group_name=gr.group_name))
53
55
54 def test_show_advanced_settings_delete(self, autologin_user, user_util,
56 def test_show_advanced_settings_delete(self, autologin_user, user_util,
55 csrf_token):
57 csrf_token):
56 gr = user_util.create_repo_group(auto_cleanup=False)
58 gr = user_util.create_repo_group(auto_cleanup=False)
57 repo_group_name = gr.group_name
59 repo_group_name = gr.group_name
58
60
59 params = dict(
61 params = dict(
60 csrf_token=csrf_token
62 csrf_token=csrf_token
61 )
63 )
62 response = self.app.post(
64 response = self.app.post(
63 route_path('edit_repo_group_advanced_delete',
65 route_path('edit_repo_group_advanced_delete',
64 repo_group_name=repo_group_name), params=params)
66 repo_group_name=repo_group_name), params=params)
65 assert_session_flash(
67 assert_session_flash(
66 response, 'Removed repository group `{}`'.format(repo_group_name))
68 response, 'Removed repository group `{}`'.format(repo_group_name))
67
69
68 def test_delete_not_possible_with_objects_inside(self, autologin_user,
70 def test_delete_not_possible_with_objects_inside(self, autologin_user,
69 repo_groups, csrf_token):
71 repo_groups, csrf_token):
70 zombie_group, parent_group, child_group = repo_groups
72 zombie_group, parent_group, child_group = repo_groups
71
73
72 response = self.app.get(
74 response = self.app.get(
73 route_path('edit_repo_group_advanced',
75 route_path('edit_repo_group_advanced',
74 repo_group_name=parent_group.group_name))
76 repo_group_name=parent_group.group_name))
75
77
76 response.mustcontain(
78 response.mustcontain(
77 'This repository group includes 1 children repository group')
79 'This repository group includes 1 children repository group')
78
80
79 params = dict(
81 params = dict(
80 csrf_token=csrf_token
82 csrf_token=csrf_token
81 )
83 )
82 response = self.app.post(
84 response = self.app.post(
83 route_path('edit_repo_group_advanced_delete',
85 route_path('edit_repo_group_advanced_delete',
84 repo_group_name=parent_group.group_name), params=params)
86 repo_group_name=parent_group.group_name), params=params)
85
87
86 assert_session_flash(
88 assert_session_flash(
87 response, 'This repository group contains 1 subgroup '
89 response, 'This repository group contains 1 subgroup '
88 'and cannot be deleted')
90 'and cannot be deleted')
@@ -1,85 +1,87 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.tests.utils import permission_update_data_generator
22 from rhodecode.tests.utils import permission_update_data_generator
23
23
24
24
25 def route_path(name, params=None, **kwargs):
25 def route_path(name, params=None, **kwargs):
26 import urllib.request, urllib.parse, urllib.error
26 import urllib.request
27 import urllib.parse
28 import urllib.error
27
29
28 base_url = {
30 base_url = {
29 'edit_repo_group_perms':
31 'edit_repo_group_perms':
30 '/{repo_group_name:}/_settings/permissions',
32 '/{repo_group_name:}/_settings/permissions',
31 'edit_repo_group_perms_update':
33 'edit_repo_group_perms_update':
32 '/{repo_group_name}/_settings/permissions/update',
34 '/{repo_group_name}/_settings/permissions/update',
33 }[name].format(**kwargs)
35 }[name].format(**kwargs)
34
36
35 if params:
37 if params:
36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
37 return base_url
39 return base_url
38
40
39
41
40 @pytest.mark.usefixtures("app")
42 @pytest.mark.usefixtures("app")
41 class TestRepoGroupPermissionsView(object):
43 class TestRepoGroupPermissionsView(object):
42
44
43 def test_edit_perms_view(self, user_util, autologin_user):
45 def test_edit_perms_view(self, user_util, autologin_user):
44 repo_group = user_util.create_repo_group()
46 repo_group = user_util.create_repo_group()
45
47
46 self.app.get(
48 self.app.get(
47 route_path('edit_repo_group_perms',
49 route_path('edit_repo_group_perms',
48 repo_group_name=repo_group.group_name), status=200)
50 repo_group_name=repo_group.group_name), status=200)
49
51
50 def test_update_permissions(self, csrf_token, user_util):
52 def test_update_permissions(self, csrf_token, user_util):
51 repo_group = user_util.create_repo_group()
53 repo_group = user_util.create_repo_group()
52 repo_group_name = repo_group.group_name
54 repo_group_name = repo_group.group_name
53 user = user_util.create_user()
55 user = user_util.create_user()
54 user_id = user.user_id
56 user_id = user.user_id
55 username = user.username
57 username = user.username
56
58
57 # grant new
59 # grant new
58 form_data = permission_update_data_generator(
60 form_data = permission_update_data_generator(
59 csrf_token,
61 csrf_token,
60 default='group.write',
62 default='group.write',
61 grant=[(user_id, 'group.write', username, 'user')])
63 grant=[(user_id, 'group.write', username, 'user')])
62
64
63 # recursive flag required for repo groups
65 # recursive flag required for repo groups
64 form_data.extend([('recursive', u'none')])
66 form_data.extend([('recursive', u'none')])
65
67
66 response = self.app.post(
68 response = self.app.post(
67 route_path('edit_repo_group_perms_update',
69 route_path('edit_repo_group_perms_update',
68 repo_group_name=repo_group_name), form_data).follow()
70 repo_group_name=repo_group_name), form_data).follow()
69
71
70 assert 'Repository Group permissions updated' in response
72 assert 'Repository Group permissions updated' in response
71
73
72 # revoke given
74 # revoke given
73 form_data = permission_update_data_generator(
75 form_data = permission_update_data_generator(
74 csrf_token,
76 csrf_token,
75 default='group.read',
77 default='group.read',
76 revoke=[(user_id, 'user')])
78 revoke=[(user_id, 'user')])
77
79
78 # recursive flag required for repo groups
80 # recursive flag required for repo groups
79 form_data.extend([('recursive', u'none')])
81 form_data.extend([('recursive', u'none')])
80
82
81 response = self.app.post(
83 response = self.app.post(
82 route_path('edit_repo_group_perms_update',
84 route_path('edit_repo_group_perms_update',
83 repo_group_name=repo_group_name), form_data).follow()
85 repo_group_name=repo_group_name), form_data).follow()
84
86
85 assert 'Repository Group permissions updated' in response
87 assert 'Repository Group permissions updated' in response
@@ -1,89 +1,91 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.tests import assert_session_flash
22 from rhodecode.tests import assert_session_flash
23
23
24
24
25 def route_path(name, params=None, **kwargs):
25 def route_path(name, params=None, **kwargs):
26 import urllib.request, urllib.parse, urllib.error
26 import urllib.request
27 import urllib.parse
28 import urllib.error
27
29
28 base_url = {
30 base_url = {
29 'edit_repo_group': '/{repo_group_name}/_edit',
31 'edit_repo_group': '/{repo_group_name}/_edit',
30 # Update is POST to the above url
32 # Update is POST to the above url
31 }[name].format(**kwargs)
33 }[name].format(**kwargs)
32
34
33 if params:
35 if params:
34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
35 return base_url
37 return base_url
36
38
37
39
38 @pytest.mark.usefixtures("app")
40 @pytest.mark.usefixtures("app")
39 class TestRepoGroupsSettingsView(object):
41 class TestRepoGroupsSettingsView(object):
40
42
41 @pytest.mark.parametrize('repo_group_name', [
43 @pytest.mark.parametrize('repo_group_name', [
42 'gro',
44 'gro',
43 u'12345',
45 u'12345',
44 ])
46 ])
45 def test_edit(self, user_util, autologin_user, repo_group_name):
47 def test_edit(self, user_util, autologin_user, repo_group_name):
46 user_util._test_name = repo_group_name
48 user_util._test_name = repo_group_name
47 repo_group = user_util.create_repo_group()
49 repo_group = user_util.create_repo_group()
48
50
49 self.app.get(
51 self.app.get(
50 route_path('edit_repo_group', repo_group_name=repo_group.group_name),
52 route_path('edit_repo_group', repo_group_name=repo_group.group_name),
51 status=200)
53 status=200)
52
54
53 def test_update(self, csrf_token, autologin_user, user_util, rc_fixture):
55 def test_update(self, csrf_token, autologin_user, user_util, rc_fixture):
54 repo_group = user_util.create_repo_group()
56 repo_group = user_util.create_repo_group()
55 repo_group_name = repo_group.group_name
57 repo_group_name = repo_group.group_name
56
58
57 description = 'description for newly created repo group'
59 description = 'description for newly created repo group'
58 form_data = rc_fixture._get_group_create_params(
60 form_data = rc_fixture._get_group_create_params(
59 group_name=repo_group.group_name,
61 group_name=repo_group.group_name,
60 group_description=description,
62 group_description=description,
61 csrf_token=csrf_token,
63 csrf_token=csrf_token,
62 repo_group_name=repo_group.group_name,
64 repo_group_name=repo_group.group_name,
63 repo_group_owner=repo_group.user.username)
65 repo_group_owner=repo_group.user.username)
64
66
65 response = self.app.post(
67 response = self.app.post(
66 route_path('edit_repo_group',
68 route_path('edit_repo_group',
67 repo_group_name=repo_group.group_name),
69 repo_group_name=repo_group.group_name),
68 form_data,
70 form_data,
69 status=302)
71 status=302)
70
72
71 assert_session_flash(
73 assert_session_flash(
72 response, 'Repository Group `{}` updated successfully'.format(
74 response, 'Repository Group `{}` updated successfully'.format(
73 repo_group_name))
75 repo_group_name))
74
76
75 def test_update_fails_when_parent_pointing_to_self(
77 def test_update_fails_when_parent_pointing_to_self(
76 self, csrf_token, user_util, autologin_user, rc_fixture):
78 self, csrf_token, user_util, autologin_user, rc_fixture):
77 group = user_util.create_repo_group()
79 group = user_util.create_repo_group()
78 response = self.app.post(
80 response = self.app.post(
79 route_path('edit_repo_group', repo_group_name=group.group_name),
81 route_path('edit_repo_group', repo_group_name=group.group_name),
80 rc_fixture._get_group_create_params(
82 rc_fixture._get_group_create_params(
81 repo_group_name=group.group_name,
83 repo_group_name=group.group_name,
82 repo_group_owner=group.user.username,
84 repo_group_owner=group.user.username,
83 repo_group=group.group_id,
85 repo_group=group.group_id,
84 csrf_token=csrf_token),
86 csrf_token=csrf_token),
85 status=200
87 status=200
86 )
88 )
87 response.mustcontain(
89 response.mustcontain(
88 '<span class="error-message">"{}" is not one of -1'.format(
90 '<span class="error-message">"{}" is not one of -1'.format(
89 group.group_id))
91 group.group_id))
@@ -1,83 +1,85 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21 from rhodecode.model.db import Repository
21 from rhodecode.model.db import Repository
22
22
23
23
24 def route_path(name, params=None, **kwargs):
24 def route_path(name, params=None, **kwargs):
25 import urllib.request, urllib.parse, urllib.error
25 import urllib.request
26 import urllib.parse
27 import urllib.error
26
28
27 base_url = {
29 base_url = {
28 'pullrequest_show_all': '/{repo_name}/pull-request',
30 'pullrequest_show_all': '/{repo_name}/pull-request',
29 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
31 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
30 }[name].format(**kwargs)
32 }[name].format(**kwargs)
31
33
32 if params:
34 if params:
33 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
35 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
34 return base_url
36 return base_url
35
37
36
38
37 @pytest.mark.backends("git", "hg")
39 @pytest.mark.backends("git", "hg")
38 @pytest.mark.usefixtures('autologin_user', 'app')
40 @pytest.mark.usefixtures('autologin_user', 'app')
39 class TestPullRequestList(object):
41 class TestPullRequestList(object):
40
42
41 @pytest.mark.parametrize('params, expected_title', [
43 @pytest.mark.parametrize('params, expected_title', [
42 ({'source': 0, 'closed': 1}, 'Closed'),
44 ({'source': 0, 'closed': 1}, 'Closed'),
43 ({'source': 0, 'my': 1}, 'Created by me'),
45 ({'source': 0, 'my': 1}, 'Created by me'),
44 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
46 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
45 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
47 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
46 ({'source': 1}, 'From this repo'),
48 ({'source': 1}, 'From this repo'),
47 ])
49 ])
48 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 def test_showing_list_page(self, backend, pr_util, params, expected_title):
49 pull_request = pr_util.create_pull_request()
51 pull_request = pr_util.create_pull_request()
50
52
51 response = self.app.get(
53 response = self.app.get(
52 route_path('pullrequest_show_all',
54 route_path('pullrequest_show_all',
53 repo_name=pull_request.target_repo.repo_name,
55 repo_name=pull_request.target_repo.repo_name,
54 params=params))
56 params=params))
55
57
56 assert_response = response.assert_response()
58 assert_response = response.assert_response()
57
59
58 element = assert_response.get_element('.title .active')
60 element = assert_response.get_element('.title .active')
59 element_text = element.text_content()
61 element_text = element.text_content()
60 assert expected_title == element_text
62 assert expected_title == element_text
61
63
62 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
64 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
63 pull_request = pr_util.create_pull_request()
65 pull_request = pr_util.create_pull_request()
64 response = self.app.get(
66 response = self.app.get(
65 route_path('pullrequest_show_all_data',
67 route_path('pullrequest_show_all_data',
66 repo_name=pull_request.target_repo.repo_name),
68 repo_name=pull_request.target_repo.repo_name),
67 extra_environ=xhr_header)
69 extra_environ=xhr_header)
68
70
69 assert response.json['recordsTotal'] == 1
71 assert response.json['recordsTotal'] == 1
70 assert response.json['data'][0]['description'] == 'Description'
72 assert response.json['data'][0]['description'] == 'Description'
71
73
72 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
74 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
73 xss_description = "<script>alert('Hi!')</script>"
75 xss_description = "<script>alert('Hi!')</script>"
74 pull_request = pr_util.create_pull_request(description=xss_description)
76 pull_request = pr_util.create_pull_request(description=xss_description)
75
77
76 response = self.app.get(
78 response = self.app.get(
77 route_path('pullrequest_show_all_data',
79 route_path('pullrequest_show_all_data',
78 repo_name=pull_request.target_repo.repo_name),
80 repo_name=pull_request.target_repo.repo_name),
79 extra_environ=xhr_header)
81 extra_environ=xhr_header)
80
82
81 assert response.json['recordsTotal'] == 1
83 assert response.json['recordsTotal'] == 1
82 assert response.json['data'][0]['description'] == \
84 assert response.json['data'][0]['description'] == \
83 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
85 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -1,51 +1,53 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21 from rhodecode.model.db import Repository
21 from rhodecode.model.db import Repository
22
22
23
23
24 def route_path(name, params=None, **kwargs):
24 def route_path(name, params=None, **kwargs):
25 import urllib.request, urllib.parse, urllib.error
25 import urllib.request
26 import urllib.parse
27 import urllib.error
26
28
27 base_url = {
29 base_url = {
28 'bookmarks_home': '/{repo_name}/bookmarks',
30 'bookmarks_home': '/{repo_name}/bookmarks',
29 }[name].format(**kwargs)
31 }[name].format(**kwargs)
30
32
31 if params:
33 if params:
32 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
33 return base_url
35 return base_url
34
36
35
37
36 @pytest.mark.usefixtures('autologin_user', 'app')
38 @pytest.mark.usefixtures('autologin_user', 'app')
37 class TestBookmarks(object):
39 class TestBookmarks(object):
38
40
39 def test_index(self, backend):
41 def test_index(self, backend):
40 if backend.alias == 'hg':
42 if backend.alias == 'hg':
41 response = self.app.get(
43 response = self.app.get(
42 route_path('bookmarks_home', repo_name=backend.repo_name))
44 route_path('bookmarks_home', repo_name=backend.repo_name))
43
45
44 repo = Repository.get_by_repo_name(backend.repo_name)
46 repo = Repository.get_by_repo_name(backend.repo_name)
45 for commit_id, obj_name in repo.scm_instance().bookmarks.items():
47 for commit_id, obj_name in repo.scm_instance().bookmarks.items():
46 assert commit_id in response
48 assert commit_id in response
47 assert obj_name in response
49 assert obj_name in response
48 else:
50 else:
49 self.app.get(
51 self.app.get(
50 route_path('bookmarks_home', repo_name=backend.repo_name),
52 route_path('bookmarks_home', repo_name=backend.repo_name),
51 status=404)
53 status=404)
@@ -1,47 +1,49 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21 from rhodecode.model.db import Repository
21 from rhodecode.model.db import Repository
22
22
23
23
24 def route_path(name, params=None, **kwargs):
24 def route_path(name, params=None, **kwargs):
25 import urllib.request, urllib.parse, urllib.error
25 import urllib.request
26 import urllib.parse
27 import urllib.error
26
28
27 base_url = {
29 base_url = {
28 'branches_home': '/{repo_name}/branches',
30 'branches_home': '/{repo_name}/branches',
29 }[name].format(**kwargs)
31 }[name].format(**kwargs)
30
32
31 if params:
33 if params:
32 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
33 return base_url
35 return base_url
34
36
35
37
36 @pytest.mark.usefixtures('autologin_user', 'app')
38 @pytest.mark.usefixtures('autologin_user', 'app')
37 class TestBranchesController(object):
39 class TestBranchesController(object):
38
40
39 def test_index(self, backend):
41 def test_index(self, backend):
40 response = self.app.get(
42 response = self.app.get(
41 route_path('branches_home', repo_name=backend.repo_name))
43 route_path('branches_home', repo_name=backend.repo_name))
42
44
43 repo = Repository.get_by_repo_name(backend.repo_name)
45 repo = Repository.get_by_repo_name(backend.repo_name)
44
46
45 for commit_id, obj_name in repo.scm_instance().branches.items():
47 for commit_id, obj_name in repo.scm_instance().branches.items():
46 assert commit_id in response
48 assert commit_id in response
47 assert obj_name in response
49 assert obj_name in response
@@ -1,219 +1,220 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import re
20 import re
21
21
22 import pytest
22 import pytest
23
23
24 from rhodecode.apps.repository.views.repo_changelog import DEFAULT_CHANGELOG_SIZE
24 from rhodecode.apps.repository.views.repo_changelog import DEFAULT_CHANGELOG_SIZE
25 from rhodecode.tests import TestController
25 from rhodecode.tests import TestController
26
26
27 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
27 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
28
28
29
29
30 def route_path(name, params=None, **kwargs):
30 def route_path(name, params=None, **kwargs):
31 import urllib.request, urllib.parse, urllib.error
31 import urllib.request
32 import urllib.parse
33 import urllib.error
32
34
33 base_url = {
35 base_url = {
34 'repo_changelog': '/{repo_name}/changelog',
36 'repo_changelog': '/{repo_name}/changelog',
35 'repo_commits': '/{repo_name}/commits',
37 'repo_commits': '/{repo_name}/commits',
36 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
38 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
37 'repo_commits_elements': '/{repo_name}/commits_elements',
39 'repo_commits_elements': '/{repo_name}/commits_elements',
38 }[name].format(**kwargs)
40 }[name].format(**kwargs)
39
41
40 if params:
42 if params:
41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 return base_url
44 return base_url
43
45
44
46
45 def assert_commits_on_page(response, indexes):
47 def assert_commits_on_page(response, indexes):
46 found_indexes = [int(idx) for idx in MATCH_HASH.findall(response.body)]
48 found_indexes = [int(idx) for idx in MATCH_HASH.findall(response.text)]
47 assert found_indexes == indexes
49 assert found_indexes == indexes
48
50
49
51
50 class TestChangelogController(TestController):
52 class TestChangelogController(TestController):
51
53
52 def test_commits_page(self, backend):
54 def test_commits_page(self, backend):
53 self.log_user()
55 self.log_user()
54 response = self.app.get(
56 response = self.app.get(
55 route_path('repo_commits', repo_name=backend.repo_name))
57 route_path('repo_commits', repo_name=backend.repo_name))
56
58
57 first_idx = -1
59 first_idx = -1
58 last_idx = -DEFAULT_CHANGELOG_SIZE
60 last_idx = -DEFAULT_CHANGELOG_SIZE
59 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
61 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
60
62
61 def test_changelog(self, backend):
63 def test_changelog(self, backend):
62 self.log_user()
64 self.log_user()
63 response = self.app.get(
65 response = self.app.get(
64 route_path('repo_changelog', repo_name=backend.repo_name))
66 route_path('repo_changelog', repo_name=backend.repo_name))
65
67
66 first_idx = -1
68 first_idx = -1
67 last_idx = -DEFAULT_CHANGELOG_SIZE
69 last_idx = -DEFAULT_CHANGELOG_SIZE
68 self.assert_commit_range_on_page(
70 self.assert_commit_range_on_page(
69 response, first_idx, last_idx, backend)
71 response, first_idx, last_idx, backend)
70
72
71 @pytest.mark.backends("hg", "git")
73 @pytest.mark.backends("hg", "git")
72 def test_changelog_filtered_by_branch(self, backend):
74 def test_changelog_filtered_by_branch(self, backend):
73 self.log_user()
75 self.log_user()
74 self.app.get(
76 self.app.get(
75 route_path('repo_changelog', repo_name=backend.repo_name,
77 route_path('repo_changelog', repo_name=backend.repo_name,
76 params=dict(branch=backend.default_branch_name)),
78 params=dict(branch=backend.default_branch_name)),
77 status=200)
79 status=200)
78
80
79 @pytest.mark.backends("hg", "git")
81 @pytest.mark.backends("hg", "git")
80 def test_commits_filtered_by_branch(self, backend):
82 def test_commits_filtered_by_branch(self, backend):
81 self.log_user()
83 self.log_user()
82 self.app.get(
84 self.app.get(
83 route_path('repo_commits', repo_name=backend.repo_name,
85 route_path('repo_commits', repo_name=backend.repo_name,
84 params=dict(branch=backend.default_branch_name)),
86 params=dict(branch=backend.default_branch_name)),
85 status=200)
87 status=200)
86
88
87 @pytest.mark.backends("svn")
89 @pytest.mark.backends("svn")
88 def test_changelog_filtered_by_branch_svn(self, autologin_user, backend):
90 def test_changelog_filtered_by_branch_svn(self, autologin_user, backend):
89 repo = backend['svn-simple-layout']
91 repo = backend['svn-simple-layout']
90 response = self.app.get(
92 response = self.app.get(
91 route_path('repo_changelog', repo_name=repo.repo_name,
93 route_path('repo_changelog', repo_name=repo.repo_name,
92 params=dict(branch='trunk')),
94 params=dict(branch='trunk')),
93 status=200)
95 status=200)
94
96
95 assert_commits_on_page(response, indexes=[15, 12, 7, 3, 2, 1])
97 assert_commits_on_page(response, indexes=[15, 12, 7, 3, 2, 1])
96
98
97 def test_commits_filtered_by_wrong_branch(self, backend):
99 def test_commits_filtered_by_wrong_branch(self, backend):
98 self.log_user()
100 self.log_user()
99 branch = 'wrong-branch-name'
101 branch = 'wrong-branch-name'
100 response = self.app.get(
102 response = self.app.get(
101 route_path('repo_commits', repo_name=backend.repo_name,
103 route_path('repo_commits', repo_name=backend.repo_name,
102 params=dict(branch=branch)),
104 params=dict(branch=branch)),
103 status=302)
105 status=302)
104 expected_url = '/{repo}/commits/{branch}'.format(
106 expected_url = '/{repo}/commits/{branch}'.format(
105 repo=backend.repo_name, branch=branch)
107 repo=backend.repo_name, branch=branch)
106 assert expected_url in response.location
108 assert expected_url in response.location
107 response = response.follow()
109 response = response.follow()
108 expected_warning = 'Branch {} is not found.'.format(branch)
110 expected_warning = 'Branch {} is not found.'.format(branch)
109 assert expected_warning in response.text
111 assert expected_warning in response.text
110
112
111 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
113 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
112 def test_changelog_filtered_by_branch_with_merges(
114 def test_changelog_filtered_by_branch_with_merges(self, autologin_user, backend):
113 self, autologin_user, backend):
114
115
115 # 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"
116 # 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
117 # which have a smaller index than commit "a1".
118 # which have a smaller index than commit "a1".
118 commits = [
119 commits = [
119 {'message': 'a'},
120 {'message': 'a'},
120 {'message': 'b', 'branch': 'b'},
121 {'message': 'b', 'branch': 'b'},
121 {'message': 'a1', 'parents': ['a']},
122 {'message': 'a1', 'parents': ['a']},
122 {'message': 'b1', 'branch': 'b', 'parents': ['b', 'a1']},
123 {'message': 'b1', 'branch': 'b', 'parents': ['b', 'a1']},
123 ]
124 ]
124 backend.create_repo(commits)
125 backend.create_repo(commits)
125
126
126 self.app.get(
127 self.app.get(
127 route_path('repo_changelog', repo_name=backend.repo_name,
128 route_path('repo_changelog', repo_name=backend.repo_name,
128 params=dict(branch='b')),
129 params=dict(branch='b')),
129 status=200)
130 status=200)
130
131
131 @pytest.mark.backends("hg")
132 @pytest.mark.backends("hg")
132 def test_commits_closed_branches(self, autologin_user, backend):
133 def test_commits_closed_branches(self, autologin_user, backend):
133 repo = backend['closed_branch']
134 repo = backend['closed_branch']
134 response = self.app.get(
135 response = self.app.get(
135 route_path('repo_commits', repo_name=repo.repo_name,
136 route_path('repo_commits', repo_name=repo.repo_name,
136 params=dict(branch='experimental')),
137 params=dict(branch='experimental')),
137 status=200)
138 status=200)
138
139
139 assert_commits_on_page(response, indexes=[3, 1])
140 assert_commits_on_page(response, indexes=[3, 1])
140
141
141 def test_changelog_pagination(self, backend):
142 def test_changelog_pagination(self, backend):
142 self.log_user()
143 self.log_user()
143 # pagination, walk up to page 6
144 # pagination, walk up to page 6
144 changelog_url = route_path(
145 changelog_url = route_path(
145 'repo_commits', repo_name=backend.repo_name)
146 'repo_commits', repo_name=backend.repo_name)
146
147
147 for page in range(1, 7):
148 for page in range(1, 7):
148 response = self.app.get(changelog_url, {'page': page})
149 response = self.app.get(changelog_url, {'page': page})
149
150
150 first_idx = -DEFAULT_CHANGELOG_SIZE * (page - 1) - 1
151 first_idx = -DEFAULT_CHANGELOG_SIZE * (page - 1) - 1
151 last_idx = -DEFAULT_CHANGELOG_SIZE * page
152 last_idx = -DEFAULT_CHANGELOG_SIZE * page
152 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)
153
154
154 def assert_commit_range_on_page(
155 def assert_commit_range_on_page(
155 self, response, first_idx, last_idx, backend):
156 self, response, first_idx, last_idx, backend):
156 input_template = (
157 input_template = (
157 """<input class="commit-range" """
158 """<input class="commit-range" """
158 """data-commit-id="%(raw_id)s" data-commit-idx="%(idx)s" """
159 """data-commit-id="%(raw_id)s" data-commit-idx="%(idx)s" """
159 """data-short-id="%(short_id)s" id="%(raw_id)s" """
160 """data-short-id="%(short_id)s" id="%(raw_id)s" """
160 """name="%(raw_id)s" type="checkbox" value="1" />"""
161 """name="%(raw_id)s" type="checkbox" value="1" />"""
161 )
162 )
162
163
163 commit_span_template = """<span class="commit_hash">r%s:%s</span>"""
164 commit_span_template = """<span class="commit_hash">r%s:%s</span>"""
164 repo = backend.repo
165 repo = backend.repo
165
166
166 first_commit_on_page = repo.get_commit(commit_idx=first_idx)
167 first_commit_on_page = repo.get_commit(commit_idx=first_idx)
167 response.mustcontain(
168 response.mustcontain(
168 input_template % {'raw_id': first_commit_on_page.raw_id,
169 input_template % {'raw_id': first_commit_on_page.raw_id,
169 'idx': first_commit_on_page.idx,
170 'idx': first_commit_on_page.idx,
170 'short_id': first_commit_on_page.short_id})
171 'short_id': first_commit_on_page.short_id})
171
172
172 response.mustcontain(commit_span_template % (
173 response.mustcontain(commit_span_template % (
173 first_commit_on_page.idx, first_commit_on_page.short_id)
174 first_commit_on_page.idx, first_commit_on_page.short_id)
174 )
175 )
175
176
176 last_commit_on_page = repo.get_commit(commit_idx=last_idx)
177 last_commit_on_page = repo.get_commit(commit_idx=last_idx)
177 response.mustcontain(
178 response.mustcontain(
178 input_template % {'raw_id': last_commit_on_page.raw_id,
179 input_template % {'raw_id': last_commit_on_page.raw_id,
179 'idx': last_commit_on_page.idx,
180 'idx': last_commit_on_page.idx,
180 'short_id': last_commit_on_page.short_id})
181 'short_id': last_commit_on_page.short_id})
181 response.mustcontain(commit_span_template % (
182 response.mustcontain(commit_span_template % (
182 last_commit_on_page.idx, last_commit_on_page.short_id)
183 last_commit_on_page.idx, last_commit_on_page.short_id)
183 )
184 )
184
185
185 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)
186 first_span_of_next_page = commit_span_template % (
187 first_span_of_next_page = commit_span_template % (
187 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)
188 assert first_span_of_next_page not in response
189 assert first_span_of_next_page not in response
189
190
190 @pytest.mark.parametrize('test_path', [
191 @pytest.mark.parametrize('test_path', [
191 'vcs/exceptions.py',
192 'vcs/exceptions.py',
192 '/vcs/exceptions.py',
193 '/vcs/exceptions.py',
193 '//vcs/exceptions.py'
194 '//vcs/exceptions.py'
194 ])
195 ])
195 def test_commits_with_filenode(self, backend, test_path):
196 def test_commits_with_filenode(self, backend, test_path):
196 self.log_user()
197 self.log_user()
197 response = self.app.get(
198 response = self.app.get(
198 route_path('repo_commits_file', repo_name=backend.repo_name,
199 route_path('repo_commits_file', repo_name=backend.repo_name,
199 commit_id='tip', f_path=test_path),
200 commit_id='tip', f_path=test_path),
200 )
201 )
201
202
202 # history commits messages
203 # history commits messages
203 response.mustcontain('Added exceptions module, this time for real')
204 response.mustcontain('Added exceptions module, this time for real')
204 response.mustcontain('Added not implemented hg backend test case')
205 response.mustcontain('Added not implemented hg backend test case')
205 response.mustcontain('Added BaseChangeset class')
206 response.mustcontain('Added BaseChangeset class')
206
207
207 def test_commits_with_filenode_that_is_dirnode(self, backend):
208 def test_commits_with_filenode_that_is_dirnode(self, backend):
208 self.log_user()
209 self.log_user()
209 self.app.get(
210 self.app.get(
210 route_path('repo_commits_file', repo_name=backend.repo_name,
211 route_path('repo_commits_file', repo_name=backend.repo_name,
211 commit_id='tip', f_path='/tests'),
212 commit_id='tip', f_path='/tests'),
212 status=302)
213 status=302)
213
214
214 def test_commits_with_filenode_not_existing(self, backend):
215 def test_commits_with_filenode_not_existing(self, backend):
215 self.log_user()
216 self.log_user()
216 self.app.get(
217 self.app.get(
217 route_path('repo_commits_file', repo_name=backend.repo_name,
218 route_path('repo_commits_file', repo_name=backend.repo_name,
218 commit_id='tip', f_path='wrong_path'),
219 commit_id='tip', f_path='wrong_path'),
219 status=302)
220 status=302)
@@ -1,493 +1,495 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.tests import TestController
22 from rhodecode.tests import TestController
23
23
24 from rhodecode.model.db import ChangesetComment, Notification
24 from rhodecode.model.db import ChangesetComment, Notification
25 from rhodecode.model.meta import Session
25 from rhodecode.model.meta import Session
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27
27
28
28
29 def route_path(name, params=None, **kwargs):
29 def route_path(name, params=None, **kwargs):
30 import urllib.request, urllib.parse, urllib.error
30 import urllib.request
31 import urllib.parse
32 import urllib.error
31
33
32 base_url = {
34 base_url = {
33 'repo_commit': '/{repo_name}/changeset/{commit_id}',
35 'repo_commit': '/{repo_name}/changeset/{commit_id}',
34 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
36 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
35 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
37 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
36 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
38 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
37 'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
39 'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
38 }[name].format(**kwargs)
40 }[name].format(**kwargs)
39
41
40 if params:
42 if params:
41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 return base_url
44 return base_url
43
45
44
46
45 @pytest.mark.backends("git", "hg", "svn")
47 @pytest.mark.backends("git", "hg", "svn")
46 class TestRepoCommitCommentsView(TestController):
48 class TestRepoCommitCommentsView(TestController):
47
49
48 @pytest.fixture(autouse=True)
50 @pytest.fixture(autouse=True)
49 def prepare(self, request, baseapp):
51 def prepare(self, request, baseapp):
50 for x in ChangesetComment.query().all():
52 for x in ChangesetComment.query().all():
51 Session().delete(x)
53 Session().delete(x)
52 Session().commit()
54 Session().commit()
53
55
54 for x in Notification.query().all():
56 for x in Notification.query().all():
55 Session().delete(x)
57 Session().delete(x)
56 Session().commit()
58 Session().commit()
57
59
58 request.addfinalizer(self.cleanup)
60 request.addfinalizer(self.cleanup)
59
61
60 def cleanup(self):
62 def cleanup(self):
61 for x in ChangesetComment.query().all():
63 for x in ChangesetComment.query().all():
62 Session().delete(x)
64 Session().delete(x)
63 Session().commit()
65 Session().commit()
64
66
65 for x in Notification.query().all():
67 for x in Notification.query().all():
66 Session().delete(x)
68 Session().delete(x)
67 Session().commit()
69 Session().commit()
68
70
69 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
71 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
70 def test_create(self, comment_type, backend):
72 def test_create(self, comment_type, backend):
71 self.log_user()
73 self.log_user()
72 commit = backend.repo.get_commit('300')
74 commit = backend.repo.get_commit('300')
73 commit_id = commit.raw_id
75 commit_id = commit.raw_id
74 text = u'CommentOnCommit'
76 text = 'CommentOnCommit'
75
77
76 params = {'text': text, 'csrf_token': self.csrf_token,
78 params = {'text': text, 'csrf_token': self.csrf_token,
77 'comment_type': comment_type}
79 'comment_type': comment_type}
78 self.app.post(
80 self.app.post(
79 route_path('repo_commit_comment_create',
81 route_path('repo_commit_comment_create',
80 repo_name=backend.repo_name, commit_id=commit_id),
82 repo_name=backend.repo_name, commit_id=commit_id),
81 params=params)
83 params=params)
82
84
83 response = self.app.get(
85 response = self.app.get(
84 route_path('repo_commit',
86 route_path('repo_commit',
85 repo_name=backend.repo_name, commit_id=commit_id))
87 repo_name=backend.repo_name, commit_id=commit_id))
86
88
87 # test DB
89 # test DB
88 assert ChangesetComment.query().count() == 1
90 assert ChangesetComment.query().count() == 1
89 assert_comment_links(response, ChangesetComment.query().count(), 0)
91 assert_comment_links(response, ChangesetComment.query().count(), 0)
90
92
91 assert Notification.query().count() == 1
93 assert Notification.query().count() == 1
92 assert ChangesetComment.query().count() == 1
94 assert ChangesetComment.query().count() == 1
93
95
94 notification = Notification.query().all()[0]
96 notification = Notification.query().all()[0]
95
97
96 comment_id = ChangesetComment.query().first().comment_id
98 comment_id = ChangesetComment.query().first().comment_id
97 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
99 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
98
100
99 author = notification.created_by_user.username_and_name
101 author = notification.created_by_user.username_and_name
100 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
102 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
101 author, comment_type, h.show_id(commit), backend.repo_name)
103 author, comment_type, h.show_id(commit), backend.repo_name)
102 assert sbj == notification.subject
104 assert sbj == notification.subject
103
105
104 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
106 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
105 backend.repo_name, commit_id, comment_id))
107 backend.repo_name, commit_id, comment_id))
106 assert lnk in notification.body
108 assert lnk in notification.body
107
109
108 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
110 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
109 def test_create_inline(self, comment_type, backend):
111 def test_create_inline(self, comment_type, backend):
110 self.log_user()
112 self.log_user()
111 commit = backend.repo.get_commit('300')
113 commit = backend.repo.get_commit('300')
112 commit_id = commit.raw_id
114 commit_id = commit.raw_id
113 text = u'CommentOnCommit'
115 text = 'CommentOnCommit'
114 f_path = 'vcs/web/simplevcs/views/repository.py'
116 f_path = 'vcs/web/simplevcs/views/repository.py'
115 line = 'n1'
117 line = 'n1'
116
118
117 params = {'text': text, 'f_path': f_path, 'line': line,
119 params = {'text': text, 'f_path': f_path, 'line': line,
118 'comment_type': comment_type,
120 'comment_type': comment_type,
119 'csrf_token': self.csrf_token}
121 'csrf_token': self.csrf_token}
120
122
121 self.app.post(
123 self.app.post(
122 route_path('repo_commit_comment_create',
124 route_path('repo_commit_comment_create',
123 repo_name=backend.repo_name, commit_id=commit_id),
125 repo_name=backend.repo_name, commit_id=commit_id),
124 params=params)
126 params=params)
125
127
126 response = self.app.get(
128 response = self.app.get(
127 route_path('repo_commit',
129 route_path('repo_commit',
128 repo_name=backend.repo_name, commit_id=commit_id))
130 repo_name=backend.repo_name, commit_id=commit_id))
129
131
130 # test DB
132 # test DB
131 assert ChangesetComment.query().count() == 1
133 assert ChangesetComment.query().count() == 1
132 assert_comment_links(response, 0, ChangesetComment.query().count())
134 assert_comment_links(response, 0, ChangesetComment.query().count())
133
135
134 if backend.alias == 'svn':
136 if backend.alias == 'svn':
135 response.mustcontain(
137 response.mustcontain(
136 '''data-f-path="vcs/commands/summary.py" '''
138 '''data-f-path="vcs/commands/summary.py" '''
137 '''data-anchor-id="c-300-ad05457a43f8"'''
139 '''data-anchor-id="c-300-ad05457a43f8"'''
138 )
140 )
139 if backend.alias == 'git':
141 if backend.alias == 'git':
140 response.mustcontain(
142 response.mustcontain(
141 '''data-f-path="vcs/backends/hg.py" '''
143 '''data-f-path="vcs/backends/hg.py" '''
142 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
144 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
143 )
145 )
144
146
145 if backend.alias == 'hg':
147 if backend.alias == 'hg':
146 response.mustcontain(
148 response.mustcontain(
147 '''data-f-path="vcs/backends/hg.py" '''
149 '''data-f-path="vcs/backends/hg.py" '''
148 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
150 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
149 )
151 )
150
152
151 assert Notification.query().count() == 1
153 assert Notification.query().count() == 1
152 assert ChangesetComment.query().count() == 1
154 assert ChangesetComment.query().count() == 1
153
155
154 notification = Notification.query().all()[0]
156 notification = Notification.query().all()[0]
155 comment = ChangesetComment.query().first()
157 comment = ChangesetComment.query().first()
156 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
158 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
157
159
158 assert comment.revision == commit_id
160 assert comment.revision == commit_id
159
161
160 author = notification.created_by_user.username_and_name
162 author = notification.created_by_user.username_and_name
161 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
163 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
162 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
164 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
163
165
164 assert sbj == notification.subject
166 assert sbj == notification.subject
165
167
166 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
168 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
167 backend.repo_name, commit_id, comment.comment_id))
169 backend.repo_name, commit_id, comment.comment_id))
168 assert lnk in notification.body
170 assert lnk in notification.body
169 assert 'on line n1' in notification.body
171 assert 'on line n1' in notification.body
170
172
171 def test_create_with_mention(self, backend):
173 def test_create_with_mention(self, backend):
172 self.log_user()
174 self.log_user()
173
175
174 commit_id = backend.repo.get_commit('300').raw_id
176 commit_id = backend.repo.get_commit('300').raw_id
175 text = u'@test_regular check CommentOnCommit'
177 text = '@test_regular check CommentOnCommit'
176
178
177 params = {'text': text, 'csrf_token': self.csrf_token}
179 params = {'text': text, 'csrf_token': self.csrf_token}
178 self.app.post(
180 self.app.post(
179 route_path('repo_commit_comment_create',
181 route_path('repo_commit_comment_create',
180 repo_name=backend.repo_name, commit_id=commit_id),
182 repo_name=backend.repo_name, commit_id=commit_id),
181 params=params)
183 params=params)
182
184
183 response = self.app.get(
185 response = self.app.get(
184 route_path('repo_commit',
186 route_path('repo_commit',
185 repo_name=backend.repo_name, commit_id=commit_id))
187 repo_name=backend.repo_name, commit_id=commit_id))
186 # test DB
188 # test DB
187 assert ChangesetComment.query().count() == 1
189 assert ChangesetComment.query().count() == 1
188 assert_comment_links(response, ChangesetComment.query().count(), 0)
190 assert_comment_links(response, ChangesetComment.query().count(), 0)
189
191
190 notification = Notification.query().one()
192 notification = Notification.query().one()
191
193
192 assert len(notification.recipients) == 2
194 assert len(notification.recipients) == 2
193 users = [x.username for x in notification.recipients]
195 users = [x.username for x in notification.recipients]
194
196
195 # test_regular gets notification by @mention
197 # test_regular gets notification by @mention
196 assert sorted(users) == [u'test_admin', u'test_regular']
198 assert sorted(users) == ['test_admin', 'test_regular']
197
199
198 def test_create_with_status_change(self, backend):
200 def test_create_with_status_change(self, backend):
199 self.log_user()
201 self.log_user()
200 commit = backend.repo.get_commit('300')
202 commit = backend.repo.get_commit('300')
201 commit_id = commit.raw_id
203 commit_id = commit.raw_id
202 text = u'CommentOnCommit'
204 text = 'CommentOnCommit'
203 f_path = 'vcs/web/simplevcs/views/repository.py'
205 f_path = 'vcs/web/simplevcs/views/repository.py'
204 line = 'n1'
206 line = 'n1'
205
207
206 params = {'text': text, 'changeset_status': 'approved',
208 params = {'text': text, 'changeset_status': 'approved',
207 'csrf_token': self.csrf_token}
209 'csrf_token': self.csrf_token}
208
210
209 self.app.post(
211 self.app.post(
210 route_path(
212 route_path(
211 'repo_commit_comment_create',
213 'repo_commit_comment_create',
212 repo_name=backend.repo_name, commit_id=commit_id),
214 repo_name=backend.repo_name, commit_id=commit_id),
213 params=params)
215 params=params)
214
216
215 response = self.app.get(
217 response = self.app.get(
216 route_path('repo_commit',
218 route_path('repo_commit',
217 repo_name=backend.repo_name, commit_id=commit_id))
219 repo_name=backend.repo_name, commit_id=commit_id))
218
220
219 # test DB
221 # test DB
220 assert ChangesetComment.query().count() == 1
222 assert ChangesetComment.query().count() == 1
221 assert_comment_links(response, ChangesetComment.query().count(), 0)
223 assert_comment_links(response, ChangesetComment.query().count(), 0)
222
224
223 assert Notification.query().count() == 1
225 assert Notification.query().count() == 1
224 assert ChangesetComment.query().count() == 1
226 assert ChangesetComment.query().count() == 1
225
227
226 notification = Notification.query().all()[0]
228 notification = Notification.query().all()[0]
227
229
228 comment_id = ChangesetComment.query().first().comment_id
230 comment_id = ChangesetComment.query().first().comment_id
229 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
231 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
230
232
231 author = notification.created_by_user.username_and_name
233 author = notification.created_by_user.username_and_name
232 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
234 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
233 author, h.show_id(commit), backend.repo_name)
235 author, h.show_id(commit), backend.repo_name)
234 assert sbj == notification.subject
236 assert sbj == notification.subject
235
237
236 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
238 lnk = ('/{0}/changeset/{1}#comment-{2}'.format(
237 backend.repo_name, commit_id, comment_id))
239 backend.repo_name, commit_id, comment_id))
238 assert lnk in notification.body
240 assert lnk in notification.body
239
241
240 def test_delete(self, backend):
242 def test_delete(self, backend):
241 self.log_user()
243 self.log_user()
242 commit_id = backend.repo.get_commit('300').raw_id
244 commit_id = backend.repo.get_commit('300').raw_id
243 text = u'CommentOnCommit'
245 text = 'CommentOnCommit'
244
246
245 params = {'text': text, 'csrf_token': self.csrf_token}
247 params = {'text': text, 'csrf_token': self.csrf_token}
246 self.app.post(
248 self.app.post(
247 route_path(
249 route_path(
248 'repo_commit_comment_create',
250 'repo_commit_comment_create',
249 repo_name=backend.repo_name, commit_id=commit_id),
251 repo_name=backend.repo_name, commit_id=commit_id),
250 params=params)
252 params=params)
251
253
252 comments = ChangesetComment.query().all()
254 comments = ChangesetComment.query().all()
253 assert len(comments) == 1
255 assert len(comments) == 1
254 comment_id = comments[0].comment_id
256 comment_id = comments[0].comment_id
255
257
256 self.app.post(
258 self.app.post(
257 route_path('repo_commit_comment_delete',
259 route_path('repo_commit_comment_delete',
258 repo_name=backend.repo_name,
260 repo_name=backend.repo_name,
259 commit_id=commit_id,
261 commit_id=commit_id,
260 comment_id=comment_id),
262 comment_id=comment_id),
261 params={'csrf_token': self.csrf_token})
263 params={'csrf_token': self.csrf_token})
262
264
263 comments = ChangesetComment.query().all()
265 comments = ChangesetComment.query().all()
264 assert len(comments) == 0
266 assert len(comments) == 0
265
267
266 response = self.app.get(
268 response = self.app.get(
267 route_path('repo_commit',
269 route_path('repo_commit',
268 repo_name=backend.repo_name, commit_id=commit_id))
270 repo_name=backend.repo_name, commit_id=commit_id))
269 assert_comment_links(response, 0, 0)
271 assert_comment_links(response, 0, 0)
270
272
271 def test_edit(self, backend):
273 def test_edit(self, backend):
272 self.log_user()
274 self.log_user()
273 commit_id = backend.repo.get_commit('300').raw_id
275 commit_id = backend.repo.get_commit('300').raw_id
274 text = u'CommentOnCommit'
276 text = 'CommentOnCommit'
275
277
276 params = {'text': text, 'csrf_token': self.csrf_token}
278 params = {'text': text, 'csrf_token': self.csrf_token}
277 self.app.post(
279 self.app.post(
278 route_path(
280 route_path(
279 'repo_commit_comment_create',
281 'repo_commit_comment_create',
280 repo_name=backend.repo_name, commit_id=commit_id),
282 repo_name=backend.repo_name, commit_id=commit_id),
281 params=params)
283 params=params)
282
284
283 comments = ChangesetComment.query().all()
285 comments = ChangesetComment.query().all()
284 assert len(comments) == 1
286 assert len(comments) == 1
285 comment_id = comments[0].comment_id
287 comment_id = comments[0].comment_id
286 test_text = 'test_text'
288 test_text = 'test_text'
287 self.app.post(
289 self.app.post(
288 route_path(
290 route_path(
289 'repo_commit_comment_edit',
291 'repo_commit_comment_edit',
290 repo_name=backend.repo_name,
292 repo_name=backend.repo_name,
291 commit_id=commit_id,
293 commit_id=commit_id,
292 comment_id=comment_id,
294 comment_id=comment_id,
293 ),
295 ),
294 params={
296 params={
295 'csrf_token': self.csrf_token,
297 'csrf_token': self.csrf_token,
296 'text': test_text,
298 'text': test_text,
297 'version': '0',
299 'version': '0',
298 })
300 })
299
301
300 text_form_db = ChangesetComment.query().filter(
302 text_form_db = ChangesetComment.query().filter(
301 ChangesetComment.comment_id == comment_id).first().text
303 ChangesetComment.comment_id == comment_id).first().text
302 assert test_text == text_form_db
304 assert test_text == text_form_db
303
305
304 def test_edit_without_change(self, backend):
306 def test_edit_without_change(self, backend):
305 self.log_user()
307 self.log_user()
306 commit_id = backend.repo.get_commit('300').raw_id
308 commit_id = backend.repo.get_commit('300').raw_id
307 text = u'CommentOnCommit'
309 text = 'CommentOnCommit'
308
310
309 params = {'text': text, 'csrf_token': self.csrf_token}
311 params = {'text': text, 'csrf_token': self.csrf_token}
310 self.app.post(
312 self.app.post(
311 route_path(
313 route_path(
312 'repo_commit_comment_create',
314 'repo_commit_comment_create',
313 repo_name=backend.repo_name, commit_id=commit_id),
315 repo_name=backend.repo_name, commit_id=commit_id),
314 params=params)
316 params=params)
315
317
316 comments = ChangesetComment.query().all()
318 comments = ChangesetComment.query().all()
317 assert len(comments) == 1
319 assert len(comments) == 1
318 comment_id = comments[0].comment_id
320 comment_id = comments[0].comment_id
319
321
320 response = self.app.post(
322 response = self.app.post(
321 route_path(
323 route_path(
322 'repo_commit_comment_edit',
324 'repo_commit_comment_edit',
323 repo_name=backend.repo_name,
325 repo_name=backend.repo_name,
324 commit_id=commit_id,
326 commit_id=commit_id,
325 comment_id=comment_id,
327 comment_id=comment_id,
326 ),
328 ),
327 params={
329 params={
328 'csrf_token': self.csrf_token,
330 'csrf_token': self.csrf_token,
329 'text': text,
331 'text': text,
330 'version': '0',
332 'version': '0',
331 },
333 },
332 status=404,
334 status=404,
333 )
335 )
334 assert response.status_int == 404
336 assert response.status_int == 404
335
337
336 def test_edit_try_edit_already_edited(self, backend):
338 def test_edit_try_edit_already_edited(self, backend):
337 self.log_user()
339 self.log_user()
338 commit_id = backend.repo.get_commit('300').raw_id
340 commit_id = backend.repo.get_commit('300').raw_id
339 text = u'CommentOnCommit'
341 text = 'CommentOnCommit'
340
342
341 params = {'text': text, 'csrf_token': self.csrf_token}
343 params = {'text': text, 'csrf_token': self.csrf_token}
342 self.app.post(
344 self.app.post(
343 route_path(
345 route_path(
344 'repo_commit_comment_create',
346 'repo_commit_comment_create',
345 repo_name=backend.repo_name, commit_id=commit_id
347 repo_name=backend.repo_name, commit_id=commit_id
346 ),
348 ),
347 params=params,
349 params=params,
348 )
350 )
349
351
350 comments = ChangesetComment.query().all()
352 comments = ChangesetComment.query().all()
351 assert len(comments) == 1
353 assert len(comments) == 1
352 comment_id = comments[0].comment_id
354 comment_id = comments[0].comment_id
353 test_text = 'test_text'
355 test_text = 'test_text'
354 self.app.post(
356 self.app.post(
355 route_path(
357 route_path(
356 'repo_commit_comment_edit',
358 'repo_commit_comment_edit',
357 repo_name=backend.repo_name,
359 repo_name=backend.repo_name,
358 commit_id=commit_id,
360 commit_id=commit_id,
359 comment_id=comment_id,
361 comment_id=comment_id,
360 ),
362 ),
361 params={
363 params={
362 'csrf_token': self.csrf_token,
364 'csrf_token': self.csrf_token,
363 'text': test_text,
365 'text': test_text,
364 'version': '0',
366 'version': '0',
365 }
367 }
366 )
368 )
367 test_text_v2 = 'test_v2'
369 test_text_v2 = 'test_v2'
368 response = self.app.post(
370 response = self.app.post(
369 route_path(
371 route_path(
370 'repo_commit_comment_edit',
372 'repo_commit_comment_edit',
371 repo_name=backend.repo_name,
373 repo_name=backend.repo_name,
372 commit_id=commit_id,
374 commit_id=commit_id,
373 comment_id=comment_id,
375 comment_id=comment_id,
374 ),
376 ),
375 params={
377 params={
376 'csrf_token': self.csrf_token,
378 'csrf_token': self.csrf_token,
377 'text': test_text_v2,
379 'text': test_text_v2,
378 'version': '0',
380 'version': '0',
379 },
381 },
380 status=409,
382 status=409,
381 )
383 )
382 assert response.status_int == 409
384 assert response.status_int == 409
383
385
384 text_form_db = ChangesetComment.query().filter(
386 text_form_db = ChangesetComment.query().filter(
385 ChangesetComment.comment_id == comment_id).first().text
387 ChangesetComment.comment_id == comment_id).first().text
386
388
387 assert test_text == text_form_db
389 assert test_text == text_form_db
388 assert test_text_v2 != text_form_db
390 assert test_text_v2 != text_form_db
389
391
390 def test_edit_forbidden_for_immutable_comments(self, backend):
392 def test_edit_forbidden_for_immutable_comments(self, backend):
391 self.log_user()
393 self.log_user()
392 commit_id = backend.repo.get_commit('300').raw_id
394 commit_id = backend.repo.get_commit('300').raw_id
393 text = u'CommentOnCommit'
395 text = 'CommentOnCommit'
394
396
395 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
397 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
396 self.app.post(
398 self.app.post(
397 route_path(
399 route_path(
398 'repo_commit_comment_create',
400 'repo_commit_comment_create',
399 repo_name=backend.repo_name,
401 repo_name=backend.repo_name,
400 commit_id=commit_id,
402 commit_id=commit_id,
401 ),
403 ),
402 params=params
404 params=params
403 )
405 )
404
406
405 comments = ChangesetComment.query().all()
407 comments = ChangesetComment.query().all()
406 assert len(comments) == 1
408 assert len(comments) == 1
407 comment_id = comments[0].comment_id
409 comment_id = comments[0].comment_id
408
410
409 comment = ChangesetComment.get(comment_id)
411 comment = ChangesetComment.get(comment_id)
410 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
412 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
411 Session().add(comment)
413 Session().add(comment)
412 Session().commit()
414 Session().commit()
413
415
414 response = self.app.post(
416 response = self.app.post(
415 route_path(
417 route_path(
416 'repo_commit_comment_edit',
418 'repo_commit_comment_edit',
417 repo_name=backend.repo_name,
419 repo_name=backend.repo_name,
418 commit_id=commit_id,
420 commit_id=commit_id,
419 comment_id=comment_id,
421 comment_id=comment_id,
420 ),
422 ),
421 params={
423 params={
422 'csrf_token': self.csrf_token,
424 'csrf_token': self.csrf_token,
423 'text': 'test_text',
425 'text': 'test_text',
424 },
426 },
425 status=403,
427 status=403,
426 )
428 )
427 assert response.status_int == 403
429 assert response.status_int == 403
428
430
429 def test_delete_forbidden_for_immutable_comments(self, backend):
431 def test_delete_forbidden_for_immutable_comments(self, backend):
430 self.log_user()
432 self.log_user()
431 commit_id = backend.repo.get_commit('300').raw_id
433 commit_id = backend.repo.get_commit('300').raw_id
432 text = u'CommentOnCommit'
434 text = 'CommentOnCommit'
433
435
434 params = {'text': text, 'csrf_token': self.csrf_token}
436 params = {'text': text, 'csrf_token': self.csrf_token}
435 self.app.post(
437 self.app.post(
436 route_path(
438 route_path(
437 'repo_commit_comment_create',
439 'repo_commit_comment_create',
438 repo_name=backend.repo_name, commit_id=commit_id),
440 repo_name=backend.repo_name, commit_id=commit_id),
439 params=params)
441 params=params)
440
442
441 comments = ChangesetComment.query().all()
443 comments = ChangesetComment.query().all()
442 assert len(comments) == 1
444 assert len(comments) == 1
443 comment_id = comments[0].comment_id
445 comment_id = comments[0].comment_id
444
446
445 comment = ChangesetComment.get(comment_id)
447 comment = ChangesetComment.get(comment_id)
446 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
448 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
447 Session().add(comment)
449 Session().add(comment)
448 Session().commit()
450 Session().commit()
449
451
450 self.app.post(
452 self.app.post(
451 route_path('repo_commit_comment_delete',
453 route_path('repo_commit_comment_delete',
452 repo_name=backend.repo_name,
454 repo_name=backend.repo_name,
453 commit_id=commit_id,
455 commit_id=commit_id,
454 comment_id=comment_id),
456 comment_id=comment_id),
455 params={'csrf_token': self.csrf_token},
457 params={'csrf_token': self.csrf_token},
456 status=403)
458 status=403)
457
459
458 @pytest.mark.parametrize('renderer, text_input, output', [
460 @pytest.mark.parametrize('renderer, text_input, output', [
459 ('rst', 'plain text', '<p>plain text</p>'),
461 ('rst', 'plain text', '<p>plain text</p>'),
460 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
462 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
461 ('rst', '*italics*', '<em>italics</em>'),
463 ('rst', '*italics*', '<em>italics</em>'),
462 ('rst', '**bold**', '<strong>bold</strong>'),
464 ('rst', '**bold**', '<strong>bold</strong>'),
463 ('markdown', 'plain text', '<p>plain text</p>'),
465 ('markdown', 'plain text', '<p>plain text</p>'),
464 ('markdown', '# header', '<h1>header</h1>'),
466 ('markdown', '# header', '<h1>header</h1>'),
465 ('markdown', '*italics*', '<em>italics</em>'),
467 ('markdown', '*italics*', '<em>italics</em>'),
466 ('markdown', '**bold**', '<strong>bold</strong>'),
468 ('markdown', '**bold**', '<strong>bold</strong>'),
467 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
469 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
468 'md-header', 'md-italics', 'md-bold', ])
470 'md-header', 'md-italics', 'md-bold', ])
469 def test_preview(self, renderer, text_input, output, backend, xhr_header):
471 def test_preview(self, renderer, text_input, output, backend, xhr_header):
470 self.log_user()
472 self.log_user()
471 params = {
473 params = {
472 'renderer': renderer,
474 'renderer': renderer,
473 'text': text_input,
475 'text': text_input,
474 'csrf_token': self.csrf_token
476 'csrf_token': self.csrf_token
475 }
477 }
476 commit_id = '0' * 16 # fake this for tests
478 commit_id = '0' * 16 # fake this for tests
477 response = self.app.post(
479 response = self.app.post(
478 route_path('repo_commit_comment_preview',
480 route_path('repo_commit_comment_preview',
479 repo_name=backend.repo_name, commit_id=commit_id,),
481 repo_name=backend.repo_name, commit_id=commit_id,),
480 params=params,
482 params=params,
481 extra_environ=xhr_header)
483 extra_environ=xhr_header)
482
484
483 response.mustcontain(output)
485 response.mustcontain(output)
484
486
485
487
486 def assert_comment_links(response, comments, inline_comments):
488 def assert_comment_links(response, comments, inline_comments):
487 response.mustcontain(
489 response.mustcontain(
488 '<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
490 '<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
489 response.mustcontain(
491 response.mustcontain(
490 '<span class="display-none" id="inline-comments-count">{}</span>'.format(inline_comments))
492 '<span class="display-none" id="inline-comments-count">{}</span>'.format(inline_comments))
491
493
492
494
493
495
@@ -1,326 +1,336 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
22 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
23 from rhodecode.lib.helpers import _shorten_commit_id
23 from rhodecode.lib.helpers import _shorten_commit_id
24
24
25
25
26 def route_path(name, params=None, **kwargs):
26 def route_path(name, params=None, **kwargs):
27 import urllib.request, urllib.parse, urllib.error
27 import urllib.request
28 import urllib.parse
29 import urllib.error
28
30
29 base_url = {
31 base_url = {
30 'repo_commit': '/{repo_name}/changeset/{commit_id}',
32 'repo_commit': '/{repo_name}/changeset/{commit_id}',
31 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
33 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
32 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
34 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
33 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
35 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
34 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
36 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
35 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
37 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
36 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
38 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
37 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
39 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
38 }[name].format(**kwargs)
40 }[name].format(**kwargs)
39
41
40 if params:
42 if params:
41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
43 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 return base_url
44 return base_url
43
45
44
46
45 @pytest.mark.usefixtures("app")
47 @pytest.mark.usefixtures("app")
46 class TestRepoCommitView(object):
48 class TestRepoCommitView(object):
47
49
48 def test_show_commit(self, backend):
50 def test_show_commit(self, backend):
49 commit_id = self.commit_id[backend.alias]
51 commit_id = self.commit_id[backend.alias]
50 response = self.app.get(route_path(
52 response = self.app.get(route_path(
51 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
53 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
52 response.mustcontain('Added a symlink')
54 response.mustcontain('Added a symlink')
53 response.mustcontain(commit_id)
55 response.mustcontain(commit_id)
54 response.mustcontain('No newline at end of file')
56 response.mustcontain('No newline at end of file')
55
57
56 def test_show_raw(self, backend):
58 def test_show_raw(self, backend):
57 commit_id = self.commit_id[backend.alias]
59 commit_id = self.commit_id[backend.alias]
60 # webtest uses linter to check if response is bytes,
61 # and we use memoryview here as a wrapper, quick turn-off
62 self.app.lint = False
63
58 response = self.app.get(route_path(
64 response = self.app.get(route_path(
59 'repo_commit_raw',
65 'repo_commit_raw',
60 repo_name=backend.repo_name, commit_id=commit_id))
66 repo_name=backend.repo_name, commit_id=commit_id))
61 assert response.text == self.diffs[backend.alias]
67 assert response.body == self.diffs[backend.alias]
62
68
63 def test_show_raw_patch(self, backend):
69 def test_show_raw_patch(self, backend):
64 response = self.app.get(route_path(
70 response = self.app.get(route_path(
65 'repo_commit_patch', repo_name=backend.repo_name,
71 'repo_commit_patch', repo_name=backend.repo_name,
66 commit_id=self.commit_id[backend.alias]))
72 commit_id=self.commit_id[backend.alias]))
67 assert response.text == self.patches[backend.alias]
73 assert response.body == self.patches[backend.alias]
68
74
69 def test_commit_download(self, backend):
75 def test_commit_download(self, backend):
76 # webtest uses linter to check if response is bytes,
77 # and we use memoryview here as a wrapper, quick turn-off
78 self.app.lint = False
79
70 response = self.app.get(route_path(
80 response = self.app.get(route_path(
71 'repo_commit_download',
81 'repo_commit_download',
72 repo_name=backend.repo_name,
82 repo_name=backend.repo_name,
73 commit_id=self.commit_id[backend.alias]))
83 commit_id=self.commit_id[backend.alias]))
74 assert response.text == self.diffs[backend.alias]
84 assert response.body == self.diffs[backend.alias]
75
85
76 def test_single_commit_page_different_ops(self, backend):
86 def test_single_commit_page_different_ops(self, backend):
77 commit_id = {
87 commit_id = {
78 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
88 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
79 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
89 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
80 'svn': '337',
90 'svn': '337',
81 }
91 }
82 diff_stat = {
92 diff_stat = {
83 'hg': (21, 943, 288),
93 'hg': (21, 943, 288),
84 'git': (20, 941, 286),
94 'git': (20, 941, 286),
85 'svn': (21, 943, 288),
95 'svn': (21, 943, 288),
86 }
96 }
87
97
88 commit_id = commit_id[backend.alias]
98 commit_id = commit_id[backend.alias]
89 response = self.app.get(route_path(
99 response = self.app.get(route_path(
90 'repo_commit',
100 'repo_commit',
91 repo_name=backend.repo_name, commit_id=commit_id))
101 repo_name=backend.repo_name, commit_id=commit_id))
92
102
93 response.mustcontain(_shorten_commit_id(commit_id))
103 response.mustcontain(_shorten_commit_id(commit_id))
94
104
95 compare_page = ComparePage(response)
105 compare_page = ComparePage(response)
96 file_changes = diff_stat[backend.alias]
106 file_changes = diff_stat[backend.alias]
97 compare_page.contains_change_summary(*file_changes)
107 compare_page.contains_change_summary(*file_changes)
98
108
99 # files op files
109 # files op files
100 response.mustcontain('File not present at commit: %s' %
110 response.mustcontain('File not present at commit: %s' %
101 _shorten_commit_id(commit_id))
111 _shorten_commit_id(commit_id))
102
112
103 # svn uses a different filename
113 # svn uses a different filename
104 if backend.alias == 'svn':
114 if backend.alias == 'svn':
105 response.mustcontain('new file 10644')
115 response.mustcontain('new file 10644')
106 else:
116 else:
107 response.mustcontain('new file 100644')
117 response.mustcontain('new file 100644')
108 response.mustcontain('Changed theme to ADC theme') # commit msg
118 response.mustcontain('Changed theme to ADC theme') # commit msg
109
119
110 self._check_new_diff_menus(response, right_menu=True)
120 self._check_new_diff_menus(response, right_menu=True)
111
121
112 def test_commit_range_page_different_ops(self, backend):
122 def test_commit_range_page_different_ops(self, backend):
113 commit_id_range = {
123 commit_id_range = {
114 'hg': (
124 'hg': (
115 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
125 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
116 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
126 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
117 'git': (
127 'git': (
118 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
128 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
119 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
129 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
120 'svn': (
130 'svn': (
121 '335',
131 '335',
122 '337'),
132 '337'),
123 }
133 }
124 commit_ids = commit_id_range[backend.alias]
134 commit_ids = commit_id_range[backend.alias]
125 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
135 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
126 response = self.app.get(route_path(
136 response = self.app.get(route_path(
127 'repo_commit',
137 'repo_commit',
128 repo_name=backend.repo_name, commit_id=commit_id))
138 repo_name=backend.repo_name, commit_id=commit_id))
129
139
130 response.mustcontain(_shorten_commit_id(commit_ids[0]))
140 response.mustcontain(_shorten_commit_id(commit_ids[0]))
131 response.mustcontain(_shorten_commit_id(commit_ids[1]))
141 response.mustcontain(_shorten_commit_id(commit_ids[1]))
132
142
133 compare_page = ComparePage(response)
143 compare_page = ComparePage(response)
134
144
135 # svn is special
145 # svn is special
136 if backend.alias == 'svn':
146 if backend.alias == 'svn':
137 response.mustcontain('new file 10644')
147 response.mustcontain('new file 10644')
138 for file_changes in [(1, 5, 1), (12, 236, 22), (21, 943, 288)]:
148 for file_changes in [(1, 5, 1), (12, 236, 22), (21, 943, 288)]:
139 compare_page.contains_change_summary(*file_changes)
149 compare_page.contains_change_summary(*file_changes)
140 elif backend.alias == 'git':
150 elif backend.alias == 'git':
141 response.mustcontain('new file 100644')
151 response.mustcontain('new file 100644')
142 for file_changes in [(12, 222, 20), (20, 941, 286)]:
152 for file_changes in [(12, 222, 20), (20, 941, 286)]:
143 compare_page.contains_change_summary(*file_changes)
153 compare_page.contains_change_summary(*file_changes)
144 else:
154 else:
145 response.mustcontain('new file 100644')
155 response.mustcontain('new file 100644')
146 for file_changes in [(12, 222, 20), (21, 943, 288)]:
156 for file_changes in [(12, 222, 20), (21, 943, 288)]:
147 compare_page.contains_change_summary(*file_changes)
157 compare_page.contains_change_summary(*file_changes)
148
158
149 # files op files
159 # files op files
150 response.mustcontain('File not present at commit: %s' % _shorten_commit_id(commit_ids[1]))
160 response.mustcontain('File not present at commit: %s' % _shorten_commit_id(commit_ids[1]))
151 response.mustcontain('Added docstrings to vcs.cli') # commit msg
161 response.mustcontain('Added docstrings to vcs.cli') # commit msg
152 response.mustcontain('Changed theme to ADC theme') # commit msg
162 response.mustcontain('Changed theme to ADC theme') # commit msg
153
163
154 self._check_new_diff_menus(response)
164 self._check_new_diff_menus(response)
155
165
156 def test_combined_compare_commit_page_different_ops(self, backend):
166 def test_combined_compare_commit_page_different_ops(self, backend):
157 commit_id_range = {
167 commit_id_range = {
158 'hg': (
168 'hg': (
159 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
169 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
160 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
170 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
161 'git': (
171 'git': (
162 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
172 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
163 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
173 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
164 'svn': (
174 'svn': (
165 '335',
175 '335',
166 '337'),
176 '337'),
167 }
177 }
168 commit_ids = commit_id_range[backend.alias]
178 commit_ids = commit_id_range[backend.alias]
169 response = self.app.get(route_path(
179 response = self.app.get(route_path(
170 'repo_compare',
180 'repo_compare',
171 repo_name=backend.repo_name,
181 repo_name=backend.repo_name,
172 source_ref_type='rev', source_ref=commit_ids[0],
182 source_ref_type='rev', source_ref=commit_ids[0],
173 target_ref_type='rev', target_ref=commit_ids[1], ))
183 target_ref_type='rev', target_ref=commit_ids[1], ))
174
184
175 response.mustcontain(_shorten_commit_id(commit_ids[0]))
185 response.mustcontain(_shorten_commit_id(commit_ids[0]))
176 response.mustcontain(_shorten_commit_id(commit_ids[1]))
186 response.mustcontain(_shorten_commit_id(commit_ids[1]))
177
187
178 # files op files
188 # files op files
179 response.mustcontain('File not present at commit: %s' %
189 response.mustcontain('File not present at commit: %s' %
180 _shorten_commit_id(commit_ids[1]))
190 _shorten_commit_id(commit_ids[1]))
181
191
182 compare_page = ComparePage(response)
192 compare_page = ComparePage(response)
183
193
184 # svn is special
194 # svn is special
185 if backend.alias == 'svn':
195 if backend.alias == 'svn':
186 response.mustcontain('new file 10644')
196 response.mustcontain('new file 10644')
187 file_changes = (32, 1179, 310)
197 file_changes = (32, 1179, 310)
188 compare_page.contains_change_summary(*file_changes)
198 compare_page.contains_change_summary(*file_changes)
189 elif backend.alias == 'git':
199 elif backend.alias == 'git':
190 response.mustcontain('new file 100644')
200 response.mustcontain('new file 100644')
191 file_changes = (31, 1163, 306)
201 file_changes = (31, 1163, 306)
192 compare_page.contains_change_summary(*file_changes)
202 compare_page.contains_change_summary(*file_changes)
193 else:
203 else:
194 response.mustcontain('new file 100644')
204 response.mustcontain('new file 100644')
195 file_changes = (32, 1165, 308)
205 file_changes = (32, 1165, 308)
196 compare_page.contains_change_summary(*file_changes)
206 compare_page.contains_change_summary(*file_changes)
197
207
198 response.mustcontain('Added docstrings to vcs.cli') # commit msg
208 response.mustcontain('Added docstrings to vcs.cli') # commit msg
199 response.mustcontain('Changed theme to ADC theme') # commit msg
209 response.mustcontain('Changed theme to ADC theme') # commit msg
200
210
201 self._check_new_diff_menus(response)
211 self._check_new_diff_menus(response)
202
212
203 def test_changeset_range(self, backend):
213 def test_changeset_range(self, backend):
204 self._check_changeset_range(
214 self._check_changeset_range(
205 backend, self.commit_id_range, self.commit_id_range_result)
215 backend, self.commit_id_range, self.commit_id_range_result)
206
216
207 def test_changeset_range_with_initial_commit(self, backend):
217 def test_changeset_range_with_initial_commit(self, backend):
208 commit_id_range = {
218 commit_id_range = {
209 'hg': (
219 'hg': (
210 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
220 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
211 '...6cba7170863a2411822803fa77a0a264f1310b35'),
221 '...6cba7170863a2411822803fa77a0a264f1310b35'),
212 'git': (
222 'git': (
213 'c1214f7e79e02fc37156ff215cd71275450cffc3'
223 'c1214f7e79e02fc37156ff215cd71275450cffc3'
214 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
224 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
215 'svn': '1...3',
225 'svn': '1...3',
216 }
226 }
217 commit_id_range_result = {
227 commit_id_range_result = {
218 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
228 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
219 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
229 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
220 'svn': ['1', '2', '3'],
230 'svn': ['1', '2', '3'],
221 }
231 }
222 self._check_changeset_range(
232 self._check_changeset_range(
223 backend, commit_id_range, commit_id_range_result)
233 backend, commit_id_range, commit_id_range_result)
224
234
225 def _check_changeset_range(
235 def _check_changeset_range(
226 self, backend, commit_id_ranges, commit_id_range_result):
236 self, backend, commit_id_ranges, commit_id_range_result):
227 response = self.app.get(
237 response = self.app.get(
228 route_path('repo_commit',
238 route_path('repo_commit',
229 repo_name=backend.repo_name,
239 repo_name=backend.repo_name,
230 commit_id=commit_id_ranges[backend.alias]))
240 commit_id=commit_id_ranges[backend.alias]))
231
241
232 expected_result = commit_id_range_result[backend.alias]
242 expected_result = commit_id_range_result[backend.alias]
233 response.mustcontain('{} commits'.format(len(expected_result)))
243 response.mustcontain('{} commits'.format(len(expected_result)))
234 for commit_id in expected_result:
244 for commit_id in expected_result:
235 response.mustcontain(commit_id)
245 response.mustcontain(commit_id)
236
246
237 commit_id = {
247 commit_id = {
238 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
248 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
239 'svn': '393',
249 'svn': '393',
240 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
250 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
241 }
251 }
242
252
243 commit_id_range = {
253 commit_id_range = {
244 'hg': (
254 'hg': (
245 'a53d9201d4bc278910d416d94941b7ea007ecd52'
255 'a53d9201d4bc278910d416d94941b7ea007ecd52'
246 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
256 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
247 'git': (
257 'git': (
248 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
258 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
249 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
259 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
250 'svn': '391...393',
260 'svn': '391...393',
251 }
261 }
252
262
253 commit_id_range_result = {
263 commit_id_range_result = {
254 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
264 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
255 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
265 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
256 'svn': ['391', '392', '393'],
266 'svn': ['391', '392', '393'],
257 }
267 }
258
268
259 diffs = {
269 diffs = {
260 'hg': r"""diff --git a/README b/README
270 'hg': br"""diff --git a/README b/README
261 new file mode 120000
271 new file mode 120000
262 --- /dev/null
272 --- /dev/null
263 +++ b/README
273 +++ b/README
264 @@ -0,0 +1,1 @@
274 @@ -0,0 +1,1 @@
265 +README.rst
275 +README.rst
266 \ No newline at end of file
276 \ No newline at end of file
267 """,
277 """,
268 'git': r"""diff --git a/README b/README
278 'git': br"""diff --git a/README b/README
269 new file mode 120000
279 new file mode 120000
270 index 0000000..92cacd2
280 index 0000000..92cacd2
271 --- /dev/null
281 --- /dev/null
272 +++ b/README
282 +++ b/README
273 @@ -0,0 +1 @@
283 @@ -0,0 +1 @@
274 +README.rst
284 +README.rst
275 \ No newline at end of file
285 \ No newline at end of file
276 """,
286 """,
277 'svn': """Index: README
287 'svn': b"""Index: README
278 ===================================================================
288 ===================================================================
279 diff --git a/README b/README
289 diff --git a/README b/README
280 new file mode 10644
290 new file mode 10644
281 --- /dev/null\t(revision 0)
291 --- /dev/null\t(revision 0)
282 +++ b/README\t(revision 393)
292 +++ b/README\t(revision 393)
283 @@ -0,0 +1 @@
293 @@ -0,0 +1 @@
284 +link README.rst
294 +link README.rst
285 \\ No newline at end of file
295 \\ No newline at end of file
286 """,
296 """,
287 }
297 }
288
298
289 patches = {
299 patches = {
290 'hg': r"""# HG changeset patch
300 'hg': br"""# HG changeset patch
291 # User Marcin Kuzminski <marcin@python-works.com>
301 # User Marcin Kuzminski <marcin@python-works.com>
292 # Date 2014-01-07 12:21:40
302 # Date 2014-01-07 12:21:40
293 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
303 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
294 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
304 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
295
305
296 Added a symlink
306 Added a symlink
297
307
298 """ + diffs['hg'],
308 """ + diffs['hg'],
299 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
309 'git': br"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
300 From: Marcin Kuzminski <marcin@python-works.com>
310 From: Marcin Kuzminski <marcin@python-works.com>
301 Date: 2014-01-07 12:22:20
311 Date: 2014-01-07 12:22:20
302 Subject: [PATCH] Added a symlink
312 Subject: [PATCH] Added a symlink
303
313
304 ---
314 ---
305
315
306 """ + diffs['git'],
316 """ + diffs['git'],
307 'svn': r"""# SVN changeset patch
317 'svn': br"""# SVN changeset patch
308 # User marcin
318 # User marcin
309 # Date 2014-09-02 12:25:22.071142
319 # Date 2014-09-02 12:25:22.071142
310 # Revision 393
320 # Revision 393
311
321
312 Added a symlink
322 Added a symlink
313
323
314 """ + diffs['svn'],
324 """ + diffs['svn'],
315 }
325 }
316
326
317 def _check_new_diff_menus(self, response, right_menu=False,):
327 def _check_new_diff_menus(self, response, right_menu=False,):
318 # individual file diff menus
328 # individual file diff menus
319 for elem in ['Show file before', 'Show file after']:
329 for elem in ['Show file before', 'Show file after']:
320 response.mustcontain(elem)
330 response.mustcontain(elem)
321
331
322 # right pane diff menus
332 # right pane diff menus
323 if right_menu:
333 if right_menu:
324 for elem in ['Hide whitespace changes', 'Toggle wide diff',
334 for elem in ['Hide whitespace changes', 'Toggle wide diff',
325 'Show full context diff']:
335 'Show full context diff']:
326 response.mustcontain(elem)
336 response.mustcontain(elem)
@@ -1,671 +1,670 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import mock
20 import mock
21 import pytest
21 import pytest
22 import lxml.html
22 import lxml.html
23
23
24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
25 from rhodecode.tests import assert_session_flash
25 from rhodecode.tests import assert_session_flash
26 from rhodecode.tests.utils import AssertResponse, commit_change
26 from rhodecode.tests.utils import AssertResponse, commit_change
27
27
28
28
29 def route_path(name, params=None, **kwargs):
29 def route_path(name, params=None, **kwargs):
30 import urllib.request, urllib.parse, urllib.error
30 import urllib.request
31 import urllib.parse
32 import urllib.error
31
33
32 base_url = {
34 base_url = {
33 'repo_compare_select': '/{repo_name}/compare',
35 'repo_compare_select': '/{repo_name}/compare',
34 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
36 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
35 }[name].format(**kwargs)
37 }[name].format(**kwargs)
36
38
37 if params:
39 if params:
38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
39 return base_url
41 return base_url
40
42
41
43
42 @pytest.mark.usefixtures("autologin_user", "app")
44 @pytest.mark.usefixtures("autologin_user", "app")
43 class TestCompareView(object):
45 class TestCompareView(object):
44
46
45 def test_compare_index_is_reached_at_least_once(self, backend):
47 def test_compare_index_is_reached_at_least_once(self, backend):
46 repo = backend.repo
48 repo = backend.repo
47 self.app.get(
49 self.app.get(
48 route_path('repo_compare_select', repo_name=repo.repo_name))
50 route_path('repo_compare_select', repo_name=repo.repo_name))
49
51
50 @pytest.mark.xfail_backends("svn", reason="Requires pull")
52 @pytest.mark.xfail_backends("svn", reason="Requires pull")
51 def test_compare_remote_with_different_commit_indexes(self, backend):
53 def test_compare_remote_with_different_commit_indexes(self, backend):
52 # Preparing the following repository structure:
54 # Preparing the following repository structure:
53 #
55 #
54 # Origin repository has two commits:
56 # Origin repository has two commits:
55 #
57 #
56 # 0 1
58 # 0 1
57 # A -- D
59 # A -- D
58 #
60 #
59 # The fork of it has a few more commits and "D" has a commit index
61 # The fork of it has a few more commits and "D" has a commit index
60 # which does not exist in origin.
62 # which does not exist in origin.
61 #
63 #
62 # 0 1 2 3 4
64 # 0 1 2 3 4
63 # A -- -- -- D -- E
65 # A -- -- -- D -- E
64 # \- B -- C
66 # \- B -- C
65 #
67 #
66
68
67 fork = backend.create_repo()
69 fork = backend.create_repo()
70 origin = backend.create_repo()
68
71
69 # prepare fork
72 # prepare fork
70 commit0 = commit_change(
73 commit0 = commit_change(
71 fork.repo_name, filename='file1', content='A',
74 fork.repo_name, filename=b'file1', content=b'A',
72 message='A', vcs_type=backend.alias, parent=None, newfile=True)
75 message='A - Initial Commit', vcs_type=backend.alias, parent=None, newfile=True)
73
76
74 commit1 = commit_change(
77 commit1 = commit_change(
75 fork.repo_name, filename='file1', content='B',
78 fork.repo_name, filename=b'file1', content=b'B',
76 message='B, child of A', vcs_type=backend.alias, parent=commit0)
79 message='B, child of A', vcs_type=backend.alias, parent=commit0)
77
80
78 commit_change( # commit 2
81 commit_change( # commit 2
79 fork.repo_name, filename='file1', content='C',
82 fork.repo_name, filename=b'file1', content=b'C',
80 message='C, child of B', vcs_type=backend.alias, parent=commit1)
83 message='C, child of B', vcs_type=backend.alias, parent=commit1)
81
84
82 commit3 = commit_change(
85 commit3 = commit_change(
83 fork.repo_name, filename='file1', content='D',
86 fork.repo_name, filename=b'file1', content=b'D',
84 message='D, child of A', vcs_type=backend.alias, parent=commit0)
87 message='D, child of A', vcs_type=backend.alias, parent=commit0)
85
88
86 commit4 = commit_change(
89 commit4 = commit_change(
87 fork.repo_name, filename='file1', content='E',
90 fork.repo_name, filename=b'file1', content=b'E',
88 message='E, child of D', vcs_type=backend.alias, parent=commit3)
91 message='E, child of D', vcs_type=backend.alias, parent=commit3)
89
92
90 # prepare origin repository, taking just the history up to D
93 # prepare origin repository, taking just the history up to D
91 origin = backend.create_repo()
92
94
93 origin_repo = origin.scm_instance(cache=False)
95 origin_repo = origin.scm_instance(cache=False)
94 origin_repo.config.clear_section('hooks')
96 origin_repo.config.clear_section('hooks')
95 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
97 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
96 origin_repo = origin.scm_instance(cache=False) # cache rebuild
98 origin_repo = origin.scm_instance(cache=False) # cache rebuild
97
99
98 # Verify test fixture setup
100 # Verify test fixture setup
99 # This does not work for git
101 # This does not work for git
100 if backend.alias != 'git':
102 if backend.alias != 'git':
101 assert 5 == len(fork.scm_instance().commit_ids)
103 assert 5 == len(fork.scm_instance(cache=False).commit_ids)
102 assert 2 == len(origin_repo.commit_ids)
104 assert 2 == len(origin_repo.commit_ids)
103
105
104 # Comparing the revisions
106 # Comparing the revisions
105 response = self.app.get(
107 response = self.app.get(
106 route_path('repo_compare',
108 route_path('repo_compare',
107 repo_name=origin.repo_name,
109 repo_name=origin.repo_name,
108 source_ref_type="rev", source_ref=commit3.raw_id,
110 source_ref_type="rev", source_ref=commit3.raw_id,
109 target_ref_type="rev", target_ref=commit4.raw_id,
111 target_ref_type="rev", target_ref=commit4.raw_id,
110 params=dict(merge='1', target_repo=fork.repo_name)
112 params=dict(merge='1', target_repo=fork.repo_name)
111 ))
113 ),
114 status=200)
112
115
113 compare_page = ComparePage(response)
116 compare_page = ComparePage(response)
114 compare_page.contains_commits([commit4])
117 compare_page.contains_commits([commit4])
115
118
116 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
119 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
117 def test_compare_forks_on_branch_extra_commits(self, backend):
120 def test_compare_forks_on_branch_extra_commits(self, backend):
118 repo1 = backend.create_repo()
121 repo1 = backend.create_repo()
119
122
120 # commit something !
123 # commit something !
121 commit0 = commit_change(
124 commit0 = commit_change(
122 repo1.repo_name, filename='file1', content='line1\n',
125 repo1.repo_name, filename=b'file1', content=b'line1\n',
123 message='commit1', vcs_type=backend.alias, parent=None,
126 message='commit1', vcs_type=backend.alias, parent=None,
124 newfile=True)
127 newfile=True)
125
128
126 # fork this repo
129 # fork this repo
127 repo2 = backend.create_fork()
130 repo2 = backend.create_fork()
128
131
129 # add two extra commit into fork
132 # add two extra commit into fork
130 commit1 = commit_change(
133 commit1 = commit_change(
131 repo2.repo_name, filename='file1', content='line1\nline2\n',
134 repo2.repo_name, filename=b'file1', content=b'line1\nline2\n',
132 message='commit2', vcs_type=backend.alias, parent=commit0)
135 message='commit2', vcs_type=backend.alias, parent=commit0)
133
136
134 commit2 = commit_change(
137 commit2 = commit_change(
135 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
138 repo2.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
136 message='commit3', vcs_type=backend.alias, parent=commit1)
139 message='commit3', vcs_type=backend.alias, parent=commit1)
137
140
138 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
141 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
139 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
142 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
140
143
141 response = self.app.get(
144 response = self.app.get(
142 route_path('repo_compare',
145 route_path('repo_compare',
143 repo_name=repo1.repo_name,
146 repo_name=repo1.repo_name,
144 source_ref_type="branch", source_ref=commit_id2,
147 source_ref_type="branch", source_ref=commit_id2,
145 target_ref_type="branch", target_ref=commit_id1,
148 target_ref_type="branch", target_ref=commit_id1,
146 params=dict(merge='1', target_repo=repo2.repo_name)
149 params=dict(merge='1', target_repo=repo2.repo_name)
147 ))
150 ))
148
151
149 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
152 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
150 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
153 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
151
154
152 compare_page = ComparePage(response)
155 compare_page = ComparePage(response)
153 compare_page.contains_change_summary(1, 2, 0)
156 compare_page.contains_change_summary(1, 2, 0)
154 compare_page.contains_commits([commit1, commit2])
157 compare_page.contains_commits([commit1, commit2])
155
158
156 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
159 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
157 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
160 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
158
161
159 # Swap is removed when comparing branches since it's a PR feature and
162 # Swap is removed when comparing branches since it's a PR feature and
160 # it is then a preview mode
163 # it is then a preview mode
161 compare_page.swap_is_hidden()
164 compare_page.swap_is_hidden()
162 compare_page.target_source_are_disabled()
165 compare_page.target_source_are_disabled()
163
166
164 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
167 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
165 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(self, backend):
168 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(self, backend):
166 repo1 = backend.create_repo()
169 repo1 = backend.create_repo()
167
170
168 # commit something !
171 # commit something !
169 commit0 = commit_change(
172 commit0 = commit_change(
170 repo1.repo_name, filename='file1', content='line1\n',
173 repo1.repo_name, filename=b'file1', content=b'line1\n',
171 message='commit1', vcs_type=backend.alias, parent=None,
174 message='commit1', vcs_type=backend.alias, parent=None,
172 newfile=True)
175 newfile=True)
173
176
174 # fork this repo
177 # fork this repo
175 repo2 = backend.create_fork()
178 repo2 = backend.create_fork()
176
179
177 # now commit something to origin repo
180 # now commit something to origin repo
178 commit_change(
181 commit_change(
179 repo1.repo_name, filename='file2', content='line1file2\n',
182 repo1.repo_name, filename=b'file2', content=b'line1file2\n',
180 message='commit2', vcs_type=backend.alias, parent=commit0,
183 message='commit2', vcs_type=backend.alias, parent=commit0,
181 newfile=True)
184 newfile=True)
182
185
183 # add two extra commit into fork
186 # add two extra commit into fork
184 commit1 = commit_change(
187 commit1 = commit_change(
185 repo2.repo_name, filename='file1', content='line1\nline2\n',
188 repo2.repo_name, filename=b'file1', content=b'line1\nline2\n',
186 message='commit2', vcs_type=backend.alias, parent=commit0)
189 message='commit2', vcs_type=backend.alias, parent=commit0)
187
190
188 commit2 = commit_change(
191 commit2 = commit_change(
189 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
192 repo2.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
190 message='commit3', vcs_type=backend.alias, parent=commit1)
193 message='commit3', vcs_type=backend.alias, parent=commit1)
191
194
192 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
195 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
193 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
196 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
194
197
195 response = self.app.get(
198 response = self.app.get(
196 route_path('repo_compare',
199 route_path('repo_compare',
197 repo_name=repo1.repo_name,
200 repo_name=repo1.repo_name,
198 source_ref_type="branch", source_ref=commit_id2,
201 source_ref_type="branch", source_ref=commit_id2,
199 target_ref_type="branch", target_ref=commit_id1,
202 target_ref_type="branch", target_ref=commit_id1,
200 params=dict(merge='1', target_repo=repo2.repo_name),
203 params=dict(merge='1', target_repo=repo2.repo_name),
201 ))
204 ))
202
205
203 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
206 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
204 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
207 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
205
208
206 compare_page = ComparePage(response)
209 compare_page = ComparePage(response)
207 compare_page.contains_change_summary(1, 2, 0)
210 compare_page.contains_change_summary(1, 2, 0)
208 compare_page.contains_commits([commit1, commit2])
211 compare_page.contains_commits([commit1, commit2])
209 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
212 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
210 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
213 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
211
214
212 # Swap is removed when comparing branches since it's a PR feature and
215 # Swap is removed when comparing branches since it's a PR feature and
213 # it is then a preview mode
216 # it is then a preview mode
214 compare_page.swap_is_hidden()
217 compare_page.swap_is_hidden()
215 compare_page.target_source_are_disabled()
218 compare_page.target_source_are_disabled()
216
219
217 @pytest.mark.xfail_backends("svn")
220 @pytest.mark.xfail_backends("svn")
218 # TODO(marcink): no svn support for compare two seperate repos
221 # TODO(marcink): no svn support for compare two seperate repos
219 def test_compare_of_unrelated_forks(self, backend):
222 def test_compare_of_unrelated_forks(self, backend):
220 orig = backend.create_repo(number_of_commits=1)
223 orig = backend.create_repo(number_of_commits=1)
221 fork = backend.create_repo(number_of_commits=1)
224 fork = backend.create_repo(number_of_commits=1)
222
225
223 response = self.app.get(
226 response = self.app.get(
224 route_path('repo_compare',
227 route_path('repo_compare',
225 repo_name=orig.repo_name,
228 repo_name=orig.repo_name,
226 source_ref_type="rev", source_ref="tip",
229 source_ref_type="rev", source_ref="tip",
227 target_ref_type="rev", target_ref="tip",
230 target_ref_type="rev", target_ref="tip",
228 params=dict(merge='1', target_repo=fork.repo_name),
231 params=dict(merge='1', target_repo=fork.repo_name),
229 ),
232 ),
230 status=302)
233 status=302)
231 response = response.follow()
234 response = response.follow()
232 response.mustcontain("Repositories unrelated.")
235 response.mustcontain("Repositories unrelated.")
233
236
234 @pytest.mark.xfail_backends("svn")
237 @pytest.mark.xfail_backends("svn")
235 def test_compare_cherry_pick_commits_from_bottom(self, backend):
238 def test_compare_cherry_pick_commits_from_bottom(self, backend):
236
239
237 # repo1:
240 # repo1:
238 # commit0:
241 # commit0:
239 # commit1:
242 # commit1:
240 # repo1-fork- in which we will cherry pick bottom commits
243 # repo1-fork- in which we will cherry pick bottom commits
241 # commit0:
244 # commit0:
242 # commit1:
245 # commit1:
243 # commit2: x
246 # commit2: x
244 # commit3: x
247 # commit3: x
245 # commit4: x
248 # commit4: x
246 # commit5:
249 # commit5:
247 # make repo1, and commit1+commit2
250 # make repo1, and commit1+commit2
248
251
249 repo1 = backend.create_repo()
252 repo1 = backend.create_repo()
250
253
251 # commit something !
254 # commit something !
252 commit0 = commit_change(
255 commit0 = commit_change(
253 repo1.repo_name, filename='file1', content='line1\n',
256 repo1.repo_name, filename=b'file1', content=b'line1\n',
254 message='commit1', vcs_type=backend.alias, parent=None,
257 message='commit1', vcs_type=backend.alias, parent=None,
255 newfile=True)
258 newfile=True)
256 commit1 = commit_change(
259 commit1 = commit_change(
257 repo1.repo_name, filename='file1', content='line1\nline2\n',
260 repo1.repo_name, filename=b'file1', content=b'line1\nline2\n',
258 message='commit2', vcs_type=backend.alias, parent=commit0)
261 message='commit2', vcs_type=backend.alias, parent=commit0)
259
262
260 # fork this repo
263 # fork this repo
261 repo2 = backend.create_fork()
264 repo2 = backend.create_fork()
262
265
263 # now make commit3-6
266 # now make commit3-6
264 commit2 = commit_change(
267 commit2 = commit_change(
265 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
268 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
266 message='commit3', vcs_type=backend.alias, parent=commit1)
269 message='commit3', vcs_type=backend.alias, parent=commit1)
267 commit3 = commit_change(
270 commit3 = commit_change(
268 repo1.repo_name, filename='file1',
271 repo1.repo_name, filename=b'file1',content=b'line1\nline2\nline3\nline4\n',
269 content='line1\nline2\nline3\nline4\n', message='commit4',
272 message='commit4', vcs_type=backend.alias, parent=commit2)
270 vcs_type=backend.alias, parent=commit2)
271 commit4 = commit_change(
273 commit4 = commit_change(
272 repo1.repo_name, filename='file1',
274 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\nline4\nline5\n',
273 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
275 message='commit5', vcs_type=backend.alias, parent=commit3)
274 vcs_type=backend.alias, parent=commit3)
275 commit_change( # commit 5
276 commit_change( # commit 5
276 repo1.repo_name, filename='file1',
277 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\nline4\nline5\nline6\n',
277 content='line1\nline2\nline3\nline4\nline5\nline6\n',
278 message='commit6', vcs_type=backend.alias, parent=commit4)
278 message='commit6', vcs_type=backend.alias, parent=commit4)
279
279
280 response = self.app.get(
280 response = self.app.get(
281 route_path('repo_compare',
281 route_path('repo_compare',
282 repo_name=repo2.repo_name,
282 repo_name=repo2.repo_name,
283 # parent of commit2, in target repo2
283 # parent of commit2, in target repo2
284 source_ref_type="rev", source_ref=commit1.raw_id,
284 source_ref_type="rev", source_ref=commit1.raw_id,
285 target_ref_type="rev", target_ref=commit4.raw_id,
285 target_ref_type="rev", target_ref=commit4.raw_id,
286 params=dict(merge='1', target_repo=repo1.repo_name),
286 params=dict(merge='1', target_repo=repo1.repo_name),
287 ))
287 ))
288 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
288 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
289 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
289 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
290
290
291 # files
291 # files
292 compare_page = ComparePage(response)
292 compare_page = ComparePage(response)
293 compare_page.contains_change_summary(1, 3, 0)
293 compare_page.contains_change_summary(1, 3, 0)
294 compare_page.contains_commits([commit2, commit3, commit4])
294 compare_page.contains_commits([commit2, commit3, commit4])
295 anchor = 'a_c-{}-826e8142e6ba'.format(commit1.short_id)
295 anchor = 'a_c-{}-826e8142e6ba'.format(commit1.short_id)
296 compare_page.contains_file_links_and_anchors([('file1', anchor),])
296 compare_page.contains_file_links_and_anchors([('file1', anchor),])
297
297
298 @pytest.mark.xfail_backends("svn")
298 @pytest.mark.xfail_backends("svn")
299 def test_compare_cherry_pick_commits_from_top(self, backend):
299 def test_compare_cherry_pick_commits_from_top(self, backend):
300 # repo1:
300 # repo1:
301 # commit0:
301 # commit0:
302 # commit1:
302 # commit1:
303 # repo1-fork- in which we will cherry pick bottom commits
303 # repo1-fork- in which we will cherry pick bottom commits
304 # commit0:
304 # commit0:
305 # commit1:
305 # commit1:
306 # commit2:
306 # commit2:
307 # commit3: x
307 # commit3: x
308 # commit4: x
308 # commit4: x
309 # commit5: x
309 # commit5: x
310
310
311 # make repo1, and commit1+commit2
311 # make repo1, and commit1+commit2
312 repo1 = backend.create_repo()
312 repo1 = backend.create_repo()
313
313
314 # commit something !
314 # commit something !
315 commit0 = commit_change(
315 commit0 = commit_change(
316 repo1.repo_name, filename='file1', content='line1\n',
316 repo1.repo_name, filename=b'file1', content=b'line1\n',
317 message='commit1', vcs_type=backend.alias, parent=None,
317 message='commit1', vcs_type=backend.alias, parent=None,
318 newfile=True)
318 newfile=True)
319 commit1 = commit_change(
319 commit1 = commit_change(
320 repo1.repo_name, filename='file1', content='line1\nline2\n',
320 repo1.repo_name, filename=b'file1', content=b'line1\nline2\n',
321 message='commit2', vcs_type=backend.alias, parent=commit0)
321 message='commit2', vcs_type=backend.alias, parent=commit0)
322
322
323 # fork this repo
323 # fork this repo
324 backend.create_fork()
324 backend.create_fork()
325
325
326 # now make commit3-6
326 # now make commit3-6
327 commit2 = commit_change(
327 commit2 = commit_change(
328 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
328 repo1.repo_name, filename=b'file1', content=b'line1\nline2\nline3\n',
329 message='commit3', vcs_type=backend.alias, parent=commit1)
329 message='commit3', vcs_type=backend.alias, parent=commit1)
330 commit3 = commit_change(
330 commit3 = commit_change(
331 repo1.repo_name, filename='file1',
331 repo1.repo_name, filename=b'file1',
332 content='line1\nline2\nline3\nline4\n', message='commit4',
332 content=b'line1\nline2\nline3\nline4\n', message='commit4',
333 vcs_type=backend.alias, parent=commit2)
333 vcs_type=backend.alias, parent=commit2)
334 commit4 = commit_change(
334 commit4 = commit_change(
335 repo1.repo_name, filename='file1',
335 repo1.repo_name, filename=b'file1',
336 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
336 content=b'line1\nline2\nline3\nline4\nline5\n', message='commit5',
337 vcs_type=backend.alias, parent=commit3)
337 vcs_type=backend.alias, parent=commit3)
338 commit5 = commit_change(
338 commit5 = commit_change(
339 repo1.repo_name, filename='file1',
339 repo1.repo_name, filename=b'file1',
340 content='line1\nline2\nline3\nline4\nline5\nline6\n',
340 content=b'line1\nline2\nline3\nline4\nline5\nline6\n',
341 message='commit6', vcs_type=backend.alias, parent=commit4)
341 message='commit6', vcs_type=backend.alias, parent=commit4)
342
342
343 response = self.app.get(
343 response = self.app.get(
344 route_path('repo_compare',
344 route_path('repo_compare',
345 repo_name=repo1.repo_name,
345 repo_name=repo1.repo_name,
346 # parent of commit3, not in source repo2
346 # parent of commit3, not in source repo2
347 source_ref_type="rev", source_ref=commit2.raw_id,
347 source_ref_type="rev", source_ref=commit2.raw_id,
348 target_ref_type="rev", target_ref=commit5.raw_id,
348 target_ref_type="rev", target_ref=commit5.raw_id,
349 params=dict(merge='1'),))
349 params=dict(merge='1'),))
350
350
351 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
351 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
352 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
352 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
353
353
354 compare_page = ComparePage(response)
354 compare_page = ComparePage(response)
355 compare_page.contains_change_summary(1, 3, 0)
355 compare_page.contains_change_summary(1, 3, 0)
356 compare_page.contains_commits([commit3, commit4, commit5])
356 compare_page.contains_commits([commit3, commit4, commit5])
357
357
358 # files
358 # files
359 anchor = 'a_c-{}-826e8142e6ba'.format(commit2.short_id)
359 anchor = 'a_c-{}-826e8142e6ba'.format(commit2.short_id)
360 compare_page.contains_file_links_and_anchors([('file1', anchor),])
360 compare_page.contains_file_links_and_anchors([('file1', anchor),])
361
361
362 @pytest.mark.xfail_backends("svn")
362 @pytest.mark.xfail_backends("svn")
363 def test_compare_remote_branches(self, backend):
363 def test_compare_remote_branches(self, backend):
364 repo1 = backend.repo
364 repo1 = backend.repo
365 repo2 = backend.create_fork()
365 repo2 = backend.create_fork()
366
366
367 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
367 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
368 commit_id1_short = repo1.get_commit(commit_idx=3).short_id
368 commit_id1_short = repo1.get_commit(commit_idx=3).short_id
369 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
369 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
370 commit_id2_short = repo1.get_commit(commit_idx=6).short_id
370 commit_id2_short = repo1.get_commit(commit_idx=6).short_id
371
371
372 response = self.app.get(
372 response = self.app.get(
373 route_path('repo_compare',
373 route_path('repo_compare',
374 repo_name=repo1.repo_name,
374 repo_name=repo1.repo_name,
375 source_ref_type="rev", source_ref=commit_id1,
375 source_ref_type="rev", source_ref=commit_id1,
376 target_ref_type="rev", target_ref=commit_id2,
376 target_ref_type="rev", target_ref=commit_id2,
377 params=dict(merge='1', target_repo=repo2.repo_name),
377 params=dict(merge='1', target_repo=repo2.repo_name),
378 ))
378 ))
379
379
380 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
380 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
381 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
381 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
382
382
383 compare_page = ComparePage(response)
383 compare_page = ComparePage(response)
384
384
385 # outgoing commits between those commits
385 # outgoing commits between those commits
386 compare_page.contains_commits(
386 compare_page.contains_commits(
387 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
387 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
388
388
389 # files
389 # files
390 compare_page.contains_file_links_and_anchors([
390 compare_page.contains_file_links_and_anchors([
391 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(commit_id2_short)),
391 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(commit_id2_short)),
392 ('vcs/backends/__init__.py', 'a_c-{}-41b41c1f2796'.format(commit_id1_short)),
392 ('vcs/backends/__init__.py', 'a_c-{}-41b41c1f2796'.format(commit_id1_short)),
393 ('vcs/backends/base.py', 'a_c-{}-2f574d260608'.format(commit_id1_short)),
393 ('vcs/backends/base.py', 'a_c-{}-2f574d260608'.format(commit_id1_short)),
394 ])
394 ])
395
395
396 @pytest.mark.xfail_backends("svn")
396 @pytest.mark.xfail_backends("svn")
397 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
397 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
398 repo1 = backend.create_repo()
398 repo1 = backend.create_repo()
399 r1_name = repo1.repo_name
399 r1_name = repo1.repo_name
400
400
401 commit0 = commit_change(
401 commit0 = commit_change(
402 repo=r1_name, filename='file1',
402 repo=r1_name, filename=b'file1',
403 content='line1', message='commit1', vcs_type=backend.alias,
403 content=b'line1', message='commit1', vcs_type=backend.alias,
404 newfile=True)
404 newfile=True)
405 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
405 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
406
406
407 # fork the repo1
407 # fork the repo1
408 repo2 = backend.create_fork()
408 repo2 = backend.create_fork()
409 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
409 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
410
410
411 self.r2_id = repo2.repo_id
411 self.r2_id = repo2.repo_id
412 r2_name = repo2.repo_name
412 r2_name = repo2.repo_name
413
413
414 commit1 = commit_change(
414 commit1 = commit_change(
415 repo=r2_name, filename='file1-fork',
415 repo=r2_name, filename=b'file1-fork',
416 content='file1-line1-from-fork', message='commit1-fork',
416 content=b'file1-line1-from-fork', message='commit1-fork',
417 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
417 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
418 newfile=True)
418 newfile=True)
419
419
420 commit2 = commit_change(
420 commit2 = commit_change(
421 repo=r2_name, filename='file2-fork',
421 repo=r2_name, filename=b'file2-fork',
422 content='file2-line1-from-fork', message='commit2-fork',
422 content=b'file2-line1-from-fork', message='commit2-fork',
423 vcs_type=backend.alias, parent=commit1,
423 vcs_type=backend.alias, parent=commit1,
424 newfile=True)
424 newfile=True)
425
425
426 commit_change( # commit 3
426 commit_change( # commit 3
427 repo=r2_name, filename='file3-fork',
427 repo=r2_name, filename=b'file3-fork',
428 content='file3-line1-from-fork', message='commit3-fork',
428 content=b'file3-line1-from-fork', message='commit3-fork',
429 vcs_type=backend.alias, parent=commit2, newfile=True)
429 vcs_type=backend.alias, parent=commit2, newfile=True)
430
430
431 # compare !
431 # compare !
432 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
432 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
433 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
433 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
434
434
435 response = self.app.get(
435 response = self.app.get(
436 route_path('repo_compare',
436 route_path('repo_compare',
437 repo_name=r2_name,
437 repo_name=r2_name,
438 source_ref_type="branch", source_ref=commit_id1,
438 source_ref_type="branch", source_ref=commit_id1,
439 target_ref_type="branch", target_ref=commit_id2,
439 target_ref_type="branch", target_ref=commit_id2,
440 params=dict(merge='1', target_repo=r1_name),
440 params=dict(merge='1', target_repo=r1_name),
441 ))
441 ))
442
442
443 response.mustcontain('%s@%s' % (r2_name, commit_id1))
443 response.mustcontain('%s@%s' % (r2_name, commit_id1))
444 response.mustcontain('%s@%s' % (r1_name, commit_id2))
444 response.mustcontain('%s@%s' % (r1_name, commit_id2))
445 response.mustcontain('No files')
445 response.mustcontain('No files')
446 response.mustcontain('No commits in this compare')
446 response.mustcontain('No commits in this compare')
447
447
448 commit0 = commit_change(
448 commit0 = commit_change(
449 repo=r1_name, filename='file2',
449 repo=r1_name, filename=b'file2',
450 content='line1-added-after-fork', message='commit2-parent',
450 content=b'line1-added-after-fork', message='commit2-parent',
451 vcs_type=backend.alias, parent=None, newfile=True)
451 vcs_type=backend.alias, parent=None, newfile=True)
452
452
453 # compare !
453 # compare !
454 response = self.app.get(
454 response = self.app.get(
455 route_path('repo_compare',
455 route_path('repo_compare',
456 repo_name=r2_name,
456 repo_name=r2_name,
457 source_ref_type="branch", source_ref=commit_id1,
457 source_ref_type="branch", source_ref=commit_id1,
458 target_ref_type="branch", target_ref=commit_id2,
458 target_ref_type="branch", target_ref=commit_id2,
459 params=dict(merge='1', target_repo=r1_name),
459 params=dict(merge='1', target_repo=r1_name),
460 ))
460 ))
461
461
462 response.mustcontain('%s@%s' % (r2_name, commit_id1))
462 response.mustcontain('%s@%s' % (r2_name, commit_id1))
463 response.mustcontain('%s@%s' % (r1_name, commit_id2))
463 response.mustcontain('%s@%s' % (r1_name, commit_id2))
464
464
465 response.mustcontain("""commit2-parent""")
465 response.mustcontain("""commit2-parent""")
466 response.mustcontain("""line1-added-after-fork""")
466 response.mustcontain("""line1-added-after-fork""")
467 compare_page = ComparePage(response)
467 compare_page = ComparePage(response)
468 compare_page.contains_change_summary(1, 1, 0)
468 compare_page.contains_change_summary(1, 1, 0)
469
469
470 @pytest.mark.xfail_backends("svn")
470 @pytest.mark.xfail_backends("svn")
471 def test_compare_commits(self, backend, xhr_header):
471 def test_compare_commits(self, backend, xhr_header):
472 commit0 = backend.repo.get_commit(commit_idx=0)
472 commit0 = backend.repo.get_commit(commit_idx=0)
473 commit1 = backend.repo.get_commit(commit_idx=1)
473 commit1 = backend.repo.get_commit(commit_idx=1)
474
474
475 response = self.app.get(
475 response = self.app.get(
476 route_path('repo_compare',
476 route_path('repo_compare',
477 repo_name=backend.repo_name,
477 repo_name=backend.repo_name,
478 source_ref_type="rev", source_ref=commit0.raw_id,
478 source_ref_type="rev", source_ref=commit0.raw_id,
479 target_ref_type="rev", target_ref=commit1.raw_id,
479 target_ref_type="rev", target_ref=commit1.raw_id,
480 params=dict(merge='1')
480 params=dict(merge='1')
481 ),
481 ),
482 extra_environ=xhr_header, )
482 extra_environ=xhr_header, )
483
483
484 # outgoing commits between those commits
484 # outgoing commits between those commits
485 compare_page = ComparePage(response)
485 compare_page = ComparePage(response)
486 compare_page.contains_commits(commits=[commit1])
486 compare_page.contains_commits(commits=[commit1])
487
487
488 def test_errors_when_comparing_unknown_source_repo(self, backend):
488 def test_errors_when_comparing_unknown_source_repo(self, backend):
489 repo = backend.repo
489 repo = backend.repo
490 badrepo = 'badrepo'
491
490
492 response = self.app.get(
491 self.app.get(
493 route_path('repo_compare',
492 route_path('repo_compare',
494 repo_name=badrepo,
493 repo_name='badrepo',
495 source_ref_type="rev", source_ref='tip',
494 source_ref_type="rev", source_ref='tip',
496 target_ref_type="rev", target_ref='tip',
495 target_ref_type="rev", target_ref='tip',
497 params=dict(merge='1', target_repo=repo.repo_name)
496 params=dict(merge='1', target_repo=repo.repo_name)
498 ),
497 ),
499 status=404)
498 status=404)
500
499
501 def test_errors_when_comparing_unknown_target_repo(self, backend):
500 def test_errors_when_comparing_unknown_target_repo(self, backend):
502 repo = backend.repo
501 repo = backend.repo
503 badrepo = 'badrepo'
502 badrepo = 'badrepo'
504
503
505 response = self.app.get(
504 response = self.app.get(
506 route_path('repo_compare',
505 route_path('repo_compare',
507 repo_name=repo.repo_name,
506 repo_name=repo.repo_name,
508 source_ref_type="rev", source_ref='tip',
507 source_ref_type="rev", source_ref='tip',
509 target_ref_type="rev", target_ref='tip',
508 target_ref_type="rev", target_ref='tip',
510 params=dict(merge='1', target_repo=badrepo),
509 params=dict(merge='1', target_repo=badrepo),
511 ),
510 ),
512 status=302)
511 status=302)
513 redirected = response.follow()
512 redirected = response.follow()
514 redirected.mustcontain(
513 redirected.mustcontain(
515 'Could not find the target repo: `{}`'.format(badrepo))
514 'Could not find the target repo: `{}`'.format(badrepo))
516
515
517 def test_compare_not_in_preview_mode(self, backend_stub):
516 def test_compare_not_in_preview_mode(self, backend_stub):
518 commit0 = backend_stub.repo.get_commit(commit_idx=0)
517 commit0 = backend_stub.repo.get_commit(commit_idx=0)
519 commit1 = backend_stub.repo.get_commit(commit_idx=1)
518 commit1 = backend_stub.repo.get_commit(commit_idx=1)
520
519
521 response = self.app.get(
520 response = self.app.get(
522 route_path('repo_compare',
521 route_path('repo_compare',
523 repo_name=backend_stub.repo_name,
522 repo_name=backend_stub.repo_name,
524 source_ref_type="rev", source_ref=commit0.raw_id,
523 source_ref_type="rev", source_ref=commit0.raw_id,
525 target_ref_type="rev", target_ref=commit1.raw_id,
524 target_ref_type="rev", target_ref=commit1.raw_id,
526 ))
525 ))
527
526
528 # outgoing commits between those commits
527 # outgoing commits between those commits
529 compare_page = ComparePage(response)
528 compare_page = ComparePage(response)
530 compare_page.swap_is_visible()
529 compare_page.swap_is_visible()
531 compare_page.target_source_are_enabled()
530 compare_page.target_source_are_enabled()
532
531
533 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
532 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
534 orig = backend_hg.create_repo(number_of_commits=1)
533 orig = backend_hg.create_repo(number_of_commits=1)
535 fork = backend_hg.create_fork()
534 fork = backend_hg.create_fork()
536
535
537 settings_util.create_repo_rhodecode_ui(
536 settings_util.create_repo_rhodecode_ui(
538 orig, 'extensions', value='', key='largefiles', active=False)
537 orig, 'extensions', value='', key='largefiles', active=False)
539 settings_util.create_repo_rhodecode_ui(
538 settings_util.create_repo_rhodecode_ui(
540 fork, 'extensions', value='', key='largefiles', active=True)
539 fork, 'extensions', value='', key='largefiles', active=True)
541
540
542 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
541 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
543 'MercurialRepository.compare')
542 'MercurialRepository.compare')
544 with mock.patch(compare_module) as compare_mock:
543 with mock.patch(compare_module) as compare_mock:
545 compare_mock.side_effect = RepositoryRequirementError()
544 compare_mock.side_effect = RepositoryRequirementError()
546
545
547 response = self.app.get(
546 response = self.app.get(
548 route_path('repo_compare',
547 route_path('repo_compare',
549 repo_name=orig.repo_name,
548 repo_name=orig.repo_name,
550 source_ref_type="rev", source_ref="tip",
549 source_ref_type="rev", source_ref="tip",
551 target_ref_type="rev", target_ref="tip",
550 target_ref_type="rev", target_ref="tip",
552 params=dict(merge='1', target_repo=fork.repo_name),
551 params=dict(merge='1', target_repo=fork.repo_name),
553 ),
552 ),
554 status=302)
553 status=302)
555
554
556 assert_session_flash(
555 assert_session_flash(
557 response,
556 response,
558 'Could not compare repos with different large file settings')
557 'Could not compare repos with different large file settings')
559
558
560
559
561 @pytest.mark.usefixtures("autologin_user")
560 @pytest.mark.usefixtures("autologin_user")
562 class TestCompareControllerSvn(object):
561 class TestCompareControllerSvn(object):
563
562
564 def test_supports_references_with_path(self, app, backend_svn):
563 def test_supports_references_with_path(self, app, backend_svn):
565 repo = backend_svn['svn-simple-layout']
564 repo = backend_svn['svn-simple-layout']
566 commit_id = repo.get_commit(commit_idx=-1).raw_id
565 commit_id = repo.get_commit(commit_idx=-1).raw_id
567 response = app.get(
566 response = app.get(
568 route_path('repo_compare',
567 route_path('repo_compare',
569 repo_name=repo.repo_name,
568 repo_name=repo.repo_name,
570 source_ref_type="tag",
569 source_ref_type="tag",
571 source_ref="%s@%s" % ('tags/v0.1', commit_id),
570 source_ref="%s@%s" % ('tags/v0.1', commit_id),
572 target_ref_type="tag",
571 target_ref_type="tag",
573 target_ref="%s@%s" % ('tags/v0.2', commit_id),
572 target_ref="%s@%s" % ('tags/v0.2', commit_id),
574 params=dict(merge='1'),
573 params=dict(merge='1'),
575 ),
574 ),
576 status=200)
575 status=200)
577
576
578 # Expecting no commits, since both paths are at the same revision
577 # Expecting no commits, since both paths are at the same revision
579 response.mustcontain('No commits in this compare')
578 response.mustcontain('No commits in this compare')
580
579
581 # Should find only one file changed when comparing those two tags
580 # Should find only one file changed when comparing those two tags
582 response.mustcontain('example.py')
581 response.mustcontain('example.py')
583 compare_page = ComparePage(response)
582 compare_page = ComparePage(response)
584 compare_page.contains_change_summary(1, 5, 1)
583 compare_page.contains_change_summary(1, 5, 1)
585
584
586 def test_shows_commits_if_different_ids(self, app, backend_svn):
585 def test_shows_commits_if_different_ids(self, app, backend_svn):
587 repo = backend_svn['svn-simple-layout']
586 repo = backend_svn['svn-simple-layout']
588 source_id = repo.get_commit(commit_idx=-6).raw_id
587 source_id = repo.get_commit(commit_idx=-6).raw_id
589 target_id = repo.get_commit(commit_idx=-1).raw_id
588 target_id = repo.get_commit(commit_idx=-1).raw_id
590 response = app.get(
589 response = app.get(
591 route_path('repo_compare',
590 route_path('repo_compare',
592 repo_name=repo.repo_name,
591 repo_name=repo.repo_name,
593 source_ref_type="tag",
592 source_ref_type="tag",
594 source_ref="%s@%s" % ('tags/v0.1', source_id),
593 source_ref="%s@%s" % ('tags/v0.1', source_id),
595 target_ref_type="tag",
594 target_ref_type="tag",
596 target_ref="%s@%s" % ('tags/v0.2', target_id),
595 target_ref="%s@%s" % ('tags/v0.2', target_id),
597 params=dict(merge='1')
596 params=dict(merge='1')
598 ),
597 ),
599 status=200)
598 status=200)
600
599
601 # It should show commits
600 # It should show commits
602 assert 'No commits in this compare' not in response.text
601 assert 'No commits in this compare' not in response.text
603
602
604 # Should find only one file changed when comparing those two tags
603 # Should find only one file changed when comparing those two tags
605 response.mustcontain('example.py')
604 response.mustcontain('example.py')
606 compare_page = ComparePage(response)
605 compare_page = ComparePage(response)
607 compare_page.contains_change_summary(1, 5, 1)
606 compare_page.contains_change_summary(1, 5, 1)
608
607
609
608
610 class ComparePage(AssertResponse):
609 class ComparePage(AssertResponse):
611 """
610 """
612 Abstracts the page template from the tests
611 Abstracts the page template from the tests
613 """
612 """
614
613
615 def contains_file_links_and_anchors(self, files):
614 def contains_file_links_and_anchors(self, files):
616 doc = lxml.html.fromstring(self.response.body)
615 doc = lxml.html.fromstring(self.response.body)
617 for filename, file_id in files:
616 for filename, file_id in files:
618 self.contains_one_anchor(file_id)
617 self.contains_one_anchor(file_id)
619 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
618 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
620 assert len(diffblock) == 2
619 assert len(diffblock) == 2
621 for lnk in diffblock[0].cssselect('a'):
620 for lnk in diffblock[0].cssselect('a'):
622 if 'permalink' in lnk.text:
621 if 'permalink' in lnk.text:
623 assert '#{}'.format(file_id) in lnk.attrib['href']
622 assert '#{}'.format(file_id) in lnk.attrib['href']
624 break
623 break
625 else:
624 else:
626 pytest.fail('Unable to find permalink')
625 pytest.fail('Unable to find permalink')
627
626
628 def contains_change_summary(self, files_changed, inserted, deleted):
627 def contains_change_summary(self, files_changed, inserted, deleted):
629 template = (
628 template = (
630 '{files_changed} file{plural} changed: '
629 '{files_changed} file{plural} changed: '
631 '<span class="op-added">{inserted} inserted</span>, <span class="op-deleted">{deleted} deleted</span>')
630 '<span class="op-added">{inserted} inserted</span>, <span class="op-deleted">{deleted} deleted</span>')
632 self.response.mustcontain(template.format(
631 self.response.mustcontain(template.format(
633 files_changed=files_changed,
632 files_changed=files_changed,
634 plural="s" if files_changed > 1 else "",
633 plural="s" if files_changed > 1 else "",
635 inserted=inserted,
634 inserted=inserted,
636 deleted=deleted))
635 deleted=deleted))
637
636
638 def contains_commits(self, commits, ancestors=None):
637 def contains_commits(self, commits, ancestors=None):
639 response = self.response
638 response = self.response
640
639
641 for commit in commits:
640 for commit in commits:
642 # Expecting to see the commit message in an element which
641 # Expecting to see the commit message in an element which
643 # has the ID "c-{commit.raw_id}"
642 # has the ID "c-{commit.raw_id}"
644 self.element_contains('#c-' + commit.raw_id, commit.message)
643 self.element_contains('#c-' + commit.raw_id, commit.message)
645 self.contains_one_link(
644 self.contains_one_link(
646 'r%s:%s' % (commit.idx, commit.short_id),
645 'r%s:%s' % (commit.idx, commit.short_id),
647 self._commit_url(commit))
646 self._commit_url(commit))
648
647
649 if ancestors:
648 if ancestors:
650 response.mustcontain('Ancestor')
649 response.mustcontain('Ancestor')
651 for ancestor in ancestors:
650 for ancestor in ancestors:
652 self.contains_one_link(
651 self.contains_one_link(
653 ancestor.short_id, self._commit_url(ancestor))
652 ancestor.short_id, self._commit_url(ancestor))
654
653
655 def _commit_url(self, commit):
654 def _commit_url(self, commit):
656 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
655 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
657
656
658 def swap_is_hidden(self):
657 def swap_is_hidden(self):
659 assert '<a id="btn-swap"' not in self.response.text
658 assert '<a id="btn-swap"' not in self.response.text
660
659
661 def swap_is_visible(self):
660 def swap_is_visible(self):
662 assert '<a id="btn-swap"' in self.response.text
661 assert '<a id="btn-swap"' in self.response.text
663
662
664 def target_source_are_disabled(self):
663 def target_source_are_disabled(self):
665 response = self.response
664 response = self.response
666 response.mustcontain("var enable_fields = false;")
665 response.mustcontain("var enable_fields = false;")
667 response.mustcontain('.select2("enable", enable_fields)')
666 response.mustcontain('.select2("enable", enable_fields)')
668
667
669 def target_source_are_enabled(self):
668 def target_source_are_enabled(self):
670 response = self.response
669 response = self.response
671 response.mustcontain("var enable_fields = true;")
670 response.mustcontain("var enable_fields = true;")
@@ -1,166 +1,168 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from .test_repo_compare import ComparePage
22 from .test_repo_compare import ComparePage
23
23
24
24
25 def route_path(name, params=None, **kwargs):
25 def route_path(name, params=None, **kwargs):
26 import urllib.request, urllib.parse, urllib.error
26 import urllib.request
27 import urllib.parse
28 import urllib.error
27
29
28 base_url = {
30 base_url = {
29 'repo_compare_select': '/{repo_name}/compare',
31 'repo_compare_select': '/{repo_name}/compare',
30 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
32 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
31 }[name].format(**kwargs)
33 }[name].format(**kwargs)
32
34
33 if params:
35 if params:
34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
35 return base_url
37 return base_url
36
38
37
39
38 @pytest.mark.usefixtures("autologin_user", "app")
40 @pytest.mark.usefixtures("autologin_user", "app")
39 class TestCompareView(object):
41 class TestCompareView(object):
40
42
41 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
43 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
42 def test_compare_tag(self, backend):
44 def test_compare_tag(self, backend):
43 tag1 = 'v0.1.2'
45 tag1 = 'v0.1.2'
44 tag2 = 'v0.1.3'
46 tag2 = 'v0.1.3'
45 response = self.app.get(
47 response = self.app.get(
46 route_path(
48 route_path(
47 'repo_compare',
49 'repo_compare',
48 repo_name=backend.repo_name,
50 repo_name=backend.repo_name,
49 source_ref_type="tag", source_ref=tag1,
51 source_ref_type="tag", source_ref=tag1,
50 target_ref_type="tag", target_ref=tag2),
52 target_ref_type="tag", target_ref=tag2),
51 status=200)
53 status=200)
52
54
53 response.mustcontain('%s@%s' % (backend.repo_name, tag1))
55 response.mustcontain('%s@%s' % (backend.repo_name, tag1))
54 response.mustcontain('%s@%s' % (backend.repo_name, tag2))
56 response.mustcontain('%s@%s' % (backend.repo_name, tag2))
55
57
56 # outgoing commits between tags
58 # outgoing commits between tags
57 commit_indexes = {
59 commit_indexes = {
58 'git': [113] + range(115, 121),
60 'git': [113] + list(range(115, 121)),
59 'hg': [112] + range(115, 121),
61 'hg': [112] + list(range(115, 121)),
60 }
62 }
61 repo = backend.repo
63 repo = backend.repo
62 commits = (repo.get_commit(commit_idx=idx)
64 commits = (repo.get_commit(commit_idx=idx)
63 for idx in commit_indexes[backend.alias])
65 for idx in commit_indexes[backend.alias])
64 compare_page = ComparePage(response)
66 compare_page = ComparePage(response)
65 compare_page.contains_change_summary(11, 94, 64)
67 compare_page.contains_change_summary(11, 94, 64)
66 compare_page.contains_commits(commits)
68 compare_page.contains_commits(commits)
67
69
68 # files diff
70 # files diff
69 short_id = short_id_new = ''
71 short_id = short_id_new = ''
70 if backend.alias == 'git':
72 if backend.alias == 'git':
71 short_id = '5a3a8fb00555'
73 short_id = '5a3a8fb00555'
72 short_id_new = '0ba5f8a46600'
74 short_id_new = '0ba5f8a46600'
73 if backend.alias == 'hg':
75 if backend.alias == 'hg':
74 short_id = '17544fbfcd33'
76 short_id = '17544fbfcd33'
75 short_id_new = 'a7e60bff65d5'
77 short_id_new = 'a7e60bff65d5'
76
78
77 compare_page.contains_file_links_and_anchors([
79 compare_page.contains_file_links_and_anchors([
78 # modified
80 # modified
79 ('docs/api/utils/index.rst', 'a_c-{}-1c5cf9e91c12'.format(short_id)),
81 ('docs/api/utils/index.rst', 'a_c-{}-1c5cf9e91c12'.format(short_id)),
80 ('test_and_report.sh', 'a_c-{}-e3305437df55'.format(short_id)),
82 ('test_and_report.sh', 'a_c-{}-e3305437df55'.format(short_id)),
81 # added
83 # added
82 ('.hgignore', 'a_c-{}-c8e92ef85cd1'.format(short_id_new)),
84 ('.hgignore', 'a_c-{}-c8e92ef85cd1'.format(short_id_new)),
83 ('.hgtags', 'a_c-{}-6e08b694d687'.format(short_id_new)),
85 ('.hgtags', 'a_c-{}-6e08b694d687'.format(short_id_new)),
84 ('docs/api/index.rst', 'a_c-{}-2c14b00f3393'.format(short_id_new)),
86 ('docs/api/index.rst', 'a_c-{}-2c14b00f3393'.format(short_id_new)),
85 ('vcs/__init__.py', 'a_c-{}-430ccbc82bdf'.format(short_id_new)),
87 ('vcs/__init__.py', 'a_c-{}-430ccbc82bdf'.format(short_id_new)),
86 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(short_id_new)),
88 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(short_id_new)),
87 ('vcs/utils/__init__.py', 'a_c-{}-ebb592c595c0'.format(short_id_new)),
89 ('vcs/utils/__init__.py', 'a_c-{}-ebb592c595c0'.format(short_id_new)),
88 ('vcs/utils/annotate.py', 'a_c-{}-7abc741b5052'.format(short_id_new)),
90 ('vcs/utils/annotate.py', 'a_c-{}-7abc741b5052'.format(short_id_new)),
89 ('vcs/utils/diffs.py', 'a_c-{}-2ef0ef106c56'.format(short_id_new)),
91 ('vcs/utils/diffs.py', 'a_c-{}-2ef0ef106c56'.format(short_id_new)),
90 ('vcs/utils/lazy.py', 'a_c-{}-3150cb87d4b7'.format(short_id_new)),
92 ('vcs/utils/lazy.py', 'a_c-{}-3150cb87d4b7'.format(short_id_new)),
91 ])
93 ])
92
94
93 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
95 @pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
94 def test_compare_tag_branch(self, backend):
96 def test_compare_tag_branch(self, backend):
95 revisions = {
97 revisions = {
96 'hg': {
98 'hg': {
97 'tag': 'v0.2.0',
99 'tag': 'v0.2.0',
98 'branch': 'default',
100 'branch': 'default',
99 'response': (147, 5701, 10177)
101 'response': (147, 5701, 10177)
100 },
102 },
101 'git': {
103 'git': {
102 'tag': 'v0.2.2',
104 'tag': 'v0.2.2',
103 'branch': 'master',
105 'branch': 'master',
104 'response': (70, 1855, 3002)
106 'response': (70, 1855, 3002)
105 },
107 },
106 }
108 }
107
109
108 # Backend specific data, depends on the test repository for
110 # Backend specific data, depends on the test repository for
109 # functional tests.
111 # functional tests.
110 data = revisions[backend.alias]
112 data = revisions[backend.alias]
111
113
112 response = self.app.get(
114 response = self.app.get(
113 route_path(
115 route_path(
114 'repo_compare',
116 'repo_compare',
115 repo_name=backend.repo_name,
117 repo_name=backend.repo_name,
116 source_ref_type='branch', source_ref=data['branch'],
118 source_ref_type='branch', source_ref=data['branch'],
117 target_ref_type="tag", target_ref=data['tag'],
119 target_ref_type="tag", target_ref=data['tag'],
118 ))
120 ))
119
121
120 response.mustcontain('%s@%s' % (backend.repo_name, data['branch']))
122 response.mustcontain('%s@%s' % (backend.repo_name, data['branch']))
121 response.mustcontain('%s@%s' % (backend.repo_name, data['tag']))
123 response.mustcontain('%s@%s' % (backend.repo_name, data['tag']))
122 compare_page = ComparePage(response)
124 compare_page = ComparePage(response)
123 compare_page.contains_change_summary(*data['response'])
125 compare_page.contains_change_summary(*data['response'])
124
126
125 def test_index_branch(self, backend):
127 def test_index_branch(self, backend):
126 head_id = backend.default_head_id
128 head_id = backend.default_head_id
127 response = self.app.get(
129 response = self.app.get(
128 route_path(
130 route_path(
129 'repo_compare',
131 'repo_compare',
130 repo_name=backend.repo_name,
132 repo_name=backend.repo_name,
131 source_ref_type="branch", source_ref=head_id,
133 source_ref_type="branch", source_ref=head_id,
132 target_ref_type="branch", target_ref=head_id,
134 target_ref_type="branch", target_ref=head_id,
133 ))
135 ))
134
136
135 response.mustcontain('%s@%s' % (backend.repo_name, head_id))
137 response.mustcontain('%s@%s' % (backend.repo_name, head_id))
136
138
137 # branches are equal
139 # branches are equal
138 response.mustcontain('No files')
140 response.mustcontain('No files')
139 response.mustcontain('No commits in this compare')
141 response.mustcontain('No commits in this compare')
140
142
141 def test_compare_commits(self, backend):
143 def test_compare_commits(self, backend):
142 repo = backend.repo
144 repo = backend.repo
143 commit1 = repo.get_commit(commit_idx=0)
145 commit1 = repo.get_commit(commit_idx=0)
144 commit1_short_id = commit1.short_id
146 commit1_short_id = commit1.short_id
145 commit2 = repo.get_commit(commit_idx=1)
147 commit2 = repo.get_commit(commit_idx=1)
146 commit2_short_id = commit2.short_id
148 commit2_short_id = commit2.short_id
147
149
148 response = self.app.get(
150 response = self.app.get(
149 route_path(
151 route_path(
150 'repo_compare',
152 'repo_compare',
151 repo_name=backend.repo_name,
153 repo_name=backend.repo_name,
152 source_ref_type="rev", source_ref=commit1.raw_id,
154 source_ref_type="rev", source_ref=commit1.raw_id,
153 target_ref_type="rev", target_ref=commit2.raw_id,
155 target_ref_type="rev", target_ref=commit2.raw_id,
154 ))
156 ))
155 response.mustcontain('%s@%s' % (backend.repo_name, commit1.raw_id))
157 response.mustcontain('%s@%s' % (backend.repo_name, commit1.raw_id))
156 response.mustcontain('%s@%s' % (backend.repo_name, commit2.raw_id))
158 response.mustcontain('%s@%s' % (backend.repo_name, commit2.raw_id))
157 compare_page = ComparePage(response)
159 compare_page = ComparePage(response)
158
160
159 # files
161 # files
160 compare_page.contains_change_summary(1, 7, 0)
162 compare_page.contains_change_summary(1, 7, 0)
161
163
162 # outgoing commits between those commits
164 # outgoing commits between those commits
163 compare_page.contains_commits([commit2])
165 compare_page.contains_commits([commit2])
164 anchor = 'a_c-{}-c8e92ef85cd1'.format(commit2_short_id)
166 anchor = 'a_c-{}-c8e92ef85cd1'.format(commit2_short_id)
165 response.mustcontain(anchor)
167 response.mustcontain(anchor)
166 compare_page.contains_file_links_and_anchors([('.hgignore', anchor),])
168 compare_page.contains_file_links_and_anchors([('.hgignore', anchor),])
@@ -1,290 +1,292 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
22 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
23 from rhodecode.lib.vcs import nodes
23 from rhodecode.lib.vcs import nodes
24 from rhodecode.lib.vcs.backends.base import EmptyCommit
24 from rhodecode.lib.vcs.backends.base import EmptyCommit
25 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.utils import commit_change
26 from rhodecode.tests.utils import commit_change
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.request, urllib.parse, urllib.error
32 import urllib.request
33 import urllib.parse
34 import urllib.error
33
35
34 base_url = {
36 base_url = {
35 'repo_compare_select': '/{repo_name}/compare',
37 'repo_compare_select': '/{repo_name}/compare',
36 '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}',
37 }[name].format(**kwargs)
39 }[name].format(**kwargs)
38
40
39 if params:
41 if params:
40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 return base_url
43 return base_url
42
44
43
45
44 @pytest.mark.usefixtures("autologin_user", "app")
46 @pytest.mark.usefixtures("autologin_user", "app")
45 class TestSideBySideDiff(object):
47 class TestSideBySideDiff(object):
46
48
47 def test_diff_sidebyside_single_commit(self, app, backend):
49 def test_diff_sidebyside_single_commit(self, app, backend):
48 commit_id_range = {
50 commit_id_range = {
49 'hg': {
51 'hg': {
50 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
52 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
51 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
53 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
52 'changes': (21, 943, 288),
54 'changes': (21, 943, 288),
53 },
55 },
54 'git': {
56 'git': {
55 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
57 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
56 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
58 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
57 'changes': (20, 941, 286),
59 'changes': (20, 941, 286),
58 },
60 },
59
61
60 'svn': {
62 'svn': {
61 'commits': ['336',
63 'commits': ['336',
62 '337'],
64 '337'],
63 'changes': (21, 943, 288),
65 'changes': (21, 943, 288),
64 },
66 },
65 }
67 }
66
68
67 commit_info = commit_id_range[backend.alias]
69 commit_info = commit_id_range[backend.alias]
68 commit2, commit1 = commit_info['commits']
70 commit2, commit1 = commit_info['commits']
69 file_changes = commit_info['changes']
71 file_changes = commit_info['changes']
70
72
71 response = self.app.get(route_path(
73 response = self.app.get(route_path(
72 'repo_compare',
74 'repo_compare',
73 repo_name=backend.repo_name,
75 repo_name=backend.repo_name,
74 source_ref_type='rev',
76 source_ref_type='rev',
75 source_ref=commit2,
77 source_ref=commit2,
76 target_repo=backend.repo_name,
78 target_repo=backend.repo_name,
77 target_ref_type='rev',
79 target_ref_type='rev',
78 target_ref=commit1,
80 target_ref=commit1,
79 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
81 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
80 ))
82 ))
81
83
82 compare_page = ComparePage(response)
84 compare_page = ComparePage(response)
83 compare_page.contains_change_summary(*file_changes)
85 compare_page.contains_change_summary(*file_changes)
84 response.mustcontain('Collapse 1 commit')
86 response.mustcontain('Collapse 1 commit')
85
87
86 def test_diff_sidebyside_two_commits(self, app, backend):
88 def test_diff_sidebyside_two_commits(self, app, backend):
87 commit_id_range = {
89 commit_id_range = {
88 'hg': {
90 'hg': {
89 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
91 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
90 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
92 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
91 'changes': (32, 1165, 308),
93 'changes': (32, 1165, 308),
92 },
94 },
93 'git': {
95 'git': {
94 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
96 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
95 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
97 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
96 'changes': (31, 1163, 306),
98 'changes': (31, 1163, 306),
97 },
99 },
98
100
99 'svn': {
101 'svn': {
100 'commits': ['335',
102 'commits': ['335',
101 '337'],
103 '337'],
102 'changes': (32, 1179, 310),
104 'changes': (32, 1179, 310),
103 },
105 },
104 }
106 }
105
107
106 commit_info = commit_id_range[backend.alias]
108 commit_info = commit_id_range[backend.alias]
107 commit2, commit1 = commit_info['commits']
109 commit2, commit1 = commit_info['commits']
108 file_changes = commit_info['changes']
110 file_changes = commit_info['changes']
109
111
110 response = self.app.get(route_path(
112 response = self.app.get(route_path(
111 'repo_compare',
113 'repo_compare',
112 repo_name=backend.repo_name,
114 repo_name=backend.repo_name,
113 source_ref_type='rev',
115 source_ref_type='rev',
114 source_ref=commit2,
116 source_ref=commit2,
115 target_repo=backend.repo_name,
117 target_repo=backend.repo_name,
116 target_ref_type='rev',
118 target_ref_type='rev',
117 target_ref=commit1,
119 target_ref=commit1,
118 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
120 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
119 ))
121 ))
120
122
121 compare_page = ComparePage(response)
123 compare_page = ComparePage(response)
122 compare_page.contains_change_summary(*file_changes)
124 compare_page.contains_change_summary(*file_changes)
123
125
124 response.mustcontain('Collapse 2 commits')
126 response.mustcontain('Collapse 2 commits')
125
127
126 def test_diff_sidebyside_collapsed_commits(self, app, backend_svn):
128 def test_diff_sidebyside_collapsed_commits(self, app, backend_svn):
127 commit_id_range = {
129 commit_id_range = {
128
130
129 'svn': {
131 'svn': {
130 'commits': ['330',
132 'commits': ['330',
131 '337'],
133 '337'],
132
134
133 },
135 },
134 }
136 }
135
137
136 commit_info = commit_id_range['svn']
138 commit_info = commit_id_range['svn']
137 commit2, commit1 = commit_info['commits']
139 commit2, commit1 = commit_info['commits']
138
140
139 response = self.app.get(route_path(
141 response = self.app.get(route_path(
140 'repo_compare',
142 'repo_compare',
141 repo_name=backend_svn.repo_name,
143 repo_name=backend_svn.repo_name,
142 source_ref_type='rev',
144 source_ref_type='rev',
143 source_ref=commit2,
145 source_ref=commit2,
144 target_repo=backend_svn.repo_name,
146 target_repo=backend_svn.repo_name,
145 target_ref_type='rev',
147 target_ref_type='rev',
146 target_ref=commit1,
148 target_ref=commit1,
147 params=dict(target_repo=backend_svn.repo_name, diffmode='sidebyside')
149 params=dict(target_repo=backend_svn.repo_name, diffmode='sidebyside')
148 ))
150 ))
149
151
150 response.mustcontain('Expand 7 commits')
152 response.mustcontain('Expand 7 commits')
151
153
152 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
154 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
153 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
155 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
154 f_path = 'test_sidebyside_file.py'
156 f_path = b'test_sidebyside_file.py'
155 commit1_content = 'content-25d7e49c18b159446c\n'
157 commit1_content = b'content-25d7e49c18b159446c\n'
156 commit2_content = 'content-603d6c72c46d953420\n'
158 commit2_content = b'content-603d6c72c46d953420\n'
157 repo = backend.create_repo()
159 repo = backend.create_repo()
158
160
159 commit1 = commit_change(
161 commit1 = commit_change(
160 repo.repo_name, filename=f_path, content=commit1_content,
162 repo.repo_name, filename=f_path, content=commit1_content,
161 message='A', vcs_type=backend.alias, parent=None, newfile=True)
163 message='A', vcs_type=backend.alias, parent=None, newfile=True)
162
164
163 commit2 = commit_change(
165 commit2 = commit_change(
164 repo.repo_name, filename=f_path, content=commit2_content,
166 repo.repo_name, filename=f_path, content=commit2_content,
165 message='B, child of A', vcs_type=backend.alias, parent=commit1)
167 message='B, child of A', vcs_type=backend.alias, parent=commit1)
166
168
167 response = self.app.get(route_path(
169 response = self.app.get(route_path(
168 'repo_compare',
170 'repo_compare',
169 repo_name=repo.repo_name,
171 repo_name=repo.repo_name,
170 source_ref_type='rev',
172 source_ref_type='rev',
171 source_ref=EmptyCommit().raw_id,
173 source_ref=EmptyCommit().raw_id,
172 target_ref_type='rev',
174 target_ref_type='rev',
173 target_ref=commit2.raw_id,
175 target_ref=commit2.raw_id,
174 params=dict(diffmode='sidebyside')
176 params=dict(diffmode='sidebyside')
175 ))
177 ))
176
178
177 response.mustcontain('Collapse 2 commits')
179 response.mustcontain('Collapse 2 commits')
178 response.mustcontain('123 file changed')
180 response.mustcontain('123 file changed')
179
181
180 response.mustcontain(
182 response.mustcontain(
181 'r%s:%s...r%s:%s' % (
183 'r%s:%s...r%s:%s' % (
182 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
184 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
183
185
184 response.mustcontain(f_path)
186 response.mustcontain(f_path)
185
187
186 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
188 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
187 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
189 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
188 f_path = 'test_sidebyside_file.py'
190 f_path = b'test_sidebyside_file.py'
189 commit1_content = 'content-25d7e49c18b159446c\n'
191 commit1_content = b'content-25d7e49c18b159446c\n'
190 commit2_content = 'content-603d6c72c46d953420\n'
192 commit2_content = b'content-603d6c72c46d953420\n'
191 repo = backend.create_repo()
193 repo = backend.create_repo()
192
194
193 commit1 = commit_change(
195 commit1 = commit_change(
194 repo.repo_name, filename=f_path, content=commit1_content,
196 repo.repo_name, filename=f_path, content=commit1_content,
195 message='A', vcs_type=backend.alias, parent=None, newfile=True)
197 message='A', vcs_type=backend.alias, parent=None, newfile=True)
196
198
197 commit2 = commit_change(
199 commit2 = commit_change(
198 repo.repo_name, filename=f_path, content=commit2_content,
200 repo.repo_name, filename=f_path, content=commit2_content,
199 message='B, child of A', vcs_type=backend.alias, parent=commit1)
201 message='B, child of A', vcs_type=backend.alias, parent=commit1)
200
202
201 response = self.app.get(route_path(
203 response = self.app.get(route_path(
202 'repo_compare',
204 'repo_compare',
203 repo_name=repo.repo_name,
205 repo_name=repo.repo_name,
204 source_ref_type='rev',
206 source_ref_type='rev',
205 source_ref=EmptyCommit().raw_id,
207 source_ref=EmptyCommit().raw_id,
206 target_ref_type='rev',
208 target_ref_type='rev',
207 target_ref=commit2.raw_id,
209 target_ref=commit2.raw_id,
208 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
210 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
209 ))
211 ))
210
212
211 response.mustcontain('Collapse 2 commits')
213 response.mustcontain('Collapse 2 commits')
212 response.mustcontain('1 file changed')
214 response.mustcontain('1 file changed')
213
215
214 response.mustcontain(
216 response.mustcontain(
215 'r%s:%s...r%s:%s' % (
217 'r%s:%s...r%s:%s' % (
216 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
218 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
217
219
218 response.mustcontain(f_path)
220 response.mustcontain(f_path)
219
221
220 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
222 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
221 commits = [
223 commits = [
222 {'message': 'First commit'},
224 {'message': 'First commit'},
223 {'message': 'Second commit'},
225 {'message': 'Second commit'},
224 {'message': 'Commit with binary',
226 {'message': 'Commit with binary',
225 'added': [nodes.FileNode('file.empty', content='')]},
227 'added': [nodes.FileNode(b'file.empty', content=b'')]},
226 ]
228 ]
227 f_path = 'file.empty'
229 f_path = 'file.empty'
228 repo = backend.create_repo(commits=commits)
230 repo = backend.create_repo(commits=commits)
229 commit1 = repo.get_commit(commit_idx=0)
231 commit1 = repo.get_commit(commit_idx=0)
230 commit2 = repo.get_commit(commit_idx=1)
232 commit2 = repo.get_commit(commit_idx=1)
231 commit3 = repo.get_commit(commit_idx=2)
233 commit3 = repo.get_commit(commit_idx=2)
232
234
233 response = self.app.get(route_path(
235 response = self.app.get(route_path(
234 'repo_compare',
236 'repo_compare',
235 repo_name=repo.repo_name,
237 repo_name=repo.repo_name,
236 source_ref_type='rev',
238 source_ref_type='rev',
237 source_ref=commit1.raw_id,
239 source_ref=commit1.raw_id,
238 target_ref_type='rev',
240 target_ref_type='rev',
239 target_ref=commit3.raw_id,
241 target_ref=commit3.raw_id,
240 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
242 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
241 ))
243 ))
242
244
243 response.mustcontain('Collapse 2 commits')
245 response.mustcontain('Collapse 2 commits')
244 response.mustcontain('1 file changed')
246 response.mustcontain('1 file changed')
245
247
246 response.mustcontain(
248 response.mustcontain(
247 'r%s:%s...r%s:%s' % (
249 'r%s:%s...r%s:%s' % (
248 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
250 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
249
251
250 response.mustcontain(f_path)
252 response.mustcontain(f_path)
251
253
252 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
254 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
253 commit_id_range = {
255 commit_id_range = {
254 'hg': {
256 'hg': {
255 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
257 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
256 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
258 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
257 'changes': (1, 3, 3)
259 'changes': (1, 3, 3)
258 },
260 },
259 'git': {
261 'git': {
260 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
262 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
261 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
263 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
262 'changes': (1, 3, 3)
264 'changes': (1, 3, 3)
263 },
265 },
264
266
265 'svn': {
267 'svn': {
266 'commits': ['335',
268 'commits': ['335',
267 '337'],
269 '337'],
268 'changes': (1, 3, 3)
270 'changes': (1, 3, 3)
269 },
271 },
270 }
272 }
271 f_path = 'docs/conf.py'
273 f_path = 'docs/conf.py'
272
274
273 commit_info = commit_id_range[backend.alias]
275 commit_info = commit_id_range[backend.alias]
274 commit2, commit1 = commit_info['commits']
276 commit2, commit1 = commit_info['commits']
275 file_changes = commit_info['changes']
277 file_changes = commit_info['changes']
276
278
277 response = self.app.get(route_path(
279 response = self.app.get(route_path(
278 'repo_compare',
280 'repo_compare',
279 repo_name=backend.repo_name,
281 repo_name=backend.repo_name,
280 source_ref_type='rev',
282 source_ref_type='rev',
281 source_ref=commit2,
283 source_ref=commit2,
282 target_ref_type='rev',
284 target_ref_type='rev',
283 target_ref=commit1,
285 target_ref=commit1,
284 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
286 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
285 ))
287 ))
286
288
287 response.mustcontain('Collapse 2 commits')
289 response.mustcontain('Collapse 2 commits')
288
290
289 compare_page = ComparePage(response)
291 compare_page = ComparePage(response)
290 compare_page.contains_change_summary(*file_changes)
292 compare_page.contains_change_summary(*file_changes)
@@ -1,137 +1,138 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # 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
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # 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/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import pytest
20 import pytest
22 from rhodecode.model.auth_token import AuthTokenModel
21 from rhodecode.model.auth_token import AuthTokenModel
23 from rhodecode.tests import TestController
22 from rhodecode.tests import TestController
24
23
25
24
26 def route_path(name, params=None, **kwargs):
25 def route_path(name, params=None, **kwargs):
27 import urllib.request, urllib.parse, urllib.error
26 import urllib.request
27 import urllib.parse
28 import urllib.error
28
29
29 base_url = {
30 base_url = {
30 'rss_feed_home': '/{repo_name}/feed-rss',
31 'rss_feed_home': '/{repo_name}/feed-rss',
31 'atom_feed_home': '/{repo_name}/feed-atom',
32 'atom_feed_home': '/{repo_name}/feed-atom',
32 'rss_feed_home_old': '/{repo_name}/feed/rss',
33 'rss_feed_home_old': '/{repo_name}/feed/rss',
33 'atom_feed_home_old': '/{repo_name}/feed/atom',
34 'atom_feed_home_old': '/{repo_name}/feed/atom',
34 }[name].format(**kwargs)
35 }[name].format(**kwargs)
35
36
36 if params:
37 if params:
37 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 return base_url
39 return base_url
39
40
40
41
41 class TestFeedView(TestController):
42 class TestFeedView(TestController):
42
43
43 @pytest.mark.parametrize("feed_type,response_types,content_type",[
44 @pytest.mark.parametrize("feed_type,response_types,content_type",[
44 ('rss', ['<rss version="2.0"'],
45 ('rss', ['<rss version="2.0"'],
45 "application/rss+xml"),
46 "application/rss+xml"),
46 ('atom', ['xmlns="http://www.w3.org/2005/Atom"', 'xml:lang="en-us"'],
47 ('atom', ['xmlns="http://www.w3.org/2005/Atom"', 'xml:lang="en-us"'],
47 "application/atom+xml"),
48 "application/atom+xml"),
48 ])
49 ])
49 def test_feed(self, backend, feed_type, response_types, content_type):
50 def test_feed(self, backend, feed_type, response_types, content_type):
50 self.log_user()
51 self.log_user()
51 response = self.app.get(
52 response = self.app.get(
52 route_path('{}_feed_home'.format(feed_type),
53 route_path('{}_feed_home'.format(feed_type),
53 repo_name=backend.repo_name))
54 repo_name=backend.repo_name))
54
55
55 for content in response_types:
56 for content in response_types:
56 response.mustcontain(content)
57 response.mustcontain(content)
57
58
58 assert response.content_type == content_type
59 assert response.content_type == content_type
59
60
60 @pytest.mark.parametrize("feed_type, content_type", [
61 @pytest.mark.parametrize("feed_type, content_type", [
61 ('rss', "application/rss+xml"),
62 ('rss', "application/rss+xml"),
62 ('atom', "application/atom+xml")
63 ('atom', "application/atom+xml")
63 ])
64 ])
64 def test_feed_with_auth_token(
65 def test_feed_with_auth_token(
65 self, backend, user_admin, feed_type, content_type):
66 self, backend, user_admin, feed_type, content_type):
66 auth_token = user_admin.feed_token
67 auth_token = user_admin.feed_token
67 assert auth_token != ''
68 assert auth_token != ''
68
69
69 response = self.app.get(
70 response = self.app.get(
70 route_path(
71 route_path(
71 '{}_feed_home'.format(feed_type),
72 '{}_feed_home'.format(feed_type),
72 repo_name=backend.repo_name,
73 repo_name=backend.repo_name,
73 params=dict(auth_token=auth_token)),
74 params=dict(auth_token=auth_token)),
74 status=200)
75 status=200)
75
76
76 assert response.content_type == content_type
77 assert response.content_type == content_type
77
78
78 @pytest.mark.parametrize("feed_type, content_type", [
79 @pytest.mark.parametrize("feed_type, content_type", [
79 ('rss', "application/rss+xml"),
80 ('rss', "application/rss+xml"),
80 ('atom', "application/atom+xml")
81 ('atom', "application/atom+xml")
81 ])
82 ])
82 def test_feed_with_auth_token_by_uid(
83 def test_feed_with_auth_token_by_uid(
83 self, backend, user_admin, feed_type, content_type):
84 self, backend, user_admin, feed_type, content_type):
84 auth_token = user_admin.feed_token
85 auth_token = user_admin.feed_token
85 assert auth_token != ''
86 assert auth_token != ''
86
87
87 response = self.app.get(
88 response = self.app.get(
88 route_path(
89 route_path(
89 '{}_feed_home'.format(feed_type),
90 '{}_feed_home'.format(feed_type),
90 repo_name='_{}'.format(backend.repo.repo_id),
91 repo_name='_{}'.format(backend.repo.repo_id),
91 params=dict(auth_token=auth_token)),
92 params=dict(auth_token=auth_token)),
92 status=200)
93 status=200)
93
94
94 assert response.content_type == content_type
95 assert response.content_type == content_type
95
96
96 @pytest.mark.parametrize("feed_type, content_type", [
97 @pytest.mark.parametrize("feed_type, content_type", [
97 ('rss', "application/rss+xml"),
98 ('rss', "application/rss+xml"),
98 ('atom', "application/atom+xml")
99 ('atom', "application/atom+xml")
99 ])
100 ])
100 def test_feed_old_urls_with_auth_token(
101 def test_feed_old_urls_with_auth_token(
101 self, backend, user_admin, feed_type, content_type):
102 self, backend, user_admin, feed_type, content_type):
102 auth_token = user_admin.feed_token
103 auth_token = user_admin.feed_token
103 assert auth_token != ''
104 assert auth_token != ''
104
105
105 response = self.app.get(
106 response = self.app.get(
106 route_path(
107 route_path(
107 '{}_feed_home_old'.format(feed_type),
108 '{}_feed_home_old'.format(feed_type),
108 repo_name=backend.repo_name,
109 repo_name=backend.repo_name,
109 params=dict(auth_token=auth_token)),
110 params=dict(auth_token=auth_token)),
110 status=200)
111 status=200)
111
112
112 assert response.content_type == content_type
113 assert response.content_type == content_type
113
114
114 @pytest.mark.parametrize("feed_type", ['rss', 'atom'])
115 @pytest.mark.parametrize("feed_type", ['rss', 'atom'])
115 def test_feed_with_auth_token_of_wrong_type(
116 def test_feed_with_auth_token_of_wrong_type(
116 self, backend, user_util, feed_type):
117 self, backend, user_util, feed_type):
117 user = user_util.create_user()
118 user = user_util.create_user()
118 auth_token = AuthTokenModel().create(
119 auth_token = AuthTokenModel().create(
119 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_API)
120 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_API)
120 auth_token = auth_token.api_key
121 auth_token = auth_token.api_key
121
122
122 self.app.get(
123 self.app.get(
123 route_path(
124 route_path(
124 '{}_feed_home'.format(feed_type),
125 '{}_feed_home'.format(feed_type),
125 repo_name=backend.repo_name,
126 repo_name=backend.repo_name,
126 params=dict(auth_token=auth_token)),
127 params=dict(auth_token=auth_token)),
127 status=302)
128 status=302)
128
129
129 auth_token = AuthTokenModel().create(
130 auth_token = AuthTokenModel().create(
130 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
131 user.user_id, u'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
131 auth_token = auth_token.api_key
132 auth_token = auth_token.api_key
132 self.app.get(
133 self.app.get(
133 route_path(
134 route_path(
134 '{}_feed_home'.format(feed_type),
135 '{}_feed_home'.format(feed_type),
135 repo_name=backend.repo_name,
136 repo_name=backend.repo_name,
136 params=dict(auth_token=auth_token)),
137 params=dict(auth_token=auth_token)),
137 status=200)
138 status=200)
@@ -1,1091 +1,1100 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import os
20 import os
21
21
22 import mock
22 import mock
23 import pytest
23 import pytest
24
24
25 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
25 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
26 from rhodecode.apps.repository.views.repo_files import RepoFilesView
26 from rhodecode.apps.repository.views.repo_files import RepoFilesView, get_archive_name, get_path_sha
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from collections import OrderedDict
28 from collections import OrderedDict
29 from rhodecode.lib.ext_json import json
29 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.str_utils import safe_str
30 from rhodecode.lib.vcs import nodes
31 from rhodecode.lib.vcs import nodes
31
32
32 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.tests import assert_session_flash
34 from rhodecode.tests import assert_session_flash
34 from rhodecode.tests.fixture import Fixture
35 from rhodecode.tests.fixture import Fixture
35 from rhodecode.model.db import Session
36 from rhodecode.model.db import Session
36
37
37 fixture = Fixture()
38 fixture = Fixture()
38
39
39
40
40 def get_node_history(backend_type):
41 def get_node_history(backend_type):
41 return {
42 return {
42 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
43 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
43 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
44 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
44 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
45 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
45 }[backend_type]
46 }[backend_type]
46
47
47
48
48 def route_path(name, params=None, **kwargs):
49 def route_path(name, params=None, **kwargs):
49 import urllib.request, urllib.parse, urllib.error
50 import urllib.request
51 import urllib.parse
52 import urllib.error
50
53
51 base_url = {
54 base_url = {
52 'repo_summary': '/{repo_name}',
55 'repo_summary': '/{repo_name}',
53 'repo_archivefile': '/{repo_name}/archive/{fname}',
56 'repo_archivefile': '/{repo_name}/archive/{fname}',
54 'repo_files_diff': '/{repo_name}/diff/{f_path}',
57 'repo_files_diff': '/{repo_name}/diff/{f_path}',
55 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
58 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
56 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
59 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
57 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
60 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
58 'repo_files:default_commit': '/{repo_name}/files',
61 'repo_files:default_commit': '/{repo_name}/files',
59 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
62 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
60 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
63 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
61 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
64 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
62 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
65 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
63 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
66 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
64 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
67 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
65 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
68 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
66 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
69 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
67 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
70 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
68 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
71 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
69 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
72 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
70 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
73 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
71 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
74 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
72 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
75 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
73 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
76 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
74 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
77 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
75 }[name].format(**kwargs)
78 }[name].format(**kwargs)
76
79
77 if params:
80 if params:
78 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
81 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
79 return base_url
82 return base_url
80
83
81
84
82 def assert_files_in_response(response, files, params):
85 def assert_files_in_response(response, files, params):
83 template = (
86 template = (
84 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
87 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
85 _assert_items_in_response(response, files, template, params)
88 _assert_items_in_response(response, files, template, params)
86
89
87
90
88 def assert_dirs_in_response(response, dirs, params):
91 def assert_dirs_in_response(response, dirs, params):
89 template = (
92 template = (
90 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
93 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
91 _assert_items_in_response(response, dirs, template, params)
94 _assert_items_in_response(response, dirs, template, params)
92
95
93
96
94 def _assert_items_in_response(response, items, template, params):
97 def _assert_items_in_response(response, items, template, params):
95 for item in items:
98 for item in items:
96 item_params = {'name': item}
99 item_params = {'name': item}
97 item_params.update(params)
100 item_params.update(params)
98 response.mustcontain(template % item_params)
101 response.mustcontain(template % item_params)
99
102
100
103
101 def assert_timeago_in_response(response, items, params):
104 def assert_timeago_in_response(response, items, params):
102 for item in items:
105 for item in items:
103 response.mustcontain(h.age_component(params['date']))
106 response.mustcontain(h.age_component(params['date']))
104
107
105
108
106 @pytest.mark.usefixtures("app")
109 @pytest.mark.usefixtures("app")
107 class TestFilesViews(object):
110 class TestFilesViews(object):
108
111
109 def test_show_files(self, backend):
112 def test_show_files(self, backend):
110 response = self.app.get(
113 response = self.app.get(
111 route_path('repo_files',
114 route_path('repo_files',
112 repo_name=backend.repo_name,
115 repo_name=backend.repo_name,
113 commit_id='tip', f_path='/'))
116 commit_id='tip', f_path='/'))
114 commit = backend.repo.get_commit()
117 commit = backend.repo.get_commit()
115
118
116 params = {
119 params = {
117 'repo_name': backend.repo_name,
120 'repo_name': backend.repo_name,
118 'commit_id': commit.raw_id,
121 'commit_id': commit.raw_id,
119 'date': commit.date
122 'date': commit.date
120 }
123 }
121 assert_dirs_in_response(response, ['docs', 'vcs'], params)
124 assert_dirs_in_response(response, ['docs', 'vcs'], params)
122 files = [
125 files = [
123 '.gitignore',
126 '.gitignore',
124 '.hgignore',
127 '.hgignore',
125 '.hgtags',
128 '.hgtags',
126 # TODO: missing in Git
129 # TODO: missing in Git
127 # '.travis.yml',
130 # '.travis.yml',
128 'MANIFEST.in',
131 'MANIFEST.in',
129 'README.rst',
132 'README.rst',
130 # TODO: File is missing in svn repository
133 # TODO: File is missing in svn repository
131 # 'run_test_and_report.sh',
134 # 'run_test_and_report.sh',
132 'setup.cfg',
135 'setup.cfg',
133 'setup.py',
136 'setup.py',
134 'test_and_report.sh',
137 'test_and_report.sh',
135 'tox.ini',
138 'tox.ini',
136 ]
139 ]
137 assert_files_in_response(response, files, params)
140 assert_files_in_response(response, files, params)
138 assert_timeago_in_response(response, files, params)
141 assert_timeago_in_response(response, files, params)
139
142
140 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
143 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
141 repo = backend_hg['subrepos']
144 repo = backend_hg['subrepos']
142 response = self.app.get(
145 response = self.app.get(
143 route_path('repo_files',
146 route_path('repo_files',
144 repo_name=repo.repo_name,
147 repo_name=repo.repo_name,
145 commit_id='tip', f_path='/'))
148 commit_id='tip', f_path='/'))
146 assert_response = response.assert_response()
149 assert_response = response.assert_response()
147 assert_response.contains_one_link(
150 assert_response.contains_one_link(
148 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
151 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
149
152
150 def test_show_files_links_submodules_with_absolute_url_subpaths(
153 def test_show_files_links_submodules_with_absolute_url_subpaths(
151 self, backend_hg):
154 self, backend_hg):
152 repo = backend_hg['subrepos']
155 repo = backend_hg['subrepos']
153 response = self.app.get(
156 response = self.app.get(
154 route_path('repo_files',
157 route_path('repo_files',
155 repo_name=repo.repo_name,
158 repo_name=repo.repo_name,
156 commit_id='tip', f_path='/'))
159 commit_id='tip', f_path='/'))
157 assert_response = response.assert_response()
160 assert_response = response.assert_response()
158 assert_response.contains_one_link(
161 assert_response.contains_one_link(
159 'subpaths-path @ 000000000000',
162 'subpaths-path @ 000000000000',
160 'http://sub-base.example.com/subpaths-path')
163 'http://sub-base.example.com/subpaths-path')
161
164
162 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
165 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
163 def test_files_menu(self, backend):
166 def test_files_menu(self, backend):
164 new_branch = "temp_branch_name"
167 new_branch = "temp_branch_name"
165 commits = [
168 commits = [
166 {'message': 'a'},
169 {'message': 'a'},
167 {'message': 'b', 'branch': new_branch}
170 {'message': 'b', 'branch': new_branch}
168 ]
171 ]
169 backend.create_repo(commits)
172 backend.create_repo(commits)
170 backend.repo.landing_rev = "branch:%s" % new_branch
173 backend.repo.landing_rev = "branch:%s" % new_branch
171 Session().commit()
174 Session().commit()
172
175
173 # get response based on tip and not new commit
176 # get response based on tip and not new commit
174 response = self.app.get(
177 response = self.app.get(
175 route_path('repo_files',
178 route_path('repo_files',
176 repo_name=backend.repo_name,
179 repo_name=backend.repo_name,
177 commit_id='tip', f_path='/'))
180 commit_id='tip', f_path='/'))
178
181
179 # make sure Files menu url is not tip but new commit
182 # make sure Files menu url is not tip but new commit
180 landing_rev = backend.repo.landing_ref_name
183 landing_rev = backend.repo.landing_ref_name
181 files_url = route_path('repo_files:default_path',
184 files_url = route_path('repo_files:default_path',
182 repo_name=backend.repo_name,
185 repo_name=backend.repo_name,
183 commit_id=landing_rev, params={'at': landing_rev})
186 commit_id=landing_rev, params={'at': landing_rev})
184
187
185 assert landing_rev != 'tip'
188 assert landing_rev != 'tip'
186 response.mustcontain(
189 response.mustcontain(
187 '<li class="active"><a class="menulink" href="%s">' % files_url)
190 '<li class="active"><a class="menulink" href="%s">' % files_url)
188
191
189 def test_show_files_commit(self, backend):
192 def test_show_files_commit(self, backend):
190 commit = backend.repo.get_commit(commit_idx=32)
193 commit = backend.repo.get_commit(commit_idx=32)
191
194
192 response = self.app.get(
195 response = self.app.get(
193 route_path('repo_files',
196 route_path('repo_files',
194 repo_name=backend.repo_name,
197 repo_name=backend.repo_name,
195 commit_id=commit.raw_id, f_path='/'))
198 commit_id=commit.raw_id, f_path='/'))
196
199
197 dirs = ['docs', 'tests']
200 dirs = ['docs', 'tests']
198 files = ['README.rst']
201 files = ['README.rst']
199 params = {
202 params = {
200 'repo_name': backend.repo_name,
203 'repo_name': backend.repo_name,
201 'commit_id': commit.raw_id,
204 'commit_id': commit.raw_id,
202 }
205 }
203 assert_dirs_in_response(response, dirs, params)
206 assert_dirs_in_response(response, dirs, params)
204 assert_files_in_response(response, files, params)
207 assert_files_in_response(response, files, params)
205
208
206 def test_show_files_different_branch(self, backend):
209 def test_show_files_different_branch(self, backend):
207 branches = dict(
210 branches = dict(
208 hg=(150, ['git']),
211 hg=(150, ['git']),
209 # TODO: Git test repository does not contain other branches
212 # TODO: Git test repository does not contain other branches
210 git=(633, ['master']),
213 git=(633, ['master']),
211 # TODO: Branch support in Subversion
214 # TODO: Branch support in Subversion
212 svn=(150, [])
215 svn=(150, [])
213 )
216 )
214 idx, branches = branches[backend.alias]
217 idx, branches = branches[backend.alias]
215 commit = backend.repo.get_commit(commit_idx=idx)
218 commit = backend.repo.get_commit(commit_idx=idx)
216 response = self.app.get(
219 response = self.app.get(
217 route_path('repo_files',
220 route_path('repo_files',
218 repo_name=backend.repo_name,
221 repo_name=backend.repo_name,
219 commit_id=commit.raw_id, f_path='/'))
222 commit_id=commit.raw_id, f_path='/'))
220
223
221 assert_response = response.assert_response()
224 assert_response = response.assert_response()
222 for branch in branches:
225 for branch in branches:
223 assert_response.element_contains('.tags .branchtag', branch)
226 assert_response.element_contains('.tags .branchtag', branch)
224
227
225 def test_show_files_paging(self, backend):
228 def test_show_files_paging(self, backend):
226 repo = backend.repo
229 repo = backend.repo
227 indexes = [73, 92, 109, 1, 0]
230 indexes = [73, 92, 109, 1, 0]
228 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
231 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
229 for rev in indexes]
232 for rev in indexes]
230
233
231 for idx in idx_map:
234 for idx in idx_map:
232 response = self.app.get(
235 response = self.app.get(
233 route_path('repo_files',
236 route_path('repo_files',
234 repo_name=backend.repo_name,
237 repo_name=backend.repo_name,
235 commit_id=idx[1], f_path='/'))
238 commit_id=idx[1], f_path='/'))
236
239
237 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
240 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
238
241
239 def test_file_source(self, backend):
242 def test_file_source(self, backend):
240 commit = backend.repo.get_commit(commit_idx=167)
243 commit = backend.repo.get_commit(commit_idx=167)
241 response = self.app.get(
244 response = self.app.get(
242 route_path('repo_files',
245 route_path('repo_files',
243 repo_name=backend.repo_name,
246 repo_name=backend.repo_name,
244 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
247 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
245
248
246 msgbox = """<div class="commit">%s</div>"""
249 msgbox = """<div class="commit">%s</div>"""
247 response.mustcontain(msgbox % (commit.message, ))
250 response.mustcontain(msgbox % (commit.message, ))
248
251
249 assert_response = response.assert_response()
252 assert_response = response.assert_response()
250 if commit.branch:
253 if commit.branch:
251 assert_response.element_contains(
254 assert_response.element_contains(
252 '.tags.tags-main .branchtag', commit.branch)
255 '.tags.tags-main .branchtag', commit.branch)
253 if commit.tags:
256 if commit.tags:
254 for tag in commit.tags:
257 for tag in commit.tags:
255 assert_response.element_contains('.tags.tags-main .tagtag', tag)
258 assert_response.element_contains('.tags.tags-main .tagtag', tag)
256
259
257 def test_file_source_annotated(self, backend):
260 def test_file_source_annotated(self, backend):
258 response = self.app.get(
261 response = self.app.get(
259 route_path('repo_files:annotated',
262 route_path('repo_files:annotated',
260 repo_name=backend.repo_name,
263 repo_name=backend.repo_name,
261 commit_id='tip', f_path='vcs/nodes.py'))
264 commit_id='tip', f_path='vcs/nodes.py'))
262 expected_commits = {
265 expected_commits = {
263 'hg': 'r356',
266 'hg': 'r356',
264 'git': 'r345',
267 'git': 'r345',
265 'svn': 'r208',
268 'svn': 'r208',
266 }
269 }
267 response.mustcontain(expected_commits[backend.alias])
270 response.mustcontain(expected_commits[backend.alias])
268
271
269 def test_file_source_authors(self, backend):
272 def test_file_source_authors(self, backend):
270 response = self.app.get(
273 response = self.app.get(
271 route_path('repo_file_authors',
274 route_path('repo_file_authors',
272 repo_name=backend.repo_name,
275 repo_name=backend.repo_name,
273 commit_id='tip', f_path='vcs/nodes.py'))
276 commit_id='tip', f_path='vcs/nodes.py'))
274 expected_authors = {
277 expected_authors = {
275 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
278 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
276 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
279 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 'svn': ('marcin', 'lukasz'),
280 'svn': ('marcin', 'lukasz'),
278 }
281 }
279
282
280 for author in expected_authors[backend.alias]:
283 for author in expected_authors[backend.alias]:
281 response.mustcontain(author)
284 response.mustcontain(author)
282
285
283 def test_file_source_authors_with_annotation(self, backend):
286 def test_file_source_authors_with_annotation(self, backend):
284 response = self.app.get(
287 response = self.app.get(
285 route_path('repo_file_authors',
288 route_path('repo_file_authors',
286 repo_name=backend.repo_name,
289 repo_name=backend.repo_name,
287 commit_id='tip', f_path='vcs/nodes.py',
290 commit_id='tip', f_path='vcs/nodes.py',
288 params=dict(annotate=1)))
291 params=dict(annotate=1)))
289 expected_authors = {
292 expected_authors = {
290 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
293 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
291 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
294 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 'svn': ('marcin', 'lukasz'),
295 'svn': ('marcin', 'lukasz'),
293 }
296 }
294
297
295 for author in expected_authors[backend.alias]:
298 for author in expected_authors[backend.alias]:
296 response.mustcontain(author)
299 response.mustcontain(author)
297
300
298 def test_file_source_history(self, backend, xhr_header):
301 def test_file_source_history(self, backend, xhr_header):
299 response = self.app.get(
302 response = self.app.get(
300 route_path('repo_file_history',
303 route_path('repo_file_history',
301 repo_name=backend.repo_name,
304 repo_name=backend.repo_name,
302 commit_id='tip', f_path='vcs/nodes.py'),
305 commit_id='tip', f_path='vcs/nodes.py'),
303 extra_environ=xhr_header)
306 extra_environ=xhr_header)
304 assert get_node_history(backend.alias) == json.loads(response.body)
307 assert get_node_history(backend.alias) == json.loads(response.body)
305
308
306 def test_file_source_history_svn(self, backend_svn, xhr_header):
309 def test_file_source_history_svn(self, backend_svn, xhr_header):
307 simple_repo = backend_svn['svn-simple-layout']
310 simple_repo = backend_svn['svn-simple-layout']
308 response = self.app.get(
311 response = self.app.get(
309 route_path('repo_file_history',
312 route_path('repo_file_history',
310 repo_name=simple_repo.repo_name,
313 repo_name=simple_repo.repo_name,
311 commit_id='tip', f_path='trunk/example.py'),
314 commit_id='tip', f_path='trunk/example.py'),
312 extra_environ=xhr_header)
315 extra_environ=xhr_header)
313
316
314 expected_data = json.loads(
317 expected_data = json.loads(
315 fixture.load_resource('svn_node_history_branches.json'))
318 fixture.load_resource('svn_node_history_branches.json'))
316
319
317 assert expected_data == response.json
320 assert expected_data == response.json
318
321
319 def test_file_source_history_with_annotation(self, backend, xhr_header):
322 def test_file_source_history_with_annotation(self, backend, xhr_header):
320 response = self.app.get(
323 response = self.app.get(
321 route_path('repo_file_history',
324 route_path('repo_file_history',
322 repo_name=backend.repo_name,
325 repo_name=backend.repo_name,
323 commit_id='tip', f_path='vcs/nodes.py',
326 commit_id='tip', f_path='vcs/nodes.py',
324 params=dict(annotate=1)),
327 params=dict(annotate=1)),
325
328
326 extra_environ=xhr_header)
329 extra_environ=xhr_header)
327 assert get_node_history(backend.alias) == json.loads(response.body)
330 assert get_node_history(backend.alias) == json.loads(response.body)
328
331
329 def test_tree_search_top_level(self, backend, xhr_header):
332 def test_tree_search_top_level(self, backend, xhr_header):
330 commit = backend.repo.get_commit(commit_idx=173)
333 commit = backend.repo.get_commit(commit_idx=173)
331 response = self.app.get(
334 response = self.app.get(
332 route_path('repo_files_nodelist',
335 route_path('repo_files_nodelist',
333 repo_name=backend.repo_name,
336 repo_name=backend.repo_name,
334 commit_id=commit.raw_id, f_path='/'),
337 commit_id=commit.raw_id, f_path='/'),
335 extra_environ=xhr_header)
338 extra_environ=xhr_header)
336 assert 'nodes' in response.json
339 assert 'nodes' in response.json
337 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
340 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
338
341
339 def test_tree_search_missing_xhr(self, backend):
342 def test_tree_search_missing_xhr(self, backend):
340 self.app.get(
343 self.app.get(
341 route_path('repo_files_nodelist',
344 route_path('repo_files_nodelist',
342 repo_name=backend.repo_name,
345 repo_name=backend.repo_name,
343 commit_id='tip', f_path='/'),
346 commit_id='tip', f_path='/'),
344 status=404)
347 status=404)
345
348
346 def test_tree_search_at_path(self, backend, xhr_header):
349 def test_tree_search_at_path(self, backend, xhr_header):
347 commit = backend.repo.get_commit(commit_idx=173)
350 commit = backend.repo.get_commit(commit_idx=173)
348 response = self.app.get(
351 response = self.app.get(
349 route_path('repo_files_nodelist',
352 route_path('repo_files_nodelist',
350 repo_name=backend.repo_name,
353 repo_name=backend.repo_name,
351 commit_id=commit.raw_id, f_path='/docs'),
354 commit_id=commit.raw_id, f_path='/docs'),
352 extra_environ=xhr_header)
355 extra_environ=xhr_header)
353 assert 'nodes' in response.json
356 assert 'nodes' in response.json
354 nodes = response.json['nodes']
357 nodes = response.json['nodes']
355 assert {'name': 'docs/api', 'type': 'dir'} in nodes
358 assert {'name': 'docs/api', 'type': 'dir'} in nodes
356 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
359 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
357
360
358 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
361 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
359 commit = backend.repo.get_commit(commit_idx=173)
362 commit = backend.repo.get_commit(commit_idx=173)
360 response = self.app.get(
363 response = self.app.get(
361 route_path('repo_files_nodelist',
364 route_path('repo_files_nodelist',
362 repo_name=backend.repo_name,
365 repo_name=backend.repo_name,
363 commit_id=commit.raw_id, f_path='/docs/api'),
366 commit_id=commit.raw_id, f_path='/docs/api'),
364 extra_environ=xhr_header)
367 extra_environ=xhr_header)
365 assert 'nodes' in response.json
368 assert 'nodes' in response.json
366 nodes = response.json['nodes']
369 nodes = response.json['nodes']
367 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
370 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
368
371
369 def test_tree_search_at_path_missing_xhr(self, backend):
372 def test_tree_search_at_path_missing_xhr(self, backend):
370 self.app.get(
373 self.app.get(
371 route_path('repo_files_nodelist',
374 route_path('repo_files_nodelist',
372 repo_name=backend.repo_name,
375 repo_name=backend.repo_name,
373 commit_id='tip', f_path='/docs'),
376 commit_id='tip', f_path='/docs'),
374 status=404)
377 status=404)
375
378
376 def test_nodetree(self, backend, xhr_header):
379 def test_nodetree(self, backend, xhr_header):
377 commit = backend.repo.get_commit(commit_idx=173)
380 commit = backend.repo.get_commit(commit_idx=173)
378 response = self.app.get(
381 response = self.app.get(
379 route_path('repo_nodetree_full',
382 route_path('repo_nodetree_full',
380 repo_name=backend.repo_name,
383 repo_name=backend.repo_name,
381 commit_id=commit.raw_id, f_path='/'),
384 commit_id=commit.raw_id, f_path='/'),
382 extra_environ=xhr_header)
385 extra_environ=xhr_header)
383
386
384 assert_response = response.assert_response()
387 assert_response = response.assert_response()
385
388
386 for attr in ['data-commit-id', 'data-date', 'data-author']:
389 for attr in ['data-commit-id', 'data-date', 'data-author']:
387 elements = assert_response.get_elements('[{}]'.format(attr))
390 elements = assert_response.get_elements('[{}]'.format(attr))
388 assert len(elements) > 1
391 assert len(elements) > 1
389
392
390 for element in elements:
393 for element in elements:
391 assert element.get(attr)
394 assert element.get(attr)
392
395
393 def test_nodetree_if_file(self, backend, xhr_header):
396 def test_nodetree_if_file(self, backend, xhr_header):
394 commit = backend.repo.get_commit(commit_idx=173)
397 commit = backend.repo.get_commit(commit_idx=173)
395 response = self.app.get(
398 response = self.app.get(
396 route_path('repo_nodetree_full',
399 route_path('repo_nodetree_full',
397 repo_name=backend.repo_name,
400 repo_name=backend.repo_name,
398 commit_id=commit.raw_id, f_path='README.rst'),
401 commit_id=commit.raw_id, f_path='README.rst'),
399 extra_environ=xhr_header)
402 extra_environ=xhr_header)
400 assert response.text == ''
403 assert response.text == ''
401
404
402 def test_nodetree_wrong_path(self, backend, xhr_header):
405 def test_nodetree_wrong_path(self, backend, xhr_header):
403 commit = backend.repo.get_commit(commit_idx=173)
406 commit = backend.repo.get_commit(commit_idx=173)
404 response = self.app.get(
407 response = self.app.get(
405 route_path('repo_nodetree_full',
408 route_path('repo_nodetree_full',
406 repo_name=backend.repo_name,
409 repo_name=backend.repo_name,
407 commit_id=commit.raw_id, f_path='/dont-exist'),
410 commit_id=commit.raw_id, f_path='/dont-exist'),
408 extra_environ=xhr_header)
411 extra_environ=xhr_header)
409
412
410 err = 'error: There is no file nor ' \
413 err = 'error: There is no file nor ' \
411 'directory at the given path'
414 'directory at the given path'
412 assert err in response.text
415 assert err in response.text
413
416
414 def test_nodetree_missing_xhr(self, backend):
417 def test_nodetree_missing_xhr(self, backend):
415 self.app.get(
418 self.app.get(
416 route_path('repo_nodetree_full',
419 route_path('repo_nodetree_full',
417 repo_name=backend.repo_name,
420 repo_name=backend.repo_name,
418 commit_id='tip', f_path='/'),
421 commit_id='tip', f_path='/'),
419 status=404)
422 status=404)
420
423
421
424
422 @pytest.mark.usefixtures("app", "autologin_user")
425 @pytest.mark.usefixtures("app", "autologin_user")
423 class TestRawFileHandling(object):
426 class TestRawFileHandling(object):
424
427
425 def test_download_file(self, backend):
428 def test_download_file(self, backend):
426 commit = backend.repo.get_commit(commit_idx=173)
429 commit = backend.repo.get_commit(commit_idx=173)
427 response = self.app.get(
430 response = self.app.get(
428 route_path('repo_file_download',
431 route_path('repo_file_download',
429 repo_name=backend.repo_name,
432 repo_name=backend.repo_name,
430 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
433 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
431
434
432 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
435 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
433 assert response.content_type == "text/x-python"
436 assert response.content_type == "text/x-python"
434
437
435 def test_download_file_wrong_cs(self, backend):
438 def test_download_file_wrong_cs(self, backend):
436 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
439 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
437
440
438 response = self.app.get(
441 response = self.app.get(
439 route_path('repo_file_download',
442 route_path('repo_file_download',
440 repo_name=backend.repo_name,
443 repo_name=backend.repo_name,
441 commit_id=raw_id, f_path='vcs/nodes.svg'),
444 commit_id=raw_id, f_path='vcs/nodes.svg'),
442 status=404)
445 status=404)
443
446
444 msg = """No such commit exists for this repository"""
447 msg = """No such commit exists for this repository"""
445 response.mustcontain(msg)
448 response.mustcontain(msg)
446
449
447 def test_download_file_wrong_f_path(self, backend):
450 def test_download_file_wrong_f_path(self, backend):
448 commit = backend.repo.get_commit(commit_idx=173)
451 commit = backend.repo.get_commit(commit_idx=173)
449 f_path = 'vcs/ERRORnodes.py'
452 f_path = 'vcs/ERRORnodes.py'
450
453
451 response = self.app.get(
454 response = self.app.get(
452 route_path('repo_file_download',
455 route_path('repo_file_download',
453 repo_name=backend.repo_name,
456 repo_name=backend.repo_name,
454 commit_id=commit.raw_id, f_path=f_path),
457 commit_id=commit.raw_id, f_path=f_path),
455 status=404)
458 status=404)
456
459
457 msg = (
460 msg = (
458 "There is no file nor directory at the given path: "
461 "There is no file nor directory at the given path: "
459 "`%s` at commit %s" % (f_path, commit.short_id))
462 "`%s` at commit %s" % (f_path, commit.short_id))
460 response.mustcontain(msg)
463 response.mustcontain(msg)
461
464
462 def test_file_raw(self, backend):
465 def test_file_raw(self, backend):
463 commit = backend.repo.get_commit(commit_idx=173)
466 commit = backend.repo.get_commit(commit_idx=173)
464 response = self.app.get(
467 response = self.app.get(
465 route_path('repo_file_raw',
468 route_path('repo_file_raw',
466 repo_name=backend.repo_name,
469 repo_name=backend.repo_name,
467 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
470 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
468
471
469 assert response.content_type == "text/plain"
472 assert response.content_type == "text/plain"
470
473
471 def test_file_raw_binary(self, backend):
474 def test_file_raw_binary(self, backend):
472 commit = backend.repo.get_commit()
475 commit = backend.repo.get_commit()
473 response = self.app.get(
476 response = self.app.get(
474 route_path('repo_file_raw',
477 route_path('repo_file_raw',
475 repo_name=backend.repo_name,
478 repo_name=backend.repo_name,
476 commit_id=commit.raw_id,
479 commit_id=commit.raw_id,
477 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
480 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
478
481
479 assert response.content_disposition == 'inline'
482 assert response.content_disposition == 'inline'
480
483
481 def test_raw_file_wrong_cs(self, backend):
484 def test_raw_file_wrong_cs(self, backend):
482 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
485 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
483
486
484 response = self.app.get(
487 response = self.app.get(
485 route_path('repo_file_raw',
488 route_path('repo_file_raw',
486 repo_name=backend.repo_name,
489 repo_name=backend.repo_name,
487 commit_id=raw_id, f_path='vcs/nodes.svg'),
490 commit_id=raw_id, f_path='vcs/nodes.svg'),
488 status=404)
491 status=404)
489
492
490 msg = """No such commit exists for this repository"""
493 msg = """No such commit exists for this repository"""
491 response.mustcontain(msg)
494 response.mustcontain(msg)
492
495
493 def test_raw_wrong_f_path(self, backend):
496 def test_raw_wrong_f_path(self, backend):
494 commit = backend.repo.get_commit(commit_idx=173)
497 commit = backend.repo.get_commit(commit_idx=173)
495 f_path = 'vcs/ERRORnodes.py'
498 f_path = 'vcs/ERRORnodes.py'
496 response = self.app.get(
499 response = self.app.get(
497 route_path('repo_file_raw',
500 route_path('repo_file_raw',
498 repo_name=backend.repo_name,
501 repo_name=backend.repo_name,
499 commit_id=commit.raw_id, f_path=f_path),
502 commit_id=commit.raw_id, f_path=f_path),
500 status=404)
503 status=404)
501
504
502 msg = (
505 msg = (
503 "There is no file nor directory at the given path: "
506 "There is no file nor directory at the given path: "
504 "`%s` at commit %s" % (f_path, commit.short_id))
507 "`%s` at commit %s" % (f_path, commit.short_id))
505 response.mustcontain(msg)
508 response.mustcontain(msg)
506
509
507 def test_raw_svg_should_not_be_rendered(self, backend):
510 def test_raw_svg_should_not_be_rendered(self, backend):
508 backend.create_repo()
511 backend.create_repo()
509 backend.ensure_file("xss.svg")
512 backend.ensure_file(b"xss.svg")
510 response = self.app.get(
513 response = self.app.get(
511 route_path('repo_file_raw',
514 route_path('repo_file_raw',
512 repo_name=backend.repo_name,
515 repo_name=backend.repo_name,
513 commit_id='tip', f_path='xss.svg'),)
516 commit_id='tip', f_path='xss.svg'),)
514 # If the content type is image/svg+xml then it allows to render HTML
517 # If the content type is image/svg+xml then it allows to render HTML
515 # and malicious SVG.
518 # and malicious SVG.
516 assert response.content_type == "text/plain"
519 assert response.content_type == "text/plain"
517
520
518
521
519 @pytest.mark.usefixtures("app")
522 @pytest.mark.usefixtures("app")
520 class TestRepositoryArchival(object):
523 class TestRepositoryArchival(object):
521
524
522 def test_archival(self, backend):
525 def test_archival(self, backend):
523 backend.enable_downloads()
526 backend.enable_downloads()
524 commit = backend.repo.get_commit(commit_idx=173)
527 commit = backend.repo.get_commit(commit_idx=173)
525 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
528 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
529 path_sha = get_path_sha('/')
530 filename = get_archive_name(backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha)
526
531
527 short = commit.short_id + extension
528 fname = commit.raw_id + extension
532 fname = commit.raw_id + extension
529 filename = '%s-%s' % (backend.repo_name, short)
530 response = self.app.get(
533 response = self.app.get(
531 route_path('repo_archivefile',
534 route_path('repo_archivefile',
532 repo_name=backend.repo_name,
535 repo_name=backend.repo_name,
533 fname=fname))
536 fname=fname))
534
537
535 assert response.status == '200 OK'
538 assert response.status == '200 OK'
536 headers = [
539 headers = [
537 ('Content-Disposition', 'attachment; filename=%s' % filename),
540 ('Content-Disposition', 'attachment; filename=%s' % filename),
538 ('Content-Type', '%s' % content_type),
541 ('Content-Type', '%s' % content_type),
539 ]
542 ]
540
543
541 for header in headers:
544 for header in headers:
542 assert header in response.headers.items()
545 assert header in response.headers.items()
543
546
544 def test_archival_no_hash(self, backend):
547 def test_archival_no_hash(self, backend):
545 backend.enable_downloads()
548 backend.enable_downloads()
546 commit = backend.repo.get_commit(commit_idx=173)
549 commit = backend.repo.get_commit(commit_idx=173)
547 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
550 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
551 path_sha = get_path_sha('/')
552 filename = get_archive_name(backend.repo_name, commit_sha=commit.short_id, ext=extension, path_sha=path_sha, with_hash=False)
548
553
549 short = 'plain' + extension
550 fname = commit.raw_id + extension
554 fname = commit.raw_id + extension
551 filename = '%s-%s' % (backend.repo_name, short)
552 response = self.app.get(
555 response = self.app.get(
553 route_path('repo_archivefile',
556 route_path('repo_archivefile',
554 repo_name=backend.repo_name,
557 repo_name=backend.repo_name,
555 fname=fname, params={'with_hash': 0}))
558 fname=fname, params={'with_hash': 0}))
556
559
557 assert response.status == '200 OK'
560 assert response.status == '200 OK'
558 headers = [
561 headers = [
559 ('Content-Disposition', 'attachment; filename=%s' % filename),
562 ('Content-Disposition', 'attachment; filename=%s' % filename),
560 ('Content-Type', '%s' % content_type),
563 ('Content-Type', '%s' % content_type),
561 ]
564 ]
562
565
563 for header in headers:
566 for header in headers:
564 assert header in response.headers.items()
567 assert header in response.headers.items()
565
568
566 @pytest.mark.parametrize('arch_ext',[
569 @pytest.mark.parametrize('arch_ext',[
567 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
570 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
568 def test_archival_wrong_ext(self, backend, arch_ext):
571 def test_archival_wrong_ext(self, backend, arch_ext):
569 backend.enable_downloads()
572 backend.enable_downloads()
570 commit = backend.repo.get_commit(commit_idx=173)
573 commit = backend.repo.get_commit(commit_idx=173)
571
574
572 fname = commit.raw_id + '.' + arch_ext
575 fname = commit.raw_id + '.' + arch_ext
573
576
574 response = self.app.get(
577 response = self.app.get(
575 route_path('repo_archivefile',
578 route_path('repo_archivefile',
576 repo_name=backend.repo_name,
579 repo_name=backend.repo_name,
577 fname=fname))
580 fname=fname))
578 response.mustcontain(
581 response.mustcontain(
579 'Unknown archive type for: `{}`'.format(fname))
582 'Unknown archive type for: `{}`'.format(fname))
580
583
581 @pytest.mark.parametrize('commit_id', [
584 @pytest.mark.parametrize('commit_id', [
582 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
585 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
583 def test_archival_wrong_commit_id(self, backend, commit_id):
586 def test_archival_wrong_commit_id(self, backend, commit_id):
584 backend.enable_downloads()
587 backend.enable_downloads()
585 fname = '%s.zip' % commit_id
588 fname = '%s.zip' % commit_id
586
589
587 response = self.app.get(
590 response = self.app.get(
588 route_path('repo_archivefile',
591 route_path('repo_archivefile',
589 repo_name=backend.repo_name,
592 repo_name=backend.repo_name,
590 fname=fname))
593 fname=fname))
591 response.mustcontain('Unknown commit_id')
594 response.mustcontain('Unknown commit_id')
592
595
593
596
594 @pytest.mark.usefixtures("app")
597 @pytest.mark.usefixtures("app")
595 class TestFilesDiff(object):
598 class TestFilesDiff(object):
596
599
597 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
600 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
598 def test_file_full_diff(self, backend, diff):
601 def test_file_full_diff(self, backend, diff):
599 commit1 = backend.repo.get_commit(commit_idx=-1)
602 commit1 = backend.repo.get_commit(commit_idx=-1)
600 commit2 = backend.repo.get_commit(commit_idx=-2)
603 commit2 = backend.repo.get_commit(commit_idx=-2)
601
604
602 response = self.app.get(
605 response = self.app.get(
603 route_path('repo_files_diff',
606 route_path('repo_files_diff',
604 repo_name=backend.repo_name,
607 repo_name=backend.repo_name,
605 f_path='README'),
608 f_path='README'),
606 params={
609 params={
607 'diff1': commit2.raw_id,
610 'diff1': commit2.raw_id,
608 'diff2': commit1.raw_id,
611 'diff2': commit1.raw_id,
609 'fulldiff': '1',
612 'fulldiff': '1',
610 'diff': diff,
613 'diff': diff,
611 })
614 })
612
615
613 if diff == 'diff':
616 if diff == 'diff':
614 # use redirect since this is OLD view redirecting to compare page
617 # use redirect since this is OLD view redirecting to compare page
615 response = response.follow()
618 response = response.follow()
616
619
617 # It's a symlink to README.rst
620 # It's a symlink to README.rst
618 response.mustcontain('README.rst')
621 response.mustcontain('README.rst')
619 response.mustcontain('No newline at end of file')
622 response.mustcontain('No newline at end of file')
620
623
621 def test_file_binary_diff(self, backend):
624 def test_file_binary_diff(self, backend):
622 commits = [
625 commits = [
623 {'message': 'First commit'},
626 {'message': 'First commit'},
624 {'message': 'Commit with binary',
627 {'message': 'Commit with binary',
625 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
628 'added': [nodes.FileNode(b'file.bin', content='\0BINARY\0')]},
626 ]
629 ]
627 repo = backend.create_repo(commits=commits)
630 repo = backend.create_repo(commits=commits)
628
631
629 response = self.app.get(
632 response = self.app.get(
630 route_path('repo_files_diff',
633 route_path('repo_files_diff',
631 repo_name=backend.repo_name,
634 repo_name=backend.repo_name,
632 f_path='file.bin'),
635 f_path='file.bin'),
633 params={
636 params={
634 'diff1': repo.get_commit(commit_idx=0).raw_id,
637 'diff1': repo.get_commit(commit_idx=0).raw_id,
635 'diff2': repo.get_commit(commit_idx=1).raw_id,
638 'diff2': repo.get_commit(commit_idx=1).raw_id,
636 'fulldiff': '1',
639 'fulldiff': '1',
637 'diff': 'diff',
640 'diff': 'diff',
638 })
641 })
639 # use redirect since this is OLD view redirecting to compare page
642 # use redirect since this is OLD view redirecting to compare page
640 response = response.follow()
643 response = response.follow()
641 response.mustcontain('Collapse 1 commit')
644 response.mustcontain('Collapse 1 commit')
642 file_changes = (1, 0, 0)
645 file_changes = (1, 0, 0)
643
646
644 compare_page = ComparePage(response)
647 compare_page = ComparePage(response)
645 compare_page.contains_change_summary(*file_changes)
648 compare_page.contains_change_summary(*file_changes)
646
649
647 if backend.alias == 'svn':
650 if backend.alias == 'svn':
648 response.mustcontain('new file 10644')
651 response.mustcontain('new file 10644')
649 # TODO(marcink): SVN doesn't yet detect binary changes
652 # TODO(marcink): SVN doesn't yet detect binary changes
650 else:
653 else:
651 response.mustcontain('new file 100644')
654 response.mustcontain('new file 100644')
652 response.mustcontain('binary diff hidden')
655 response.mustcontain('binary diff hidden')
653
656
654 def test_diff_2way(self, backend):
657 def test_diff_2way(self, backend):
655 commit1 = backend.repo.get_commit(commit_idx=-1)
658 commit1 = backend.repo.get_commit(commit_idx=-1)
656 commit2 = backend.repo.get_commit(commit_idx=-2)
659 commit2 = backend.repo.get_commit(commit_idx=-2)
657 response = self.app.get(
660 response = self.app.get(
658 route_path('repo_files_diff_2way_redirect',
661 route_path('repo_files_diff_2way_redirect',
659 repo_name=backend.repo_name,
662 repo_name=backend.repo_name,
660 f_path='README'),
663 f_path='README'),
661 params={
664 params={
662 'diff1': commit2.raw_id,
665 'diff1': commit2.raw_id,
663 'diff2': commit1.raw_id,
666 'diff2': commit1.raw_id,
664 })
667 })
665 # use redirect since this is OLD view redirecting to compare page
668 # use redirect since this is OLD view redirecting to compare page
666 response = response.follow()
669 response = response.follow()
667
670
668 # It's a symlink to README.rst
671 # It's a symlink to README.rst
669 response.mustcontain('README.rst')
672 response.mustcontain('README.rst')
670 response.mustcontain('No newline at end of file')
673 response.mustcontain('No newline at end of file')
671
674
672 def test_requires_one_commit_id(self, backend, autologin_user):
675 def test_requires_one_commit_id(self, backend, autologin_user):
673 response = self.app.get(
676 response = self.app.get(
674 route_path('repo_files_diff',
677 route_path('repo_files_diff',
675 repo_name=backend.repo_name,
678 repo_name=backend.repo_name,
676 f_path='README.rst'),
679 f_path='README.rst'),
677 status=400)
680 status=400)
678 response.mustcontain(
681 response.mustcontain(
679 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
682 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
680
683
681 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
684 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
682 repo = vcsbackend.repo
685 repo = vcsbackend.repo
683 response = self.app.get(
686 response = self.app.get(
684 route_path('repo_files_diff',
687 route_path('repo_files_diff',
685 repo_name=repo.name,
688 repo_name=repo.name,
686 f_path='does-not-exist-in-any-commit'),
689 f_path='does-not-exist-in-any-commit'),
687 params={
690 params={
688 'diff1': repo[0].raw_id,
691 'diff1': repo[0].raw_id,
689 'diff2': repo[1].raw_id
692 'diff2': repo[1].raw_id
690 })
693 })
691
694
692 response = response.follow()
695 response = response.follow()
693 response.mustcontain('No files')
696 response.mustcontain('No files')
694
697
695 def test_returns_redirect_if_file_not_changed(self, backend):
698 def test_returns_redirect_if_file_not_changed(self, backend):
696 commit = backend.repo.get_commit(commit_idx=-1)
699 commit = backend.repo.get_commit(commit_idx=-1)
697 response = self.app.get(
700 response = self.app.get(
698 route_path('repo_files_diff_2way_redirect',
701 route_path('repo_files_diff_2way_redirect',
699 repo_name=backend.repo_name,
702 repo_name=backend.repo_name,
700 f_path='README'),
703 f_path='README'),
701 params={
704 params={
702 'diff1': commit.raw_id,
705 'diff1': commit.raw_id,
703 'diff2': commit.raw_id,
706 'diff2': commit.raw_id,
704 })
707 })
705
708
706 response = response.follow()
709 response = response.follow()
707 response.mustcontain('No files')
710 response.mustcontain('No files')
708 response.mustcontain('No commits in this compare')
711 response.mustcontain('No commits in this compare')
709
712
710 def test_supports_diff_to_different_path_svn(self, backend_svn):
713 def test_supports_diff_to_different_path_svn(self, backend_svn):
711 #TODO: check this case
714 #TODO: check this case
712 return
715 return
713
716
714 repo = backend_svn['svn-simple-layout'].scm_instance()
717 repo = backend_svn['svn-simple-layout'].scm_instance()
715 commit_id_1 = '24'
718 commit_id_1 = '24'
716 commit_id_2 = '26'
719 commit_id_2 = '26'
717
720
718 response = self.app.get(
721 response = self.app.get(
719 route_path('repo_files_diff',
722 route_path('repo_files_diff',
720 repo_name=backend_svn.repo_name,
723 repo_name=backend_svn.repo_name,
721 f_path='trunk/example.py'),
724 f_path='trunk/example.py'),
722 params={
725 params={
723 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
726 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
724 'diff2': commit_id_2,
727 'diff2': commit_id_2,
725 })
728 })
726
729
727 response = response.follow()
730 response = response.follow()
728 response.mustcontain(
731 response.mustcontain(
729 # diff contains this
732 # diff contains this
730 "Will print out a useful message on invocation.")
733 "Will print out a useful message on invocation.")
731
734
732 # Note: Expecting that we indicate the user what's being compared
735 # Note: Expecting that we indicate the user what's being compared
733 response.mustcontain("trunk/example.py")
736 response.mustcontain("trunk/example.py")
734 response.mustcontain("tags/v0.2/example.py")
737 response.mustcontain("tags/v0.2/example.py")
735
738
736 def test_show_rev_redirects_to_svn_path(self, backend_svn):
739 def test_show_rev_redirects_to_svn_path(self, backend_svn):
737 #TODO: check this case
740 #TODO: check this case
738 return
741 return
739
742
740 repo = backend_svn['svn-simple-layout'].scm_instance()
743 repo = backend_svn['svn-simple-layout'].scm_instance()
741 commit_id = repo[-1].raw_id
744 commit_id = repo[-1].raw_id
742
745
743 response = self.app.get(
746 response = self.app.get(
744 route_path('repo_files_diff',
747 route_path('repo_files_diff',
745 repo_name=backend_svn.repo_name,
748 repo_name=backend_svn.repo_name,
746 f_path='trunk/example.py'),
749 f_path='trunk/example.py'),
747 params={
750 params={
748 'diff1': 'branches/argparse/example.py@' + commit_id,
751 'diff1': 'branches/argparse/example.py@' + commit_id,
749 'diff2': commit_id,
752 'diff2': commit_id,
750 },
753 },
751 status=302)
754 status=302)
752 response = response.follow()
755 response = response.follow()
753 assert response.headers['Location'].endswith(
756 assert response.headers['Location'].endswith(
754 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
757 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
755
758
756 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
759 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
757 #TODO: check this case
760 #TODO: check this case
758 return
761 return
759
762
760 repo = backend_svn['svn-simple-layout'].scm_instance()
763 repo = backend_svn['svn-simple-layout'].scm_instance()
761 commit_id = repo[-1].raw_id
764 commit_id = repo[-1].raw_id
762 response = self.app.get(
765 response = self.app.get(
763 route_path('repo_files_diff',
766 route_path('repo_files_diff',
764 repo_name=backend_svn.repo_name,
767 repo_name=backend_svn.repo_name,
765 f_path='trunk/example.py'),
768 f_path='trunk/example.py'),
766 params={
769 params={
767 'diff1': 'branches/argparse/example.py@' + commit_id,
770 'diff1': 'branches/argparse/example.py@' + commit_id,
768 'diff2': commit_id,
771 'diff2': commit_id,
769 'show_rev': 'Show at Revision',
772 'show_rev': 'Show at Revision',
770 'annotate': 'true',
773 'annotate': 'true',
771 },
774 },
772 status=302)
775 status=302)
773 response = response.follow()
776 response = response.follow()
774 assert response.headers['Location'].endswith(
777 assert response.headers['Location'].endswith(
775 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
778 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
776
779
777
780
778 @pytest.mark.usefixtures("app", "autologin_user")
781 @pytest.mark.usefixtures("app", "autologin_user")
779 class TestModifyFilesWithWebInterface(object):
782 class TestModifyFilesWithWebInterface(object):
780
783
781 def test_add_file_view(self, backend):
784 def test_add_file_view(self, backend):
782 self.app.get(
785 self.app.get(
783 route_path('repo_files_add_file',
786 route_path('repo_files_add_file',
784 repo_name=backend.repo_name,
787 repo_name=backend.repo_name,
785 commit_id='tip', f_path='/')
788 commit_id='tip', f_path='/')
786 )
789 )
787
790
788 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
791 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
789 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
792 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
790 backend.create_repo()
793 backend.create_repo()
791 filename = 'init.py'
794 filename = 'init.py'
792 response = self.app.post(
795 response = self.app.post(
793 route_path('repo_files_create_file',
796 route_path('repo_files_create_file',
794 repo_name=backend.repo_name,
797 repo_name=backend.repo_name,
795 commit_id='tip', f_path='/'),
798 commit_id='tip', f_path='/'),
796 params={
799 params={
797 'content': "",
800 'content': "",
798 'filename': filename,
801 'filename': filename,
799 'csrf_token': csrf_token,
802 'csrf_token': csrf_token,
800 },
803 },
801 status=302)
804 status=302)
802 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
805 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
803 assert_session_flash(response, expected_msg)
806 assert_session_flash(response, expected_msg)
804
807
805 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
808 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
806 commit_id = backend.repo.get_commit().raw_id
809 commit_id = backend.repo.get_commit().raw_id
807 response = self.app.post(
810 response = self.app.post(
808 route_path('repo_files_create_file',
811 route_path('repo_files_create_file',
809 repo_name=backend.repo_name,
812 repo_name=backend.repo_name,
810 commit_id=commit_id, f_path='/'),
813 commit_id=commit_id, f_path='/'),
811 params={
814 params={
812 'content': "foo",
815 'content': "foo",
813 'csrf_token': csrf_token,
816 'csrf_token': csrf_token,
814 },
817 },
815 status=302)
818 status=302)
816
819
817 assert_session_flash(response, 'No filename specified')
820 assert_session_flash(response, 'No filename specified')
818
821
819 def test_add_file_into_repo_errors_and_no_commits(
822 def test_add_file_into_repo_errors_and_no_commits(
820 self, backend, csrf_token):
823 self, backend, csrf_token):
821 repo = backend.create_repo()
824 repo = backend.create_repo()
822 # Create a file with no filename, it will display an error but
825 # Create a file with no filename, it will display an error but
823 # the repo has no commits yet
826 # the repo has no commits yet
824 response = self.app.post(
827 response = self.app.post(
825 route_path('repo_files_create_file',
828 route_path('repo_files_create_file',
826 repo_name=repo.repo_name,
829 repo_name=repo.repo_name,
827 commit_id='tip', f_path='/'),
830 commit_id='tip', f_path='/'),
828 params={
831 params={
829 'content': "foo",
832 'content': "foo",
830 'csrf_token': csrf_token,
833 'csrf_token': csrf_token,
831 },
834 },
832 status=302)
835 status=302)
833
836
834 assert_session_flash(response, 'No filename specified')
837 assert_session_flash(response, 'No filename specified')
835
838
836 # Not allowed, redirect to the summary
839 # Not allowed, redirect to the summary
837 redirected = response.follow()
840 redirected = response.follow()
838 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
841 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
839
842
840 # As there are no commits, displays the summary page with the error of
843 # As there are no commits, displays the summary page with the error of
841 # creating a file with no filename
844 # creating a file with no filename
842
845
843 assert redirected.request.path == summary_url
846 assert redirected.request.path == summary_url
844
847
845 @pytest.mark.parametrize("filename, clean_filename", [
848 @pytest.mark.parametrize("filename, clean_filename", [
846 ('/abs/foo', 'abs/foo'),
849 ('/abs/foo', 'abs/foo'),
847 ('../rel/foo', 'rel/foo'),
850 ('../rel/foo', 'rel/foo'),
848 ('file/../foo/foo', 'file/foo/foo'),
851 ('file/../foo/foo', 'file/foo/foo'),
849 ])
852 ])
850 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
853 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
851 repo = backend.create_repo()
854 repo = backend.create_repo()
852 commit_id = repo.get_commit().raw_id
855 commit_id = repo.get_commit().raw_id
853
856
854 response = self.app.post(
857 response = self.app.post(
855 route_path('repo_files_create_file',
858 route_path('repo_files_create_file',
856 repo_name=repo.repo_name,
859 repo_name=repo.repo_name,
857 commit_id=commit_id, f_path='/'),
860 commit_id=commit_id, f_path='/'),
858 params={
861 params={
859 'content': "foo",
862 'content': "foo",
860 'filename': filename,
863 'filename': filename,
861 'csrf_token': csrf_token,
864 'csrf_token': csrf_token,
862 },
865 },
863 status=302)
866 status=302)
864
867
865 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
868 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
866 assert_session_flash(response, expected_msg)
869 assert_session_flash(response, expected_msg)
867
870
868 @pytest.mark.parametrize("cnt, filename, content", [
871 @pytest.mark.parametrize("cnt, filename, content", [
869 (1, 'foo.txt', "Content"),
872 (1, 'foo.txt', "Content"),
870 (2, 'dir/foo.rst', "Content"),
873 (2, 'dir/foo.rst', "Content"),
871 (3, 'dir/foo-second.rst', "Content"),
874 (3, 'dir/foo-second.rst', "Content"),
872 (4, 'rel/dir/foo.bar', "Content"),
875 (4, 'rel/dir/foo.bar', "Content"),
873 ])
876 ])
874 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
877 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
875 repo = backend.create_repo()
878 repo = backend.create_repo()
876 commit_id = repo.get_commit().raw_id
879 commit_id = repo.get_commit().raw_id
877 response = self.app.post(
880 response = self.app.post(
878 route_path('repo_files_create_file',
881 route_path('repo_files_create_file',
879 repo_name=repo.repo_name,
882 repo_name=repo.repo_name,
880 commit_id=commit_id, f_path='/'),
883 commit_id=commit_id, f_path='/'),
881 params={
884 params={
882 'content': content,
885 'content': content,
883 'filename': filename,
886 'filename': filename,
884 'csrf_token': csrf_token,
887 'csrf_token': csrf_token,
885 },
888 },
886 status=302)
889 status=302)
887
890
888 expected_msg = 'Successfully committed new file `{}`'.format(filename)
891 expected_msg = 'Successfully committed new file `{}`'.format(filename)
889 assert_session_flash(response, expected_msg)
892 assert_session_flash(response, expected_msg)
890
893
891 def test_edit_file_view(self, backend):
894 def test_edit_file_view(self, backend):
892 response = self.app.get(
895 response = self.app.get(
893 route_path('repo_files_edit_file',
896 route_path('repo_files_edit_file',
894 repo_name=backend.repo_name,
897 repo_name=backend.repo_name,
895 commit_id=backend.default_head_id,
898 commit_id=backend.default_head_id,
896 f_path='vcs/nodes.py'),
899 f_path='vcs/nodes.py'),
897 status=200)
900 status=200)
898 response.mustcontain("Module holding everything related to vcs nodes.")
901 response.mustcontain("Module holding everything related to vcs nodes.")
899
902
900 def test_edit_file_view_not_on_branch(self, backend):
903 def test_edit_file_view_not_on_branch(self, backend):
901 repo = backend.create_repo()
904 repo = backend.create_repo()
902 backend.ensure_file("vcs/nodes.py")
905 backend.ensure_file(b"vcs/nodes.py")
903
906
904 response = self.app.get(
907 response = self.app.get(
905 route_path('repo_files_edit_file',
908 route_path('repo_files_edit_file',
906 repo_name=repo.repo_name,
909 repo_name=repo.repo_name,
907 commit_id='tip',
910 commit_id='tip',
908 f_path='vcs/nodes.py'),
911 f_path='vcs/nodes.py'),
909 status=302)
912 status=302)
910 assert_session_flash(
913 assert_session_flash(
911 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
914 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
912
915
913 def test_edit_file_view_commit_changes(self, backend, csrf_token):
916 def test_edit_file_view_commit_changes(self, backend, csrf_token):
914 repo = backend.create_repo()
917 repo = backend.create_repo()
915 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
918 backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'")
916
919
917 response = self.app.post(
920 response = self.app.post(
918 route_path('repo_files_update_file',
921 route_path('repo_files_update_file',
919 repo_name=repo.repo_name,
922 repo_name=repo.repo_name,
920 commit_id=backend.default_head_id,
923 commit_id=backend.default_head_id,
921 f_path='vcs/nodes.py'),
924 f_path='vcs/nodes.py'),
922 params={
925 params={
923 'content': "print 'hello world'",
926 'content': "print 'hello world'",
924 'message': 'I committed',
927 'message': 'I committed',
925 'filename': "vcs/nodes.py",
928 'filename': "vcs/nodes.py",
926 'csrf_token': csrf_token,
929 'csrf_token': csrf_token,
927 },
930 },
928 status=302)
931 status=302)
929 assert_session_flash(
932 assert_session_flash(
930 response, 'Successfully committed changes to file `vcs/nodes.py`')
933 response, 'Successfully committed changes to file `vcs/nodes.py`')
931 tip = repo.get_commit(commit_idx=-1)
934 tip = repo.get_commit(commit_idx=-1)
932 assert tip.message == 'I committed'
935 assert tip.message == 'I committed'
933
936
934 def test_edit_file_view_commit_changes_default_message(self, backend,
937 def test_edit_file_view_commit_changes_default_message(self, backend,
935 csrf_token):
938 csrf_token):
936 repo = backend.create_repo()
939 repo = backend.create_repo()
937 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
940 backend.ensure_file(b"vcs/nodes.py", content=b"print 'hello'")
938
941
939 commit_id = (
942 commit_id = (
940 backend.default_branch_name or
943 backend.default_branch_name or
941 backend.repo.scm_instance().commit_ids[-1])
944 backend.repo.scm_instance().commit_ids[-1])
942
945
943 response = self.app.post(
946 response = self.app.post(
944 route_path('repo_files_update_file',
947 route_path('repo_files_update_file',
945 repo_name=repo.repo_name,
948 repo_name=repo.repo_name,
946 commit_id=commit_id,
949 commit_id=commit_id,
947 f_path='vcs/nodes.py'),
950 f_path='vcs/nodes.py'),
948 params={
951 params={
949 'content': "print 'hello world'",
952 'content': "print 'hello world'",
950 'message': '',
953 'message': '',
951 'filename': "vcs/nodes.py",
954 'filename': "vcs/nodes.py",
952 'csrf_token': csrf_token,
955 'csrf_token': csrf_token,
953 },
956 },
954 status=302)
957 status=302)
955 assert_session_flash(
958 assert_session_flash(
956 response, 'Successfully committed changes to file `vcs/nodes.py`')
959 response, 'Successfully committed changes to file `vcs/nodes.py`')
957 tip = repo.get_commit(commit_idx=-1)
960 tip = repo.get_commit(commit_idx=-1)
958 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
961 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
959
962
960 def test_delete_file_view(self, backend):
963 def test_delete_file_view(self, backend):
961 self.app.get(
964 self.app.get(
962 route_path('repo_files_remove_file',
965 route_path('repo_files_remove_file',
963 repo_name=backend.repo_name,
966 repo_name=backend.repo_name,
964 commit_id=backend.default_head_id,
967 commit_id=backend.default_head_id,
965 f_path='vcs/nodes.py'),
968 f_path='vcs/nodes.py'),
966 status=200)
969 status=200)
967
970
968 def test_delete_file_view_not_on_branch(self, backend):
971 def test_delete_file_view_not_on_branch(self, backend):
969 repo = backend.create_repo()
972 repo = backend.create_repo()
970 backend.ensure_file('vcs/nodes.py')
973 backend.ensure_file(b'vcs/nodes.py')
971
974
972 response = self.app.get(
975 response = self.app.get(
973 route_path('repo_files_remove_file',
976 route_path('repo_files_remove_file',
974 repo_name=repo.repo_name,
977 repo_name=repo.repo_name,
975 commit_id='tip',
978 commit_id='tip',
976 f_path='vcs/nodes.py'),
979 f_path='vcs/nodes.py'),
977 status=302)
980 status=302)
978 assert_session_flash(
981 assert_session_flash(
979 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
982 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
980
983
981 def test_delete_file_view_commit_changes(self, backend, csrf_token):
984 def test_delete_file_view_commit_changes(self, backend, csrf_token):
982 repo = backend.create_repo()
985 repo = backend.create_repo()
983 backend.ensure_file("vcs/nodes.py")
986 backend.ensure_file(b"vcs/nodes.py")
984
987
985 response = self.app.post(
988 response = self.app.post(
986 route_path('repo_files_delete_file',
989 route_path('repo_files_delete_file',
987 repo_name=repo.repo_name,
990 repo_name=repo.repo_name,
988 commit_id=backend.default_head_id,
991 commit_id=backend.default_head_id,
989 f_path='vcs/nodes.py'),
992 f_path='vcs/nodes.py'),
990 params={
993 params={
991 'message': 'i committed',
994 'message': 'i committed',
992 'csrf_token': csrf_token,
995 'csrf_token': csrf_token,
993 },
996 },
994 status=302)
997 status=302)
995 assert_session_flash(
998 assert_session_flash(
996 response, 'Successfully deleted file `vcs/nodes.py`')
999 response, 'Successfully deleted file `vcs/nodes.py`')
997
1000
998
1001
999 @pytest.mark.usefixtures("app")
1002 @pytest.mark.usefixtures("app")
1000 class TestFilesViewOtherCases(object):
1003 class TestFilesViewOtherCases(object):
1001
1004
1002 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
1005 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
1003 self, backend_stub, autologin_regular_user, user_regular,
1006 self, backend_stub, autologin_regular_user, user_regular,
1004 user_util):
1007 user_util):
1005
1008
1006 repo = backend_stub.create_repo()
1009 repo = backend_stub.create_repo()
1007 user_util.grant_user_permission_to_repo(
1010 user_util.grant_user_permission_to_repo(
1008 repo, user_regular, 'repository.write')
1011 repo, user_regular, 'repository.write')
1009 response = self.app.get(
1012 response = self.app.get(
1010 route_path('repo_files',
1013 route_path('repo_files',
1011 repo_name=repo.repo_name,
1014 repo_name=repo.repo_name,
1012 commit_id='tip', f_path='/'))
1015 commit_id='tip', f_path='/'))
1013
1016
1014 repo_file_add_url = route_path(
1017 repo_file_add_url = route_path(
1015 'repo_files_add_file',
1018 'repo_files_add_file',
1016 repo_name=repo.repo_name,
1019 repo_name=repo.repo_name,
1017 commit_id=0, f_path='')
1020 commit_id=0, f_path='')
1021 add_new = f'<a class="alert-link" href="{repo_file_add_url}">add a new file</a>'
1022
1023 repo_file_upload_url = route_path(
1024 'repo_files_upload_file',
1025 repo_name=repo.repo_name,
1026 commit_id=0, f_path='')
1027 upload_new = f'<a class="alert-link" href="{repo_file_upload_url}">upload a new file</a>'
1018
1028
1019 assert_session_flash(
1029 assert_session_flash(
1020 response,
1030 response,
1021 'There are no files yet. <a class="alert-link" '
1031 'There are no files yet. Click here to %s or %s.' % (add_new, upload_new)
1022 'href="{}">Click here to add a new file.</a>'
1032 )
1023 .format(repo_file_add_url))
1024
1033
1025 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1034 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1026 self, backend_stub, autologin_regular_user):
1035 self, backend_stub, autologin_regular_user):
1027 repo = backend_stub.create_repo()
1036 repo = backend_stub.create_repo()
1028 # init session for anon user
1037 # init session for anon user
1029 route_path('repo_summary', repo_name=repo.repo_name)
1038 route_path('repo_summary', repo_name=repo.repo_name)
1030
1039
1031 repo_file_add_url = route_path(
1040 repo_file_add_url = route_path(
1032 'repo_files_add_file',
1041 'repo_files_add_file',
1033 repo_name=repo.repo_name,
1042 repo_name=repo.repo_name,
1034 commit_id=0, f_path='')
1043 commit_id=0, f_path='')
1035
1044
1036 response = self.app.get(
1045 response = self.app.get(
1037 route_path('repo_files',
1046 route_path('repo_files',
1038 repo_name=repo.repo_name,
1047 repo_name=repo.repo_name,
1039 commit_id='tip', f_path='/'))
1048 commit_id='tip', f_path='/'))
1040
1049
1041 assert_session_flash(response, no_=repo_file_add_url)
1050 assert_session_flash(response, no_=repo_file_add_url)
1042
1051
1043 @pytest.mark.parametrize('file_node', [
1052 @pytest.mark.parametrize('file_node', [
1044 'archive/file.zip',
1053 b'archive/file.zip',
1045 'diff/my-file.txt',
1054 b'diff/my-file.txt',
1046 'render.py',
1055 b'render.py',
1047 'render',
1056 b'render',
1048 'remove_file',
1057 b'remove_file',
1049 'remove_file/to-delete.txt',
1058 b'remove_file/to-delete.txt',
1050 ])
1059 ])
1051 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1060 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1052 backend.create_repo()
1061 backend.create_repo()
1053 backend.ensure_file(file_node)
1062 backend.ensure_file(file_node)
1054
1063
1055 self.app.get(
1064 self.app.get(
1056 route_path('repo_files',
1065 route_path('repo_files',
1057 repo_name=backend.repo_name,
1066 repo_name=backend.repo_name,
1058 commit_id='tip', f_path=file_node),
1067 commit_id='tip', f_path=safe_str(file_node)),
1059 status=200)
1068 status=200)
1060
1069
1061
1070
1062 class TestAdjustFilePathForSvn(object):
1071 class TestAdjustFilePathForSvn(object):
1063 """
1072 """
1064 SVN specific adjustments of node history in RepoFilesView.
1073 SVN specific adjustments of node history in RepoFilesView.
1065 """
1074 """
1066
1075
1067 def test_returns_path_relative_to_matched_reference(self):
1076 def test_returns_path_relative_to_matched_reference(self):
1068 repo = self._repo(branches=['trunk'])
1077 repo = self._repo(branches=['trunk'])
1069 self.assert_file_adjustment('trunk/file', 'file', repo)
1078 self.assert_file_adjustment('trunk/file', 'file', repo)
1070
1079
1071 def test_does_not_modify_file_if_no_reference_matches(self):
1080 def test_does_not_modify_file_if_no_reference_matches(self):
1072 repo = self._repo(branches=['trunk'])
1081 repo = self._repo(branches=['trunk'])
1073 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1082 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1074
1083
1075 def test_does_not_adjust_partial_directory_names(self):
1084 def test_does_not_adjust_partial_directory_names(self):
1076 repo = self._repo(branches=['trun'])
1085 repo = self._repo(branches=['trun'])
1077 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1086 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1078
1087
1079 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1088 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1080 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1089 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1081 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1090 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1082
1091
1083 def assert_file_adjustment(self, f_path, expected, repo):
1092 def assert_file_adjustment(self, f_path, expected, repo):
1084 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1093 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1085 assert result == expected
1094 assert result == expected
1086
1095
1087 def _repo(self, branches=None):
1096 def _repo(self, branches=None):
1088 repo = mock.Mock()
1097 repo = mock.Mock()
1089 repo.branches = OrderedDict((name, '0') for name in branches or [])
1098 repo.branches = OrderedDict((name, '0') for name in branches or [])
1090 repo.tags = {}
1099 repo.tags = {}
1091 return repo
1100 return repo
@@ -1,332 +1,334 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.tests import TestController, assert_session_flash, HG_FORK, GIT_FORK
22 from rhodecode.tests import TestController, assert_session_flash, HG_FORK, GIT_FORK
23
23
24 from rhodecode.tests.fixture import Fixture
24 from rhodecode.tests.fixture import Fixture
25 from rhodecode.lib import helpers as h
25 from rhodecode.lib import helpers as h
26
26
27 from rhodecode.model.db import Repository
27 from rhodecode.model.db import Repository
28 from rhodecode.model.repo import RepoModel
28 from rhodecode.model.repo import RepoModel
29 from rhodecode.model.user import UserModel
29 from rhodecode.model.user import UserModel
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
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.request, urllib.parse, urllib.error
36 import urllib.request
37 import urllib.parse
38 import urllib.error
37
39
38 base_url = {
40 base_url = {
39 'repo_summary': '/{repo_name}',
41 'repo_summary': '/{repo_name}',
40 'repo_creating_check': '/{repo_name}/repo_creating_check',
42 'repo_creating_check': '/{repo_name}/repo_creating_check',
41 'repo_fork_new': '/{repo_name}/fork',
43 'repo_fork_new': '/{repo_name}/fork',
42 'repo_fork_create': '/{repo_name}/fork/create',
44 'repo_fork_create': '/{repo_name}/fork/create',
43 'repo_forks_show_all': '/{repo_name}/forks',
45 'repo_forks_show_all': '/{repo_name}/forks',
44 'repo_forks_data': '/{repo_name}/forks/data',
46 'repo_forks_data': '/{repo_name}/forks/data',
45 }[name].format(**kwargs)
47 }[name].format(**kwargs)
46
48
47 if params:
49 if params:
48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
50 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 return base_url
51 return base_url
50
52
51
53
52 FORK_NAME = {
54 FORK_NAME = {
53 'hg': HG_FORK,
55 'hg': HG_FORK,
54 'git': GIT_FORK
56 'git': GIT_FORK
55 }
57 }
56
58
57
59
58 @pytest.mark.skip_backends('svn')
60 @pytest.mark.skip_backends('svn')
59 class TestRepoForkViewTests(TestController):
61 class TestRepoForkViewTests(TestController):
60
62
61 def test_show_forks(self, backend, xhr_header):
63 def test_show_forks(self, backend, xhr_header):
62 self.log_user()
64 self.log_user()
63 response = self.app.get(
65 response = self.app.get(
64 route_path('repo_forks_data', repo_name=backend.repo_name),
66 route_path('repo_forks_data', repo_name=backend.repo_name),
65 extra_environ=xhr_header)
67 extra_environ=xhr_header)
66
68
67 assert response.json == {u'data': [], u'draw': None,
69 assert response.json == {u'data': [], u'draw': None,
68 u'recordsFiltered': 0, u'recordsTotal': 0}
70 u'recordsFiltered': 0, u'recordsTotal': 0}
69
71
70 def test_no_permissions_to_fork_page(self, backend, user_util):
72 def test_no_permissions_to_fork_page(self, backend, user_util):
71 user = user_util.create_user(password='qweqwe')
73 user = user_util.create_user(password='qweqwe')
72 user_id = user.user_id
74 user_id = user.user_id
73 self.log_user(user.username, 'qweqwe')
75 self.log_user(user.username, 'qweqwe')
74
76
75 user_model = UserModel()
77 user_model = UserModel()
76 user_model.revoke_perm(user_id, 'hg.fork.repository')
78 user_model.revoke_perm(user_id, 'hg.fork.repository')
77 user_model.grant_perm(user_id, 'hg.fork.none')
79 user_model.grant_perm(user_id, 'hg.fork.none')
78 u = UserModel().get(user_id)
80 u = UserModel().get(user_id)
79 u.inherit_default_permissions = False
81 u.inherit_default_permissions = False
80 Session().commit()
82 Session().commit()
81 # try create a fork
83 # try create a fork
82 self.app.get(
84 self.app.get(
83 route_path('repo_fork_new', repo_name=backend.repo_name),
85 route_path('repo_fork_new', repo_name=backend.repo_name),
84 status=404)
86 status=404)
85
87
86 def test_no_permissions_to_fork_submit(self, backend, csrf_token, user_util):
88 def test_no_permissions_to_fork_submit(self, backend, csrf_token, user_util):
87 user = user_util.create_user(password='qweqwe')
89 user = user_util.create_user(password='qweqwe')
88 user_id = user.user_id
90 user_id = user.user_id
89 self.log_user(user.username, 'qweqwe')
91 self.log_user(user.username, 'qweqwe')
90
92
91 user_model = UserModel()
93 user_model = UserModel()
92 user_model.revoke_perm(user_id, 'hg.fork.repository')
94 user_model.revoke_perm(user_id, 'hg.fork.repository')
93 user_model.grant_perm(user_id, 'hg.fork.none')
95 user_model.grant_perm(user_id, 'hg.fork.none')
94 u = UserModel().get(user_id)
96 u = UserModel().get(user_id)
95 u.inherit_default_permissions = False
97 u.inherit_default_permissions = False
96 Session().commit()
98 Session().commit()
97 # try create a fork
99 # try create a fork
98 self.app.post(
100 self.app.post(
99 route_path('repo_fork_create', repo_name=backend.repo_name),
101 route_path('repo_fork_create', repo_name=backend.repo_name),
100 {'csrf_token': csrf_token},
102 {'csrf_token': csrf_token},
101 status=404)
103 status=404)
102
104
103 def test_fork_missing_data(self, autologin_user, backend, csrf_token):
105 def test_fork_missing_data(self, autologin_user, backend, csrf_token):
104 # try create a fork
106 # try create a fork
105 response = self.app.post(
107 response = self.app.post(
106 route_path('repo_fork_create', repo_name=backend.repo_name),
108 route_path('repo_fork_create', repo_name=backend.repo_name),
107 {'csrf_token': csrf_token},
109 {'csrf_token': csrf_token},
108 status=200)
110 status=200)
109 # test if html fill works fine
111 # test if html fill works fine
110 response.mustcontain('Missing value')
112 response.mustcontain('Missing value')
111
113
112 def test_create_fork_page(self, autologin_user, backend):
114 def test_create_fork_page(self, autologin_user, backend):
113 self.app.get(
115 self.app.get(
114 route_path('repo_fork_new', repo_name=backend.repo_name),
116 route_path('repo_fork_new', repo_name=backend.repo_name),
115 status=200)
117 status=200)
116
118
117 def test_create_and_show_fork(
119 def test_create_and_show_fork(
118 self, autologin_user, backend, csrf_token, xhr_header):
120 self, autologin_user, backend, csrf_token, xhr_header):
119
121
120 # create a fork
122 # create a fork
121 fork_name = FORK_NAME[backend.alias]
123 fork_name = FORK_NAME[backend.alias]
122 description = 'fork of vcs test'
124 description = 'fork of vcs test'
123 repo_name = backend.repo_name
125 repo_name = backend.repo_name
124 source_repo = Repository.get_by_repo_name(repo_name)
126 source_repo = Repository.get_by_repo_name(repo_name)
125 creation_args = {
127 creation_args = {
126 'repo_name': fork_name,
128 'repo_name': fork_name,
127 'repo_group': '',
129 'repo_group': '',
128 'fork_parent_id': source_repo.repo_id,
130 'fork_parent_id': source_repo.repo_id,
129 'repo_type': backend.alias,
131 'repo_type': backend.alias,
130 'description': description,
132 'description': description,
131 'private': 'False',
133 'private': 'False',
132 'csrf_token': csrf_token,
134 'csrf_token': csrf_token,
133 }
135 }
134
136
135 self.app.post(
137 self.app.post(
136 route_path('repo_fork_create', repo_name=repo_name), creation_args)
138 route_path('repo_fork_create', repo_name=repo_name), creation_args)
137
139
138 response = self.app.get(
140 response = self.app.get(
139 route_path('repo_forks_data', repo_name=repo_name),
141 route_path('repo_forks_data', repo_name=repo_name),
140 extra_environ=xhr_header)
142 extra_environ=xhr_header)
141
143
142 assert response.json['data'][0]['fork_name'] == \
144 assert response.json['data'][0]['fork_name'] == \
143 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
145 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
144
146
145 # remove this fork
147 # remove this fork
146 fixture.destroy_repo(fork_name)
148 fixture.destroy_repo(fork_name)
147
149
148 def test_fork_create(self, autologin_user, backend, csrf_token):
150 def test_fork_create(self, autologin_user, backend, csrf_token):
149 fork_name = FORK_NAME[backend.alias]
151 fork_name = FORK_NAME[backend.alias]
150 description = 'fork of vcs test'
152 description = 'fork of vcs test'
151 repo_name = backend.repo_name
153 repo_name = backend.repo_name
152 source_repo = Repository.get_by_repo_name(repo_name)
154 source_repo = Repository.get_by_repo_name(repo_name)
153 creation_args = {
155 creation_args = {
154 'repo_name': fork_name,
156 'repo_name': fork_name,
155 'repo_group': '',
157 'repo_group': '',
156 'fork_parent_id': source_repo.repo_id,
158 'fork_parent_id': source_repo.repo_id,
157 'repo_type': backend.alias,
159 'repo_type': backend.alias,
158 'description': description,
160 'description': description,
159 'private': 'False',
161 'private': 'False',
160 'csrf_token': csrf_token,
162 'csrf_token': csrf_token,
161 }
163 }
162 self.app.post(
164 self.app.post(
163 route_path('repo_fork_create', repo_name=repo_name), creation_args)
165 route_path('repo_fork_create', repo_name=repo_name), creation_args)
164 repo = Repository.get_by_repo_name(FORK_NAME[backend.alias])
166 repo = Repository.get_by_repo_name(FORK_NAME[backend.alias])
165 assert repo.fork.repo_name == backend.repo_name
167 assert repo.fork.repo_name == backend.repo_name
166
168
167 # run the check page that triggers the flash message
169 # run the check page that triggers the flash message
168 response = self.app.get(
170 response = self.app.get(
169 route_path('repo_creating_check', repo_name=fork_name))
171 route_path('repo_creating_check', repo_name=fork_name))
170 # test if we have a message that fork is ok
172 # test if we have a message that fork is ok
171 assert_session_flash(response,
173 assert_session_flash(response,
172 'Forked repository %s as <a href="/%s">%s</a>' % (
174 'Forked repository %s as <a href="/%s">%s</a>' % (
173 repo_name, fork_name, fork_name))
175 repo_name, fork_name, fork_name))
174
176
175 # test if the fork was created in the database
177 # test if the fork was created in the database
176 fork_repo = Session().query(Repository)\
178 fork_repo = Session().query(Repository)\
177 .filter(Repository.repo_name == fork_name).one()
179 .filter(Repository.repo_name == fork_name).one()
178
180
179 assert fork_repo.repo_name == fork_name
181 assert fork_repo.repo_name == fork_name
180 assert fork_repo.fork.repo_name == repo_name
182 assert fork_repo.fork.repo_name == repo_name
181
183
182 # test if the repository is visible in the list ?
184 # test if the repository is visible in the list ?
183 response = self.app.get(
185 response = self.app.get(
184 h.route_path('repo_summary', repo_name=fork_name))
186 h.route_path('repo_summary', repo_name=fork_name))
185 response.mustcontain(fork_name)
187 response.mustcontain(fork_name)
186 response.mustcontain(backend.alias)
188 response.mustcontain(backend.alias)
187 response.mustcontain('Fork of')
189 response.mustcontain('Fork of')
188 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
190 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
189
191
190 def test_fork_create_into_group(self, autologin_user, backend, csrf_token):
192 def test_fork_create_into_group(self, autologin_user, backend, csrf_token):
191 group = fixture.create_repo_group('vc')
193 group = fixture.create_repo_group('vc')
192 group_id = group.group_id
194 group_id = group.group_id
193 fork_name = FORK_NAME[backend.alias]
195 fork_name = FORK_NAME[backend.alias]
194 fork_name_full = 'vc/%s' % fork_name
196 fork_name_full = 'vc/%s' % fork_name
195 description = 'fork of vcs test'
197 description = 'fork of vcs test'
196 repo_name = backend.repo_name
198 repo_name = backend.repo_name
197 source_repo = Repository.get_by_repo_name(repo_name)
199 source_repo = Repository.get_by_repo_name(repo_name)
198 creation_args = {
200 creation_args = {
199 'repo_name': fork_name,
201 'repo_name': fork_name,
200 'repo_group': group_id,
202 'repo_group': group_id,
201 'fork_parent_id': source_repo.repo_id,
203 'fork_parent_id': source_repo.repo_id,
202 'repo_type': backend.alias,
204 'repo_type': backend.alias,
203 'description': description,
205 'description': description,
204 'private': 'False',
206 'private': 'False',
205 'csrf_token': csrf_token,
207 'csrf_token': csrf_token,
206 }
208 }
207 self.app.post(
209 self.app.post(
208 route_path('repo_fork_create', repo_name=repo_name), creation_args)
210 route_path('repo_fork_create', repo_name=repo_name), creation_args)
209 repo = Repository.get_by_repo_name(fork_name_full)
211 repo = Repository.get_by_repo_name(fork_name_full)
210 assert repo.fork.repo_name == backend.repo_name
212 assert repo.fork.repo_name == backend.repo_name
211
213
212 # run the check page that triggers the flash message
214 # run the check page that triggers the flash message
213 response = self.app.get(
215 response = self.app.get(
214 route_path('repo_creating_check', repo_name=fork_name_full))
216 route_path('repo_creating_check', repo_name=fork_name_full))
215 # test if we have a message that fork is ok
217 # test if we have a message that fork is ok
216 assert_session_flash(response,
218 assert_session_flash(response,
217 'Forked repository %s as <a href="/%s">%s</a>' % (
219 'Forked repository %s as <a href="/%s">%s</a>' % (
218 repo_name, fork_name_full, fork_name_full))
220 repo_name, fork_name_full, fork_name_full))
219
221
220 # test if the fork was created in the database
222 # test if the fork was created in the database
221 fork_repo = Session().query(Repository)\
223 fork_repo = Session().query(Repository)\
222 .filter(Repository.repo_name == fork_name_full).one()
224 .filter(Repository.repo_name == fork_name_full).one()
223
225
224 assert fork_repo.repo_name == fork_name_full
226 assert fork_repo.repo_name == fork_name_full
225 assert fork_repo.fork.repo_name == repo_name
227 assert fork_repo.fork.repo_name == repo_name
226
228
227 # test if the repository is visible in the list ?
229 # test if the repository is visible in the list ?
228 response = self.app.get(
230 response = self.app.get(
229 h.route_path('repo_summary', repo_name=fork_name_full))
231 h.route_path('repo_summary', repo_name=fork_name_full))
230 response.mustcontain(fork_name_full)
232 response.mustcontain(fork_name_full)
231 response.mustcontain(backend.alias)
233 response.mustcontain(backend.alias)
232
234
233 response.mustcontain('Fork of')
235 response.mustcontain('Fork of')
234 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
236 response.mustcontain('<a href="/%s">%s</a>' % (repo_name, repo_name))
235
237
236 fixture.destroy_repo(fork_name_full)
238 fixture.destroy_repo(fork_name_full)
237 fixture.destroy_repo_group(group_id)
239 fixture.destroy_repo_group(group_id)
238
240
239 def test_fork_read_permission(self, backend, xhr_header, user_util):
241 def test_fork_read_permission(self, backend, xhr_header, user_util):
240 user = user_util.create_user(password='qweqwe')
242 user = user_util.create_user(password='qweqwe')
241 user_id = user.user_id
243 user_id = user.user_id
242 self.log_user(user.username, 'qweqwe')
244 self.log_user(user.username, 'qweqwe')
243
245
244 # create a fake fork
246 # create a fake fork
245 fork = user_util.create_repo(repo_type=backend.alias)
247 fork = user_util.create_repo(repo_type=backend.alias)
246 source = user_util.create_repo(repo_type=backend.alias)
248 source = user_util.create_repo(repo_type=backend.alias)
247 repo_name = source.repo_name
249 repo_name = source.repo_name
248
250
249 fork.fork_id = source.repo_id
251 fork.fork_id = source.repo_id
250 fork_name = fork.repo_name
252 fork_name = fork.repo_name
251 Session().commit()
253 Session().commit()
252
254
253 forks = Repository.query()\
255 forks = Repository.query()\
254 .filter(Repository.repo_type == backend.alias)\
256 .filter(Repository.repo_type == backend.alias)\
255 .filter(Repository.fork_id == source.repo_id).all()
257 .filter(Repository.fork_id == source.repo_id).all()
256 assert 1 == len(forks)
258 assert 1 == len(forks)
257
259
258 # set read permissions for this
260 # set read permissions for this
259 RepoModel().grant_user_permission(
261 RepoModel().grant_user_permission(
260 repo=forks[0], user=user_id, perm='repository.read')
262 repo=forks[0], user=user_id, perm='repository.read')
261 Session().commit()
263 Session().commit()
262
264
263 response = self.app.get(
265 response = self.app.get(
264 route_path('repo_forks_data', repo_name=repo_name),
266 route_path('repo_forks_data', repo_name=repo_name),
265 extra_environ=xhr_header)
267 extra_environ=xhr_header)
266
268
267 assert response.json['data'][0]['fork_name'] == \
269 assert response.json['data'][0]['fork_name'] == \
268 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
270 """<a href="/%s">%s</a>""" % (fork_name, fork_name)
269
271
270 def test_fork_none_permission(self, backend, xhr_header, user_util):
272 def test_fork_none_permission(self, backend, xhr_header, user_util):
271 user = user_util.create_user(password='qweqwe')
273 user = user_util.create_user(password='qweqwe')
272 user_id = user.user_id
274 user_id = user.user_id
273 self.log_user(user.username, 'qweqwe')
275 self.log_user(user.username, 'qweqwe')
274
276
275 # create a fake fork
277 # create a fake fork
276 fork = user_util.create_repo(repo_type=backend.alias)
278 fork = user_util.create_repo(repo_type=backend.alias)
277 source = user_util.create_repo(repo_type=backend.alias)
279 source = user_util.create_repo(repo_type=backend.alias)
278 repo_name = source.repo_name
280 repo_name = source.repo_name
279
281
280 fork.fork_id = source.repo_id
282 fork.fork_id = source.repo_id
281
283
282 Session().commit()
284 Session().commit()
283
285
284 forks = Repository.query()\
286 forks = Repository.query()\
285 .filter(Repository.repo_type == backend.alias)\
287 .filter(Repository.repo_type == backend.alias)\
286 .filter(Repository.fork_id == source.repo_id).all()
288 .filter(Repository.fork_id == source.repo_id).all()
287 assert 1 == len(forks)
289 assert 1 == len(forks)
288
290
289 # set none
291 # set none
290 RepoModel().grant_user_permission(
292 RepoModel().grant_user_permission(
291 repo=forks[0], user=user_id, perm='repository.none')
293 repo=forks[0], user=user_id, perm='repository.none')
292 Session().commit()
294 Session().commit()
293
295
294 # fork shouldn't be there
296 # fork shouldn't be there
295 response = self.app.get(
297 response = self.app.get(
296 route_path('repo_forks_data', repo_name=repo_name),
298 route_path('repo_forks_data', repo_name=repo_name),
297 extra_environ=xhr_header)
299 extra_environ=xhr_header)
298
300
299 assert response.json == {u'data': [], u'draw': None,
301 assert response.json == {u'data': [], u'draw': None,
300 u'recordsFiltered': 0, u'recordsTotal': 0}
302 u'recordsFiltered': 0, u'recordsTotal': 0}
301
303
302 @pytest.mark.parametrize('url_type', [
304 @pytest.mark.parametrize('url_type', [
303 'repo_fork_new',
305 'repo_fork_new',
304 'repo_fork_create'
306 'repo_fork_create'
305 ])
307 ])
306 def test_fork_is_forbidden_on_archived_repo(self, backend, xhr_header, user_util, url_type):
308 def test_fork_is_forbidden_on_archived_repo(self, backend, xhr_header, user_util, url_type):
307 user = user_util.create_user(password='qweqwe')
309 user = user_util.create_user(password='qweqwe')
308 self.log_user(user.username, 'qweqwe')
310 self.log_user(user.username, 'qweqwe')
309
311
310 # create a temporary repo
312 # create a temporary repo
311 source = user_util.create_repo(repo_type=backend.alias)
313 source = user_util.create_repo(repo_type=backend.alias)
312 repo_name = source.repo_name
314 repo_name = source.repo_name
313 repo = Repository.get_by_repo_name(repo_name)
315 repo = Repository.get_by_repo_name(repo_name)
314 repo.archived = True
316 repo.archived = True
315 Session().commit()
317 Session().commit()
316
318
317 response = self.app.get(
319 response = self.app.get(
318 route_path(url_type, repo_name=repo_name), status=302)
320 route_path(url_type, repo_name=repo_name), status=302)
319
321
320 msg = 'Action not supported for archived repository.'
322 msg = 'Action not supported for archived repository.'
321 assert_session_flash(response, msg)
323 assert_session_flash(response, msg)
322
324
323
325
324 class TestSVNFork(TestController):
326 class TestSVNFork(TestController):
325 @pytest.mark.parametrize('route_name', [
327 @pytest.mark.parametrize('route_name', [
326 'repo_fork_create', 'repo_fork_new'
328 'repo_fork_create', 'repo_fork_new'
327 ])
329 ])
328 def test_fork_redirects(self, autologin_user, backend_svn, route_name):
330 def test_fork_redirects(self, autologin_user, backend_svn, route_name):
329
331
330 self.app.get(route_path(
332 self.app.get(route_path(
331 route_name, repo_name=backend_svn.repo_name),
333 route_name, repo_name=backend_svn.repo_name),
332 status=404)
334 status=404)
@@ -1,148 +1,150 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.lib.utils2 import md5
22 from rhodecode.lib.hash_utils import md5_safe
23 from rhodecode.model.db import Repository
23 from rhodecode.model.db import Repository
24 from rhodecode.model.meta import Session
24 from rhodecode.model.meta import Session
25 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
25 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
26
26
27
27
28 def route_path(name, params=None, **kwargs):
28 def route_path(name, params=None, **kwargs):
29 import urllib.request, urllib.parse, urllib.error
29 import urllib.request
30 import urllib.parse
31 import urllib.error
30
32
31 base_url = {
33 base_url = {
32 'repo_summary': '/{repo_name}',
34 'repo_summary': '/{repo_name}',
33 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
35 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
34 'edit_repo_issuetracker_test': '/{repo_name}/settings/issue_trackers/test',
36 'edit_repo_issuetracker_test': '/{repo_name}/settings/issue_trackers/test',
35 'edit_repo_issuetracker_delete': '/{repo_name}/settings/issue_trackers/delete',
37 'edit_repo_issuetracker_delete': '/{repo_name}/settings/issue_trackers/delete',
36 'edit_repo_issuetracker_update': '/{repo_name}/settings/issue_trackers/update',
38 'edit_repo_issuetracker_update': '/{repo_name}/settings/issue_trackers/update',
37 }[name].format(**kwargs)
39 }[name].format(**kwargs)
38
40
39 if params:
41 if params:
40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
41 return base_url
43 return base_url
42
44
43
45
44 @pytest.mark.usefixtures("app")
46 @pytest.mark.usefixtures("app")
45 class TestRepoIssueTracker(object):
47 class TestRepoIssueTracker(object):
46 def test_issuetracker_index(self, autologin_user, backend):
48 def test_issuetracker_index(self, autologin_user, backend):
47 repo = backend.create_repo()
49 repo = backend.create_repo()
48 response = self.app.get(route_path('edit_repo_issuetracker',
50 response = self.app.get(route_path('edit_repo_issuetracker',
49 repo_name=repo.repo_name))
51 repo_name=repo.repo_name))
50 assert response.status_code == 200
52 assert response.status_code == 200
51
53
52 def test_add_and_test_issuetracker_patterns(
54 def test_add_and_test_issuetracker_patterns(
53 self, autologin_user, backend, csrf_token, request, xhr_header):
55 self, autologin_user, backend, csrf_token, request, xhr_header):
54 pattern = 'issuetracker_pat'
56 pattern = 'issuetracker_pat'
55 another_pattern = pattern+'1'
57 another_pattern = pattern+'1'
56 post_url = route_path(
58 post_url = route_path(
57 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
59 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
58 post_data = {
60 post_data = {
59 'new_pattern_pattern_0': pattern,
61 'new_pattern_pattern_0': pattern,
60 'new_pattern_url_0': 'http://url',
62 'new_pattern_url_0': 'http://url',
61 'new_pattern_prefix_0': 'prefix',
63 'new_pattern_prefix_0': 'prefix',
62 'new_pattern_description_0': 'description',
64 'new_pattern_description_0': 'description',
63 'new_pattern_pattern_1': another_pattern,
65 'new_pattern_pattern_1': another_pattern,
64 'new_pattern_url_1': '/url1',
66 'new_pattern_url_1': '/url1',
65 'new_pattern_prefix_1': 'prefix1',
67 'new_pattern_prefix_1': 'prefix1',
66 'new_pattern_description_1': 'description1',
68 'new_pattern_description_1': 'description1',
67 'csrf_token': csrf_token
69 'csrf_token': csrf_token
68 }
70 }
69 self.app.post(post_url, post_data, status=302)
71 self.app.post(post_url, post_data, status=302)
70 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
72 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
71 settings = self.settings_model.get_repo_settings()
73 settings = self.settings_model.get_repo_settings()
72 self.uid = md5(pattern)
74 self.uid = md5_safe(pattern)
73 assert settings[self.uid]['pat'] == pattern
75 assert settings[self.uid]['pat'] == pattern
74 self.another_uid = md5(another_pattern)
76 self.another_uid = md5_safe(another_pattern)
75 assert settings[self.another_uid]['pat'] == another_pattern
77 assert settings[self.another_uid]['pat'] == another_pattern
76
78
77 # test pattern
79 # test pattern
78 data = {'test_text': 'example of issuetracker_pat replacement',
80 data = {'test_text': 'example of issuetracker_pat replacement',
79 'csrf_token': csrf_token}
81 'csrf_token': csrf_token}
80 response = self.app.post(
82 response = self.app.post(
81 route_path('edit_repo_issuetracker_test',
83 route_path('edit_repo_issuetracker_test',
82 repo_name=backend.repo.repo_name),
84 repo_name=backend.repo.repo_name),
83 extra_environ=xhr_header, params=data)
85 extra_environ=xhr_header, params=data)
84
86
85 assert response.text == \
87 assert response.text == \
86 'example of <a class="tooltip issue-tracker-link" href="http://url" title="description">prefix</a> replacement'
88 'example of <a class="tooltip issue-tracker-link" href="http://url" title="description">prefix</a> replacement'
87
89
88 @request.addfinalizer
90 @request.addfinalizer
89 def cleanup():
91 def cleanup():
90 self.settings_model.delete_entries(self.uid)
92 self.settings_model.delete_entries(self.uid)
91 self.settings_model.delete_entries(self.another_uid)
93 self.settings_model.delete_entries(self.another_uid)
92
94
93 def test_edit_issuetracker_pattern(
95 def test_edit_issuetracker_pattern(
94 self, autologin_user, backend, csrf_token, request):
96 self, autologin_user, backend, csrf_token, request):
95 entry_key = 'issuetracker_pat_'
97 entry_key = 'issuetracker_pat_'
96 pattern = 'issuetracker_pat2'
98 pattern = 'issuetracker_pat2'
97 old_pattern = 'issuetracker_pat'
99 old_pattern = 'issuetracker_pat'
98 old_uid = md5(old_pattern)
100 old_uid = md5_safe(old_pattern)
99
101
100 sett = SettingsModel(repo=backend.repo).create_or_update_setting(
102 sett = SettingsModel(repo=backend.repo).create_or_update_setting(
101 entry_key+old_uid, old_pattern, 'unicode')
103 entry_key+old_uid, old_pattern, 'unicode')
102 Session().add(sett)
104 Session().add(sett)
103 Session().commit()
105 Session().commit()
104 post_url = route_path(
106 post_url = route_path(
105 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
107 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
106 post_data = {
108 post_data = {
107 'new_pattern_pattern_0': pattern,
109 'new_pattern_pattern_0': pattern,
108 'new_pattern_url_0': '/url',
110 'new_pattern_url_0': '/url',
109 'new_pattern_prefix_0': 'prefix',
111 'new_pattern_prefix_0': 'prefix',
110 'new_pattern_description_0': 'description',
112 'new_pattern_description_0': 'description',
111 'uid': old_uid,
113 'uid': old_uid,
112 'csrf_token': csrf_token
114 'csrf_token': csrf_token
113 }
115 }
114 self.app.post(post_url, post_data, status=302)
116 self.app.post(post_url, post_data, status=302)
115 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
117 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
116 settings = self.settings_model.get_repo_settings()
118 settings = self.settings_model.get_repo_settings()
117 self.uid = md5(pattern)
119 self.uid = md5_safe(pattern)
118 assert settings[self.uid]['pat'] == pattern
120 assert settings[self.uid]['pat'] == pattern
119 with pytest.raises(KeyError):
121 with pytest.raises(KeyError):
120 key = settings[old_uid]
122 key = settings[old_uid]
121
123
122 @request.addfinalizer
124 @request.addfinalizer
123 def cleanup():
125 def cleanup():
124 self.settings_model.delete_entries(self.uid)
126 self.settings_model.delete_entries(self.uid)
125
127
126 def test_delete_issuetracker_pattern(
128 def test_delete_issuetracker_pattern(
127 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
129 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
128 repo = backend.create_repo()
130 repo = backend.create_repo()
129 repo_name = repo.repo_name
131 repo_name = repo.repo_name
130 entry_key = 'issuetracker_pat_'
132 entry_key = 'issuetracker_pat_'
131 pattern = 'issuetracker_pat3'
133 pattern = 'issuetracker_pat3'
132 uid = md5(pattern)
134 uid = md5_safe(pattern)
133 settings_util.create_repo_rhodecode_setting(
135 settings_util.create_repo_rhodecode_setting(
134 repo=backend.repo, name=entry_key+uid,
136 repo=backend.repo, name=entry_key+uid,
135 value=entry_key, type_='unicode', cleanup=False)
137 value=entry_key, type_='unicode', cleanup=False)
136
138
137 self.app.post(
139 self.app.post(
138 route_path(
140 route_path(
139 'edit_repo_issuetracker_delete',
141 'edit_repo_issuetracker_delete',
140 repo_name=backend.repo.repo_name),
142 repo_name=backend.repo.repo_name),
141 {
143 {
142 'uid': uid,
144 'uid': uid,
143 'csrf_token': csrf_token,
145 'csrf_token': csrf_token,
144 '': ''
146 '': ''
145 }, extra_environ=xhr_header, status=200)
147 }, extra_environ=xhr_header, status=200)
146 settings = IssueTrackerSettingsModel(
148 settings = IssueTrackerSettingsModel(
147 repo=Repository.get_by_repo_name(repo_name)).get_repo_settings()
149 repo=Repository.get_by_repo_name(repo_name)).get_repo_settings()
148 assert 'rhodecode_%s%s' % (entry_key, uid) not in settings
150 assert 'rhodecode_%s%s' % (entry_key, uid) not in settings
@@ -1,73 +1,75 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 from rhodecode.lib.utils2 import str2bool
23 from rhodecode.lib.utils2 import str2bool
24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
24 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
25 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
25 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
26 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
27 from rhodecode.tests import (
27 from rhodecode.tests import (
28 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
28 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, assert_session_flash)
29 from rhodecode.tests.fixture import Fixture
29 from rhodecode.tests.fixture import Fixture
30
30
31 fixture = Fixture()
31 fixture = Fixture()
32
32
33
33
34 def route_path(name, params=None, **kwargs):
34 def route_path(name, params=None, **kwargs):
35 import urllib.request, urllib.parse, urllib.error
35 import urllib.request
36 import urllib.parse
37 import urllib.error
36
38
37 base_url = {
39 base_url = {
38 'edit_repo_maintenance': '/{repo_name}/settings/maintenance',
40 'edit_repo_maintenance': '/{repo_name}/settings/maintenance',
39 'edit_repo_maintenance_execute': '/{repo_name}/settings/maintenance/execute',
41 'edit_repo_maintenance_execute': '/{repo_name}/settings/maintenance/execute',
40
42
41 }[name].format(**kwargs)
43 }[name].format(**kwargs)
42
44
43 if params:
45 if params:
44 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
45 return base_url
47 return base_url
46
48
47
49
48 def _get_permission_for_user(user, repo):
50 def _get_permission_for_user(user, repo):
49 perm = UserRepoToPerm.query()\
51 perm = UserRepoToPerm.query()\
50 .filter(UserRepoToPerm.repository ==
52 .filter(UserRepoToPerm.repository ==
51 Repository.get_by_repo_name(repo))\
53 Repository.get_by_repo_name(repo))\
52 .filter(UserRepoToPerm.user == User.get_by_username(user))\
54 .filter(UserRepoToPerm.user == User.get_by_username(user))\
53 .all()
55 .all()
54 return perm
56 return perm
55
57
56
58
57 @pytest.mark.usefixtures('autologin_user', 'app')
59 @pytest.mark.usefixtures('autologin_user', 'app')
58 class TestAdminRepoMaintenance(object):
60 class TestAdminRepoMaintenance(object):
59 @pytest.mark.parametrize('urlname', [
61 @pytest.mark.parametrize('urlname', [
60 'edit_repo_maintenance',
62 'edit_repo_maintenance',
61 ])
63 ])
62 def test_show_page(self, urlname, app, backend):
64 def test_show_page(self, urlname, app, backend):
63 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
65 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
64
66
65 def test_execute_maintenance_for_repo_hg(self, app, backend_hg, autologin_user, xhr_header):
67 def test_execute_maintenance_for_repo_hg(self, app, backend_hg, autologin_user, xhr_header):
66 repo_name = backend_hg.repo_name
68 repo_name = backend_hg.repo_name
67
69
68 response = app.get(
70 response = app.get(
69 route_path('edit_repo_maintenance_execute',
71 route_path('edit_repo_maintenance_execute',
70 repo_name=repo_name,),
72 repo_name=repo_name,),
71 extra_environ=xhr_header)
73 extra_environ=xhr_header)
72
74
73 assert "HG Verify repo" in ''.join(response.json)
75 assert "HG Verify repo" in ''.join(response.json)
@@ -1,76 +1,78 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.tests.utils import permission_update_data_generator
22 from rhodecode.tests.utils import permission_update_data_generator
23
23
24
24
25 def route_path(name, params=None, **kwargs):
25 def route_path(name, params=None, **kwargs):
26 import urllib.request, urllib.parse, urllib.error
26 import urllib.request
27 import urllib.parse
28 import urllib.error
27
29
28 base_url = {
30 base_url = {
29 'edit_repo_perms': '/{repo_name}/settings/permissions'
31 'edit_repo_perms': '/{repo_name}/settings/permissions'
30 # update is the same url
32 # update is the same url
31 }[name].format(**kwargs)
33 }[name].format(**kwargs)
32
34
33 if params:
35 if params:
34 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
36 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
35 return base_url
37 return base_url
36
38
37
39
38 @pytest.mark.usefixtures("app")
40 @pytest.mark.usefixtures("app")
39 class TestRepoPermissionsView(object):
41 class TestRepoPermissionsView(object):
40
42
41 def test_edit_perms_view(self, user_util, autologin_user):
43 def test_edit_perms_view(self, user_util, autologin_user):
42 repo = user_util.create_repo()
44 repo = user_util.create_repo()
43 self.app.get(
45 self.app.get(
44 route_path('edit_repo_perms',
46 route_path('edit_repo_perms',
45 repo_name=repo.repo_name), status=200)
47 repo_name=repo.repo_name), status=200)
46
48
47 def test_update_permissions(self, csrf_token, user_util):
49 def test_update_permissions(self, csrf_token, user_util):
48 repo = user_util.create_repo()
50 repo = user_util.create_repo()
49 repo_name = repo.repo_name
51 repo_name = repo.repo_name
50 user = user_util.create_user()
52 user = user_util.create_user()
51 user_id = user.user_id
53 user_id = user.user_id
52 username = user.username
54 username = user.username
53
55
54 # grant new
56 # grant new
55 form_data = permission_update_data_generator(
57 form_data = permission_update_data_generator(
56 csrf_token,
58 csrf_token,
57 default='repository.write',
59 default='repository.write',
58 grant=[(user_id, 'repository.write', username, 'user')])
60 grant=[(user_id, 'repository.write', username, 'user')])
59
61
60 response = self.app.post(
62 response = self.app.post(
61 route_path('edit_repo_perms',
63 route_path('edit_repo_perms',
62 repo_name=repo_name), form_data).follow()
64 repo_name=repo_name), form_data).follow()
63
65
64 assert 'Repository access permissions updated' in response
66 assert 'Repository access permissions updated' in response
65
67
66 # revoke given
68 # revoke given
67 form_data = permission_update_data_generator(
69 form_data = permission_update_data_generator(
68 csrf_token,
70 csrf_token,
69 default='repository.read',
71 default='repository.read',
70 revoke=[(user_id, 'user')])
72 revoke=[(user_id, 'user')])
71
73
72 response = self.app.post(
74 response = self.app.post(
73 route_path('edit_repo_perms',
75 route_path('edit_repo_perms',
74 repo_name=repo_name), form_data).follow()
76 repo_name=repo_name), form_data).follow()
75
77
76 assert 'Repository access permissions updated' in response
78 assert 'Repository access permissions updated' in response
@@ -1,1679 +1,1680 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 import mock
19 import mock
20 import pytest
20 import pytest
21
21
22 import rhodecode
22 import rhodecode
23 from rhodecode.lib import helpers as h
23 from rhodecode.lib import helpers as h
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib.ext_json import json
26 from rhodecode.lib.ext_json import json
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.comment import CommentsModel
34 from rhodecode.tests import (
34 from rhodecode.tests import (
35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36
36
37
37
38 def route_path(name, params=None, **kwargs):
38 def route_path(name, params=None, **kwargs):
39 import urllib.request, urllib.parse, urllib.error
39 import urllib.request
40 import urllib.parse
41 import urllib.error
40
42
41 base_url = {
43 base_url = {
42 'repo_changelog': '/{repo_name}/changelog',
44 'repo_changelog': '/{repo_name}/changelog',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
45 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 'repo_commits': '/{repo_name}/commits',
46 'repo_commits': '/{repo_name}/commits',
45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
47 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
48 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
47 'pullrequest_show_all': '/{repo_name}/pull-request',
49 'pullrequest_show_all': '/{repo_name}/pull-request',
48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
50 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
51 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
52 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
51 'pullrequest_new': '/{repo_name}/pull-request/new',
53 'pullrequest_new': '/{repo_name}/pull-request/new',
52 'pullrequest_create': '/{repo_name}/pull-request/create',
54 'pullrequest_create': '/{repo_name}/pull-request/create',
53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
55 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
56 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
57 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
58 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
59 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
60 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
59 }[name].format(**kwargs)
61 }[name].format(**kwargs)
60
62
61 if params:
63 if params:
62 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
64 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
63 return base_url
65 return base_url
64
66
65
67
66 @pytest.mark.usefixtures('app', 'autologin_user')
68 @pytest.mark.usefixtures('app', 'autologin_user')
67 @pytest.mark.backends("git", "hg")
69 @pytest.mark.backends("git", "hg")
68 class TestPullrequestsView(object):
70 class TestPullrequestsView(object):
69
71
70 def test_index(self, backend):
72 def test_index(self, backend):
71 self.app.get(route_path(
73 self.app.get(route_path(
72 'pullrequest_new',
74 'pullrequest_new',
73 repo_name=backend.repo_name))
75 repo_name=backend.repo_name))
74
76
75 def test_option_menu_create_pull_request_exists(self, backend):
77 def test_option_menu_create_pull_request_exists(self, backend):
76 repo_name = backend.repo_name
78 repo_name = backend.repo_name
77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
79 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
78
80
79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
81 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
80 'pullrequest_new', repo_name=repo_name)
82 'pullrequest_new', repo_name=repo_name)
81 response.mustcontain(create_pr_link)
83 response.mustcontain(create_pr_link)
82
84
83 def test_create_pr_form_with_raw_commit_id(self, backend):
85 def test_create_pr_form_with_raw_commit_id(self, backend):
84 repo = backend.repo
86 repo = backend.repo
85
87
86 self.app.get(
88 self.app.get(
87 route_path('pullrequest_new', repo_name=repo.repo_name,
89 route_path('pullrequest_new', repo_name=repo.repo_name,
88 commit=repo.get_commit().raw_id),
90 commit=repo.get_commit().raw_id),
89 status=200)
91 status=200)
90
92
91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
93 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
92 @pytest.mark.parametrize('range_diff', ["0", "1"])
94 @pytest.mark.parametrize('range_diff', ["0", "1"])
93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
95 def test_show(self, pr_util, pr_merge_enabled, range_diff):
94 pull_request = pr_util.create_pull_request(
96 pull_request = pr_util.create_pull_request(
95 mergeable=pr_merge_enabled, enable_notifications=False)
97 mergeable=pr_merge_enabled, enable_notifications=False)
96
98
97 response = self.app.get(route_path(
99 response = self.app.get(route_path(
98 'pullrequest_show',
100 'pullrequest_show',
99 repo_name=pull_request.target_repo.scm_instance().name,
101 repo_name=pull_request.target_repo.scm_instance().name,
100 pull_request_id=pull_request.pull_request_id,
102 pull_request_id=pull_request.pull_request_id,
101 params={'range-diff': range_diff}))
103 params={'range-diff': range_diff}))
102
104
103 for commit_id in pull_request.revisions:
105 for commit_id in pull_request.revisions:
104 response.mustcontain(commit_id)
106 response.mustcontain(commit_id)
105
107
106 response.mustcontain(pull_request.target_ref_parts.type)
108 response.mustcontain(pull_request.target_ref_parts.type)
107 response.mustcontain(pull_request.target_ref_parts.name)
109 response.mustcontain(pull_request.target_ref_parts.name)
108
110
109 response.mustcontain('class="pull-request-merge"')
111 response.mustcontain('class="pull-request-merge"')
110
112
111 if pr_merge_enabled:
113 if pr_merge_enabled:
112 response.mustcontain('Pull request reviewer approval is pending')
114 response.mustcontain('Pull request reviewer approval is pending')
113 else:
115 else:
114 response.mustcontain('Server-side pull request merging is disabled.')
116 response.mustcontain('Server-side pull request merging is disabled.')
115
117
116 if range_diff == "1":
118 if range_diff == "1":
117 response.mustcontain('Turn off: Show the diff as commit range')
119 response.mustcontain('Turn off: Show the diff as commit range')
118
120
119 def test_show_versions_of_pr(self, backend, csrf_token):
121 def test_show_versions_of_pr(self, backend, csrf_token):
120 commits = [
122 commits = [
121 {'message': 'initial-commit',
123 {'message': 'initial-commit',
122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
124 'added': [FileNode(b'test-file.txt', b'LINE1\n')]},
123
125
124 {'message': 'commit-1',
126 {'message': 'commit-1',
125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
127 'changed': [FileNode(b'test-file.txt', b'LINE1\nLINE2\n')]},
126 # Above is the initial version of PR that changes a single line
128 # Above is the initial version of PR that changes a single line
127
129
128 # from now on we'll add 3x commit adding a nother line on each step
130 # from now on we'll add 3x commit adding a nother line on each step
129 {'message': 'commit-2',
131 {'message': 'commit-2',
130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
132 'changed': [FileNode(b'test-file.txt', b'LINE1\nLINE2\nLINE3\n')]},
131
133
132 {'message': 'commit-3',
134 {'message': 'commit-3',
133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
135 'changed': [FileNode(b'test-file.txt', b'LINE1\nLINE2\nLINE3\nLINE4\n')]},
134
136
135 {'message': 'commit-4',
137 {'message': 'commit-4',
136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
138 'changed': [FileNode(b'test-file.txt', b'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
137 ]
139 ]
138
140
139 commit_ids = backend.create_master_repo(commits)
141 commit_ids = backend.create_master_repo(commits)
140 target = backend.create_repo(heads=['initial-commit'])
142 target = backend.create_repo(heads=['initial-commit'])
141 source = backend.create_repo(heads=['commit-1'])
143 source = backend.create_repo(heads=['commit-1'])
142 source_repo_name = source.repo_name
144 source_repo_name = source.repo_name
143 target_repo_name = target.repo_name
145 target_repo_name = target.repo_name
144
146
145 target_ref = 'branch:{branch}:{commit_id}'.format(
147 target_ref = 'branch:{branch}:{commit_id}'.format(
146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
148 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
147 source_ref = 'branch:{branch}:{commit_id}'.format(
149 source_ref = 'branch:{branch}:{commit_id}'.format(
148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
150 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
149
151
150 response = self.app.post(
152 response = self.app.post(
151 route_path('pullrequest_create', repo_name=source.repo_name),
153 route_path('pullrequest_create', repo_name=source.repo_name),
152 [
154 [
153 ('source_repo', source_repo_name),
155 ('source_repo', source_repo_name),
154 ('source_ref', source_ref),
156 ('source_ref', source_ref),
155 ('target_repo', target_repo_name),
157 ('target_repo', target_repo_name),
156 ('target_ref', target_ref),
158 ('target_ref', target_ref),
157 ('common_ancestor', commit_ids['initial-commit']),
159 ('common_ancestor', commit_ids['initial-commit']),
158 ('pullrequest_title', 'Title'),
160 ('pullrequest_title', 'Title'),
159 ('pullrequest_desc', 'Description'),
161 ('pullrequest_desc', 'Description'),
160 ('description_renderer', 'markdown'),
162 ('description_renderer', 'markdown'),
161 ('__start__', 'review_members:sequence'),
163 ('__start__', 'review_members:sequence'),
162 ('__start__', 'reviewer:mapping'),
164 ('__start__', 'reviewer:mapping'),
163 ('user_id', '1'),
165 ('user_id', '1'),
164 ('__start__', 'reasons:sequence'),
166 ('__start__', 'reasons:sequence'),
165 ('reason', 'Some reason'),
167 ('reason', 'Some reason'),
166 ('__end__', 'reasons:sequence'),
168 ('__end__', 'reasons:sequence'),
167 ('__start__', 'rules:sequence'),
169 ('__start__', 'rules:sequence'),
168 ('__end__', 'rules:sequence'),
170 ('__end__', 'rules:sequence'),
169 ('mandatory', 'False'),
171 ('mandatory', 'False'),
170 ('__end__', 'reviewer:mapping'),
172 ('__end__', 'reviewer:mapping'),
171 ('__end__', 'review_members:sequence'),
173 ('__end__', 'review_members:sequence'),
172 ('__start__', 'revisions:sequence'),
174 ('__start__', 'revisions:sequence'),
173 ('revisions', commit_ids['commit-1']),
175 ('revisions', commit_ids['commit-1']),
174 ('__end__', 'revisions:sequence'),
176 ('__end__', 'revisions:sequence'),
175 ('user', ''),
177 ('user', ''),
176 ('csrf_token', csrf_token),
178 ('csrf_token', csrf_token),
177 ],
179 ],
178 status=302)
180 status=302)
179
181
180 location = response.headers['Location']
182 location = response.headers['Location']
181
183
182 pull_request_id = location.rsplit('/', 1)[1]
184 pull_request_id = location.rsplit('/', 1)[1]
183 assert pull_request_id != 'new'
185 assert pull_request_id != 'new'
184 pull_request = PullRequest.get(int(pull_request_id))
186 pull_request = PullRequest.get(int(pull_request_id))
185
187
186 pull_request_id = pull_request.pull_request_id
188 pull_request_id = pull_request.pull_request_id
187
189
188 # Show initial version of PR
190 # Show initial version of PR
189 response = self.app.get(
191 response = self.app.get(
190 route_path('pullrequest_show',
192 route_path('pullrequest_show',
191 repo_name=target_repo_name,
193 repo_name=target_repo_name,
192 pull_request_id=pull_request_id))
194 pull_request_id=pull_request_id))
193
195
194 response.mustcontain('commit-1')
196 response.mustcontain('commit-1')
195 response.mustcontain(no=['commit-2'])
197 response.mustcontain(no=['commit-2'])
196 response.mustcontain(no=['commit-3'])
198 response.mustcontain(no=['commit-3'])
197 response.mustcontain(no=['commit-4'])
199 response.mustcontain(no=['commit-4'])
198
200
199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
201 response.mustcontain('cb-addition"></span><span>LINE2</span>')
200 response.mustcontain(no=['LINE3'])
202 response.mustcontain(no=['LINE3'])
201 response.mustcontain(no=['LINE4'])
203 response.mustcontain(no=['LINE4'])
202 response.mustcontain(no=['LINE5'])
204 response.mustcontain(no=['LINE5'])
203
205
204 # update PR #1
206 # update PR #1
205 source_repo = Repository.get_by_repo_name(source_repo_name)
207 source_repo = Repository.get_by_repo_name(source_repo_name)
206 backend.pull_heads(source_repo, heads=['commit-2'])
208 backend.pull_heads(source_repo, heads=['commit-2'])
207 response = self.app.post(
209 response = self.app.post(
208 route_path('pullrequest_update',
210 route_path('pullrequest_update',
209 repo_name=target_repo_name, pull_request_id=pull_request_id),
211 repo_name=target_repo_name, pull_request_id=pull_request_id),
210 params={'update_commits': 'true', 'csrf_token': csrf_token})
212 params={'update_commits': 'true', 'csrf_token': csrf_token})
211
213
212 # update PR #2
214 # update PR #2
213 source_repo = Repository.get_by_repo_name(source_repo_name)
215 source_repo = Repository.get_by_repo_name(source_repo_name)
214 backend.pull_heads(source_repo, heads=['commit-3'])
216 backend.pull_heads(source_repo, heads=['commit-3'])
215 response = self.app.post(
217 response = self.app.post(
216 route_path('pullrequest_update',
218 route_path('pullrequest_update',
217 repo_name=target_repo_name, pull_request_id=pull_request_id),
219 repo_name=target_repo_name, pull_request_id=pull_request_id),
218 params={'update_commits': 'true', 'csrf_token': csrf_token})
220 params={'update_commits': 'true', 'csrf_token': csrf_token})
219
221
220 # update PR #3
222 # update PR #3
221 source_repo = Repository.get_by_repo_name(source_repo_name)
223 source_repo = Repository.get_by_repo_name(source_repo_name)
222 backend.pull_heads(source_repo, heads=['commit-4'])
224 backend.pull_heads(source_repo, heads=['commit-4'])
223 response = self.app.post(
225 response = self.app.post(
224 route_path('pullrequest_update',
226 route_path('pullrequest_update',
225 repo_name=target_repo_name, pull_request_id=pull_request_id),
227 repo_name=target_repo_name, pull_request_id=pull_request_id),
226 params={'update_commits': 'true', 'csrf_token': csrf_token})
228 params={'update_commits': 'true', 'csrf_token': csrf_token})
227
229
228 # Show final version !
230 # Show final version !
229 response = self.app.get(
231 response = self.app.get(
230 route_path('pullrequest_show',
232 route_path('pullrequest_show',
231 repo_name=target_repo_name,
233 repo_name=target_repo_name,
232 pull_request_id=pull_request_id))
234 pull_request_id=pull_request_id))
233
235
234 # 3 updates, and the latest == 4
236 # 3 updates, and the latest == 4
235 response.mustcontain('4 versions available for this pull request')
237 response.mustcontain('4 versions available for this pull request')
236 response.mustcontain(no=['rhodecode diff rendering error'])
238 response.mustcontain(no=['rhodecode diff rendering error'])
237
239
238 # initial show must have 3 commits, and 3 adds
240 # initial show must have 3 commits, and 3 adds
239 response.mustcontain('commit-1')
241 response.mustcontain('commit-1')
240 response.mustcontain('commit-2')
242 response.mustcontain('commit-2')
241 response.mustcontain('commit-3')
243 response.mustcontain('commit-3')
242 response.mustcontain('commit-4')
244 response.mustcontain('commit-4')
243
245
244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
246 response.mustcontain('cb-addition"></span><span>LINE2</span>')
245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
247 response.mustcontain('cb-addition"></span><span>LINE3</span>')
246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
248 response.mustcontain('cb-addition"></span><span>LINE4</span>')
247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
249 response.mustcontain('cb-addition"></span><span>LINE5</span>')
248
250
249 # fetch versions
251 # fetch versions
250 pr = PullRequest.get(pull_request_id)
252 pr = PullRequest.get(pull_request_id)
251 versions = [x.pull_request_version_id for x in pr.versions.all()]
253 versions = [x.pull_request_version_id for x in pr.versions.all()]
252 assert len(versions) == 3
254 assert len(versions) == 3
253
255
254 # show v1,v2,v3,v4
256 # show v1,v2,v3,v4
255 def cb_line(text):
257 def cb_line(text):
256 return 'cb-addition"></span><span>{}</span>'.format(text)
258 return 'cb-addition"></span><span>{}</span>'.format(text)
257
259
258 def cb_context(text):
260 def cb_context(text):
259 return '<span class="cb-code"><span class="cb-action cb-context">' \
261 return '<span class="cb-code"><span class="cb-action cb-context">' \
260 '</span><span>{}</span></span>'.format(text)
262 '</span><span>{}</span></span>'.format(text)
261
263
262 commit_tests = {
264 commit_tests = {
263 # in response, not in response
265 # in response, not in response
264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
266 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
267 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
268 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
269 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
268 }
270 }
269 diff_tests = {
271 diff_tests = {
270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
272 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
273 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
274 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
275 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
274 }
276 }
275 for idx, ver in enumerate(versions, 1):
277 for idx, ver in enumerate(versions, 1):
276
278
277 response = self.app.get(
279 response = self.app.get(
278 route_path('pullrequest_show',
280 route_path('pullrequest_show',
279 repo_name=target_repo_name,
281 repo_name=target_repo_name,
280 pull_request_id=pull_request_id,
282 pull_request_id=pull_request_id,
281 params={'version': ver}))
283 params={'version': ver}))
282
284
283 response.mustcontain(no=['rhodecode diff rendering error'])
285 response.mustcontain(no=['rhodecode diff rendering error'])
284 response.mustcontain('Showing changes at v{}'.format(idx))
286 response.mustcontain('Showing changes at v{}'.format(idx))
285
287
286 yes, no = commit_tests[idx]
288 yes, no = commit_tests[idx]
287 for y in yes:
289 for y in yes:
288 response.mustcontain(y)
290 response.mustcontain(y)
289 for n in no:
291 for n in no:
290 response.mustcontain(no=n)
292 response.mustcontain(no=n)
291
293
292 yes, no = diff_tests[idx]
294 yes, no = diff_tests[idx]
293 for y in yes:
295 for y in yes:
294 response.mustcontain(cb_line(y))
296 response.mustcontain(cb_line(y))
295 for n in no:
297 for n in no:
296 response.mustcontain(no=n)
298 response.mustcontain(no=n)
297
299
298 # show diff between versions
300 # show diff between versions
299 diff_compare_tests = {
301 diff_compare_tests = {
300 1: (['LINE3'], ['LINE1', 'LINE2']),
302 1: (['LINE3'], ['LINE1', 'LINE2']),
301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
303 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
304 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
303 }
305 }
304 for idx, ver in enumerate(versions, 1):
306 for idx, ver in enumerate(versions, 1):
305 adds, context = diff_compare_tests[idx]
307 adds, context = diff_compare_tests[idx]
306
308
307 to_ver = ver+1
309 to_ver = ver+1
308 if idx == 3:
310 if idx == 3:
309 to_ver = 'latest'
311 to_ver = 'latest'
310
312
311 response = self.app.get(
313 response = self.app.get(
312 route_path('pullrequest_show',
314 route_path('pullrequest_show',
313 repo_name=target_repo_name,
315 repo_name=target_repo_name,
314 pull_request_id=pull_request_id,
316 pull_request_id=pull_request_id,
315 params={'from_version': versions[0], 'version': to_ver}))
317 params={'from_version': versions[0], 'version': to_ver}))
316
318
317 response.mustcontain(no=['rhodecode diff rendering error'])
319 response.mustcontain(no=['rhodecode diff rendering error'])
318
320
319 for a in adds:
321 for a in adds:
320 response.mustcontain(cb_line(a))
322 response.mustcontain(cb_line(a))
321 for c in context:
323 for c in context:
322 response.mustcontain(cb_context(c))
324 response.mustcontain(cb_context(c))
323
325
324 # test version v2 -> v3
326 # test version v2 -> v3
325 response = self.app.get(
327 response = self.app.get(
326 route_path('pullrequest_show',
328 route_path('pullrequest_show',
327 repo_name=target_repo_name,
329 repo_name=target_repo_name,
328 pull_request_id=pull_request_id,
330 pull_request_id=pull_request_id,
329 params={'from_version': versions[1], 'version': versions[2]}))
331 params={'from_version': versions[1], 'version': versions[2]}))
330
332
331 response.mustcontain(cb_context('LINE1'))
333 response.mustcontain(cb_context('LINE1'))
332 response.mustcontain(cb_context('LINE2'))
334 response.mustcontain(cb_context('LINE2'))
333 response.mustcontain(cb_context('LINE3'))
335 response.mustcontain(cb_context('LINE3'))
334 response.mustcontain(cb_line('LINE4'))
336 response.mustcontain(cb_line('LINE4'))
335
337
336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
338 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
337 # Logout
339 # Logout
338 response = self.app.post(
340 response = self.app.post(
339 h.route_path('logout'),
341 h.route_path('logout'),
340 params={'csrf_token': csrf_token})
342 params={'csrf_token': csrf_token})
341 # Login as regular user
343 # Login as regular user
342 response = self.app.post(h.route_path('login'),
344 response = self.app.post(h.route_path('login'),
343 {'username': TEST_USER_REGULAR_LOGIN,
345 {'username': TEST_USER_REGULAR_LOGIN,
344 'password': 'test12'})
346 'password': 'test12'})
345
347
346 pull_request = pr_util.create_pull_request(
348 pull_request = pr_util.create_pull_request(
347 author=TEST_USER_REGULAR_LOGIN)
349 author=TEST_USER_REGULAR_LOGIN)
348
350
349 response = self.app.get(route_path(
351 response = self.app.get(route_path(
350 'pullrequest_show',
352 'pullrequest_show',
351 repo_name=pull_request.target_repo.scm_instance().name,
353 repo_name=pull_request.target_repo.scm_instance().name,
352 pull_request_id=pull_request.pull_request_id))
354 pull_request_id=pull_request.pull_request_id))
353
355
354 response.mustcontain('Server-side pull request merging is disabled.')
356 response.mustcontain('Server-side pull request merging is disabled.')
355
357
356 assert_response = response.assert_response()
358 assert_response = response.assert_response()
357 # for regular user without a merge permissions, we don't see it
359 # for regular user without a merge permissions, we don't see it
358 assert_response.no_element_exists('#close-pull-request-action')
360 assert_response.no_element_exists('#close-pull-request-action')
359
361
360 user_util.grant_user_permission_to_repo(
362 user_util.grant_user_permission_to_repo(
361 pull_request.target_repo,
363 pull_request.target_repo,
362 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
364 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
363 'repository.write')
365 'repository.write')
364 response = self.app.get(route_path(
366 response = self.app.get(route_path(
365 'pullrequest_show',
367 'pullrequest_show',
366 repo_name=pull_request.target_repo.scm_instance().name,
368 repo_name=pull_request.target_repo.scm_instance().name,
367 pull_request_id=pull_request.pull_request_id))
369 pull_request_id=pull_request.pull_request_id))
368
370
369 response.mustcontain('Server-side pull request merging is disabled.')
371 response.mustcontain('Server-side pull request merging is disabled.')
370
372
371 assert_response = response.assert_response()
373 assert_response = response.assert_response()
372 # now regular user has a merge permissions, we have CLOSE button
374 # now regular user has a merge permissions, we have CLOSE button
373 assert_response.one_element_exists('#close-pull-request-action')
375 assert_response.one_element_exists('#close-pull-request-action')
374
376
375 def test_show_invalid_commit_id(self, pr_util):
377 def test_show_invalid_commit_id(self, pr_util):
376 # Simulating invalid revisions which will cause a lookup error
378 # Simulating invalid revisions which will cause a lookup error
377 pull_request = pr_util.create_pull_request()
379 pull_request = pr_util.create_pull_request()
378 pull_request.revisions = ['invalid']
380 pull_request.revisions = ['invalid']
379 Session().add(pull_request)
381 Session().add(pull_request)
380 Session().commit()
382 Session().commit()
381
383
382 response = self.app.get(route_path(
384 response = self.app.get(route_path(
383 'pullrequest_show',
385 'pullrequest_show',
384 repo_name=pull_request.target_repo.scm_instance().name,
386 repo_name=pull_request.target_repo.scm_instance().name,
385 pull_request_id=pull_request.pull_request_id))
387 pull_request_id=pull_request.pull_request_id))
386
388
387 for commit_id in pull_request.revisions:
389 for commit_id in pull_request.revisions:
388 response.mustcontain(commit_id)
390 response.mustcontain(commit_id)
389
391
390 def test_show_invalid_source_reference(self, pr_util):
392 def test_show_invalid_source_reference(self, pr_util):
391 pull_request = pr_util.create_pull_request()
393 pull_request = pr_util.create_pull_request()
392 pull_request.source_ref = 'branch:b:invalid'
394 pull_request.source_ref = 'branch:b:invalid'
393 Session().add(pull_request)
395 Session().add(pull_request)
394 Session().commit()
396 Session().commit()
395
397
396 self.app.get(route_path(
398 self.app.get(route_path(
397 'pullrequest_show',
399 'pullrequest_show',
398 repo_name=pull_request.target_repo.scm_instance().name,
400 repo_name=pull_request.target_repo.scm_instance().name,
399 pull_request_id=pull_request.pull_request_id))
401 pull_request_id=pull_request.pull_request_id))
400
402
401 def test_edit_title_description(self, pr_util, csrf_token):
403 def test_edit_title_description(self, pr_util, csrf_token):
402 pull_request = pr_util.create_pull_request()
404 pull_request = pr_util.create_pull_request()
403 pull_request_id = pull_request.pull_request_id
405 pull_request_id = pull_request.pull_request_id
404
406
405 response = self.app.post(
407 response = self.app.post(
406 route_path('pullrequest_update',
408 route_path('pullrequest_update',
407 repo_name=pull_request.target_repo.repo_name,
409 repo_name=pull_request.target_repo.repo_name,
408 pull_request_id=pull_request_id),
410 pull_request_id=pull_request_id),
409 params={
411 params={
410 'edit_pull_request': 'true',
412 'edit_pull_request': 'true',
411 'title': 'New title',
413 'title': 'New title',
412 'description': 'New description',
414 'description': 'New description',
413 'csrf_token': csrf_token})
415 'csrf_token': csrf_token})
414
416
415 assert_session_flash(
417 assert_session_flash(
416 response, u'Pull request title & description updated.',
418 response, 'Pull request title & description updated.',
417 category='success')
419 category='success')
418
420
419 pull_request = PullRequest.get(pull_request_id)
421 pull_request = PullRequest.get(pull_request_id)
420 assert pull_request.title == 'New title'
422 assert pull_request.title == 'New title'
421 assert pull_request.description == 'New description'
423 assert pull_request.description == 'New description'
422
424
423 def test_edit_title_description(self, pr_util, csrf_token):
425 def test_edit_title_description_special(self, pr_util, csrf_token):
424 pull_request = pr_util.create_pull_request()
426 pull_request = pr_util.create_pull_request()
425 pull_request_id = pull_request.pull_request_id
427 pull_request_id = pull_request.pull_request_id
426
428
427 response = self.app.post(
429 response = self.app.post(
428 route_path('pullrequest_update',
430 route_path('pullrequest_update',
429 repo_name=pull_request.target_repo.repo_name,
431 repo_name=pull_request.target_repo.repo_name,
430 pull_request_id=pull_request_id),
432 pull_request_id=pull_request_id),
431 params={
433 params={
432 'edit_pull_request': 'true',
434 'edit_pull_request': 'true',
433 'title': 'New title {} {2} {foo}',
435 'title': 'New title {} {2} {foo}',
434 'description': 'New description',
436 'description': 'New description',
435 'csrf_token': csrf_token})
437 'csrf_token': csrf_token})
436
438
437 assert_session_flash(
439 assert_session_flash(
438 response, u'Pull request title & description updated.',
440 response, 'Pull request title & description updated.',
439 category='success')
441 category='success')
440
442
441 pull_request = PullRequest.get(pull_request_id)
443 pull_request = PullRequest.get(pull_request_id)
442 assert pull_request.title_safe == 'New title {{}} {{2}} {{foo}}'
444 assert pull_request.title_safe == 'New title {{}} {{2}} {{foo}}'
443
445
444 def test_edit_title_description_closed(self, pr_util, csrf_token):
446 def test_edit_title_description_closed(self, pr_util, csrf_token):
445 pull_request = pr_util.create_pull_request()
447 pull_request = pr_util.create_pull_request()
446 pull_request_id = pull_request.pull_request_id
448 pull_request_id = pull_request.pull_request_id
447 repo_name = pull_request.target_repo.repo_name
449 repo_name = pull_request.target_repo.repo_name
448 pr_util.close()
450 pr_util.close()
449
451
450 response = self.app.post(
452 response = self.app.post(
451 route_path('pullrequest_update',
453 route_path('pullrequest_update',
452 repo_name=repo_name, pull_request_id=pull_request_id),
454 repo_name=repo_name, pull_request_id=pull_request_id),
453 params={
455 params={
454 'edit_pull_request': 'true',
456 'edit_pull_request': 'true',
455 'title': 'New title',
457 'title': 'New title',
456 'description': 'New description',
458 'description': 'New description',
457 'csrf_token': csrf_token}, status=200)
459 'csrf_token': csrf_token}, status=200)
458 assert_session_flash(
460 assert_session_flash(
459 response, u'Cannot update closed pull requests.',
461 response, 'Cannot update closed pull requests.',
460 category='error')
462 category='error')
461
463
462 def test_update_invalid_source_reference(self, pr_util, csrf_token):
464 def test_update_invalid_source_reference(self, pr_util, csrf_token):
463 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
465 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
464
466
465 pull_request = pr_util.create_pull_request()
467 pull_request = pr_util.create_pull_request()
466 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
468 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
467 Session().add(pull_request)
469 Session().add(pull_request)
468 Session().commit()
470 Session().commit()
469
471
470 pull_request_id = pull_request.pull_request_id
472 pull_request_id = pull_request.pull_request_id
471
473
472 response = self.app.post(
474 response = self.app.post(
473 route_path('pullrequest_update',
475 route_path('pullrequest_update',
474 repo_name=pull_request.target_repo.repo_name,
476 repo_name=pull_request.target_repo.repo_name,
475 pull_request_id=pull_request_id),
477 pull_request_id=pull_request_id),
476 params={'update_commits': 'true', 'csrf_token': csrf_token})
478 params={'update_commits': 'true', 'csrf_token': csrf_token})
477
479
478 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
480 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
479 UpdateFailureReason.MISSING_SOURCE_REF])
481 UpdateFailureReason.MISSING_SOURCE_REF])
480 assert_session_flash(response, expected_msg, category='error')
482 assert_session_flash(response, expected_msg, category='error')
481
483
482 def test_missing_target_reference(self, pr_util, csrf_token):
484 def test_missing_target_reference(self, pr_util, csrf_token):
483 from rhodecode.lib.vcs.backends.base import MergeFailureReason
485 from rhodecode.lib.vcs.backends.base import MergeFailureReason
484 pull_request = pr_util.create_pull_request(
486 pull_request = pr_util.create_pull_request(
485 approved=True, mergeable=True)
487 approved=True, mergeable=True)
486 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
488 unicode_reference = 'branch:invalid-branch:invalid-commit-id'
487 pull_request.target_ref = unicode_reference
489 pull_request.target_ref = unicode_reference
488 Session().add(pull_request)
490 Session().add(pull_request)
489 Session().commit()
491 Session().commit()
490
492
491 pull_request_id = pull_request.pull_request_id
493 pull_request_id = pull_request.pull_request_id
492 pull_request_url = route_path(
494 pull_request_url = route_path(
493 'pullrequest_show',
495 'pullrequest_show',
494 repo_name=pull_request.target_repo.repo_name,
496 repo_name=pull_request.target_repo.repo_name,
495 pull_request_id=pull_request_id)
497 pull_request_id=pull_request_id)
496
498
497 response = self.app.get(pull_request_url)
499 response = self.app.get(pull_request_url)
498 target_ref_id = 'invalid-branch'
500 target_ref_id = 'invalid-branch'
499 merge_resp = MergeResponse(
501 merge_resp = MergeResponse(
500 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
502 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
501 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
503 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
502 response.assert_response().element_contains(
504 response.assert_response().element_contains(
503 'div[data-role="merge-message"]', merge_resp.merge_status_message)
505 'div[data-role="merge-message"]', merge_resp.merge_status_message)
504
506
505 def test_comment_and_close_pull_request_custom_message_approved(
507 def test_comment_and_close_pull_request_custom_message_approved(
506 self, pr_util, csrf_token, xhr_header):
508 self, pr_util, csrf_token, xhr_header):
507
509
508 pull_request = pr_util.create_pull_request(approved=True)
510 pull_request = pr_util.create_pull_request(approved=True)
509 pull_request_id = pull_request.pull_request_id
511 pull_request_id = pull_request.pull_request_id
510 author = pull_request.user_id
512 author = pull_request.user_id
511 repo = pull_request.target_repo.repo_id
513 repo = pull_request.target_repo.repo_id
512
514
513 self.app.post(
515 self.app.post(
514 route_path('pullrequest_comment_create',
516 route_path('pullrequest_comment_create',
515 repo_name=pull_request.target_repo.scm_instance().name,
517 repo_name=pull_request.target_repo.scm_instance().name,
516 pull_request_id=pull_request_id),
518 pull_request_id=pull_request_id),
517 params={
519 params={
518 'close_pull_request': '1',
520 'close_pull_request': '1',
519 'text': 'Closing a PR',
521 'text': 'Closing a PR',
520 'csrf_token': csrf_token},
522 'csrf_token': csrf_token},
521 extra_environ=xhr_header,)
523 extra_environ=xhr_header,)
522
524
523 journal = UserLog.query()\
525 journal = UserLog.query()\
524 .filter(UserLog.user_id == author)\
526 .filter(UserLog.user_id == author)\
525 .filter(UserLog.repository_id == repo) \
527 .filter(UserLog.repository_id == repo) \
526 .order_by(UserLog.user_log_id.asc()) \
528 .order_by(UserLog.user_log_id.asc()) \
527 .all()
529 .all()
528 assert journal[-1].action == 'repo.pull_request.close'
530 assert journal[-1].action == 'repo.pull_request.close'
529
531
530 pull_request = PullRequest.get(pull_request_id)
532 pull_request = PullRequest.get(pull_request_id)
531 assert pull_request.is_closed()
533 assert pull_request.is_closed()
532
534
533 status = ChangesetStatusModel().get_status(
535 status = ChangesetStatusModel().get_status(
534 pull_request.source_repo, pull_request=pull_request)
536 pull_request.source_repo, pull_request=pull_request)
535 assert status == ChangesetStatus.STATUS_APPROVED
537 assert status == ChangesetStatus.STATUS_APPROVED
536 comments = ChangesetComment().query() \
538 comments = ChangesetComment().query() \
537 .filter(ChangesetComment.pull_request == pull_request) \
539 .filter(ChangesetComment.pull_request == pull_request) \
538 .order_by(ChangesetComment.comment_id.asc())\
540 .order_by(ChangesetComment.comment_id.asc())\
539 .all()
541 .all()
540 assert comments[-1].text == 'Closing a PR'
542 assert comments[-1].text == 'Closing a PR'
541
543
542 def test_comment_force_close_pull_request_rejected(
544 def test_comment_force_close_pull_request_rejected(
543 self, pr_util, csrf_token, xhr_header):
545 self, pr_util, csrf_token, xhr_header):
544 pull_request = pr_util.create_pull_request()
546 pull_request = pr_util.create_pull_request()
545 pull_request_id = pull_request.pull_request_id
547 pull_request_id = pull_request.pull_request_id
546 PullRequestModel().update_reviewers(
548 PullRequestModel().update_reviewers(
547 pull_request_id, [
549 pull_request_id, [
548 (1, ['reason'], False, 'reviewer', []),
550 (1, ['reason'], False, 'reviewer', []),
549 (2, ['reason2'], False, 'reviewer', [])],
551 (2, ['reason2'], False, 'reviewer', [])],
550 pull_request.author)
552 pull_request.author)
551 author = pull_request.user_id
553 author = pull_request.user_id
552 repo = pull_request.target_repo.repo_id
554 repo = pull_request.target_repo.repo_id
553
555
554 self.app.post(
556 self.app.post(
555 route_path('pullrequest_comment_create',
557 route_path('pullrequest_comment_create',
556 repo_name=pull_request.target_repo.scm_instance().name,
558 repo_name=pull_request.target_repo.scm_instance().name,
557 pull_request_id=pull_request_id),
559 pull_request_id=pull_request_id),
558 params={
560 params={
559 'close_pull_request': '1',
561 'close_pull_request': '1',
560 'csrf_token': csrf_token},
562 'csrf_token': csrf_token},
561 extra_environ=xhr_header)
563 extra_environ=xhr_header)
562
564
563 pull_request = PullRequest.get(pull_request_id)
565 pull_request = PullRequest.get(pull_request_id)
564
566
565 journal = UserLog.query()\
567 journal = UserLog.query()\
566 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
568 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
567 .order_by(UserLog.user_log_id.asc()) \
569 .order_by(UserLog.user_log_id.asc()) \
568 .all()
570 .all()
569 assert journal[-1].action == 'repo.pull_request.close'
571 assert journal[-1].action == 'repo.pull_request.close'
570
572
571 # check only the latest status, not the review status
573 # check only the latest status, not the review status
572 status = ChangesetStatusModel().get_status(
574 status = ChangesetStatusModel().get_status(
573 pull_request.source_repo, pull_request=pull_request)
575 pull_request.source_repo, pull_request=pull_request)
574 assert status == ChangesetStatus.STATUS_REJECTED
576 assert status == ChangesetStatus.STATUS_REJECTED
575
577
576 def test_comment_and_close_pull_request(
578 def test_comment_and_close_pull_request(
577 self, pr_util, csrf_token, xhr_header):
579 self, pr_util, csrf_token, xhr_header):
578 pull_request = pr_util.create_pull_request()
580 pull_request = pr_util.create_pull_request()
579 pull_request_id = pull_request.pull_request_id
581 pull_request_id = pull_request.pull_request_id
580
582
581 response = self.app.post(
583 response = self.app.post(
582 route_path('pullrequest_comment_create',
584 route_path('pullrequest_comment_create',
583 repo_name=pull_request.target_repo.scm_instance().name,
585 repo_name=pull_request.target_repo.scm_instance().name,
584 pull_request_id=pull_request.pull_request_id),
586 pull_request_id=pull_request.pull_request_id),
585 params={
587 params={
586 'close_pull_request': 'true',
588 'close_pull_request': 'true',
587 'csrf_token': csrf_token},
589 'csrf_token': csrf_token},
588 extra_environ=xhr_header)
590 extra_environ=xhr_header)
589
591
590 assert response.json
592 assert response.json
591
593
592 pull_request = PullRequest.get(pull_request_id)
594 pull_request = PullRequest.get(pull_request_id)
593 assert pull_request.is_closed()
595 assert pull_request.is_closed()
594
596
595 # check only the latest status, not the review status
597 # check only the latest status, not the review status
596 status = ChangesetStatusModel().get_status(
598 status = ChangesetStatusModel().get_status(
597 pull_request.source_repo, pull_request=pull_request)
599 pull_request.source_repo, pull_request=pull_request)
598 assert status == ChangesetStatus.STATUS_REJECTED
600 assert status == ChangesetStatus.STATUS_REJECTED
599
601
600 def test_comment_and_close_pull_request_try_edit_comment(
602 def test_comment_and_close_pull_request_try_edit_comment(
601 self, pr_util, csrf_token, xhr_header
603 self, pr_util, csrf_token, xhr_header
602 ):
604 ):
603 pull_request = pr_util.create_pull_request()
605 pull_request = pr_util.create_pull_request()
604 pull_request_id = pull_request.pull_request_id
606 pull_request_id = pull_request.pull_request_id
605 target_scm = pull_request.target_repo.scm_instance()
607 target_scm = pull_request.target_repo.scm_instance()
606 target_scm_name = target_scm.name
608 target_scm_name = target_scm.name
607
609
608 response = self.app.post(
610 response = self.app.post(
609 route_path(
611 route_path(
610 'pullrequest_comment_create',
612 'pullrequest_comment_create',
611 repo_name=target_scm_name,
613 repo_name=target_scm_name,
612 pull_request_id=pull_request_id,
614 pull_request_id=pull_request_id,
613 ),
615 ),
614 params={
616 params={
615 'close_pull_request': 'true',
617 'close_pull_request': 'true',
616 'csrf_token': csrf_token,
618 'csrf_token': csrf_token,
617 },
619 },
618 extra_environ=xhr_header)
620 extra_environ=xhr_header)
619
621
620 assert response.json
622 assert response.json
621
623
622 pull_request = PullRequest.get(pull_request_id)
624 pull_request = PullRequest.get(pull_request_id)
623 target_scm = pull_request.target_repo.scm_instance()
625 target_scm = pull_request.target_repo.scm_instance()
624 target_scm_name = target_scm.name
626 target_scm_name = target_scm.name
625 assert pull_request.is_closed()
627 assert pull_request.is_closed()
626
628
627 # check only the latest status, not the review status
629 # check only the latest status, not the review status
628 status = ChangesetStatusModel().get_status(
630 status = ChangesetStatusModel().get_status(
629 pull_request.source_repo, pull_request=pull_request)
631 pull_request.source_repo, pull_request=pull_request)
630 assert status == ChangesetStatus.STATUS_REJECTED
632 assert status == ChangesetStatus.STATUS_REJECTED
631
633
632 for comment_id in response.json.keys():
634 for comment_id in response.json.keys():
633 test_text = 'test'
635 test_text = 'test'
634 response = self.app.post(
636 response = self.app.post(
635 route_path(
637 route_path(
636 'pullrequest_comment_edit',
638 'pullrequest_comment_edit',
637 repo_name=target_scm_name,
639 repo_name=target_scm_name,
638 pull_request_id=pull_request_id,
640 pull_request_id=pull_request_id,
639 comment_id=comment_id,
641 comment_id=comment_id,
640 ),
642 ),
641 extra_environ=xhr_header,
643 extra_environ=xhr_header,
642 params={
644 params={
643 'csrf_token': csrf_token,
645 'csrf_token': csrf_token,
644 'text': test_text,
646 'text': test_text,
645 },
647 },
646 status=403,
648 status=403,
647 )
649 )
648 assert response.status_int == 403
650 assert response.status_int == 403
649
651
650 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
652 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
651 pull_request = pr_util.create_pull_request()
653 pull_request = pr_util.create_pull_request()
652 target_scm = pull_request.target_repo.scm_instance()
654 target_scm = pull_request.target_repo.scm_instance()
653 target_scm_name = target_scm.name
655 target_scm_name = target_scm.name
654
656
655 response = self.app.post(
657 response = self.app.post(
656 route_path(
658 route_path(
657 'pullrequest_comment_create',
659 'pullrequest_comment_create',
658 repo_name=target_scm_name,
660 repo_name=target_scm_name,
659 pull_request_id=pull_request.pull_request_id),
661 pull_request_id=pull_request.pull_request_id),
660 params={
662 params={
661 'csrf_token': csrf_token,
663 'csrf_token': csrf_token,
662 'text': 'init',
664 'text': 'init',
663 },
665 },
664 extra_environ=xhr_header,
666 extra_environ=xhr_header,
665 )
667 )
666 assert response.json
668 assert response.json
667
669
668 for comment_id in response.json.keys():
670 for comment_id in response.json.keys():
669 assert comment_id
671 assert comment_id
670 test_text = 'test'
672 test_text = 'test'
671 self.app.post(
673 self.app.post(
672 route_path(
674 route_path(
673 'pullrequest_comment_edit',
675 'pullrequest_comment_edit',
674 repo_name=target_scm_name,
676 repo_name=target_scm_name,
675 pull_request_id=pull_request.pull_request_id,
677 pull_request_id=pull_request.pull_request_id,
676 comment_id=comment_id,
678 comment_id=comment_id,
677 ),
679 ),
678 extra_environ=xhr_header,
680 extra_environ=xhr_header,
679 params={
681 params={
680 'csrf_token': csrf_token,
682 'csrf_token': csrf_token,
681 'text': test_text,
683 'text': test_text,
682 'version': '0',
684 'version': '0',
683 },
685 },
684
686
685 )
687 )
686 text_form_db = ChangesetComment.query().filter(
688 text_form_db = ChangesetComment.query().filter(
687 ChangesetComment.comment_id == comment_id).first().text
689 ChangesetComment.comment_id == comment_id).first().text
688 assert test_text == text_form_db
690 assert test_text == text_form_db
689
691
690 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
692 def test_comment_and_comment_edit_special(self, pr_util, csrf_token, xhr_header):
691 pull_request = pr_util.create_pull_request()
693 pull_request = pr_util.create_pull_request()
692 target_scm = pull_request.target_repo.scm_instance()
694 target_scm = pull_request.target_repo.scm_instance()
693 target_scm_name = target_scm.name
695 target_scm_name = target_scm.name
694
696
695 response = self.app.post(
697 response = self.app.post(
696 route_path(
698 route_path(
697 'pullrequest_comment_create',
699 'pullrequest_comment_create',
698 repo_name=target_scm_name,
700 repo_name=target_scm_name,
699 pull_request_id=pull_request.pull_request_id),
701 pull_request_id=pull_request.pull_request_id),
700 params={
702 params={
701 'csrf_token': csrf_token,
703 'csrf_token': csrf_token,
702 'text': 'init',
704 'text': 'init',
703 },
705 },
704 extra_environ=xhr_header,
706 extra_environ=xhr_header,
705 )
707 )
706 assert response.json
708 assert response.json
707
709
708 for comment_id in response.json.keys():
710 for comment_id in response.json.keys():
709 test_text = 'init'
711 test_text = 'init'
710 response = self.app.post(
712 response = self.app.post(
711 route_path(
713 route_path(
712 'pullrequest_comment_edit',
714 'pullrequest_comment_edit',
713 repo_name=target_scm_name,
715 repo_name=target_scm_name,
714 pull_request_id=pull_request.pull_request_id,
716 pull_request_id=pull_request.pull_request_id,
715 comment_id=comment_id,
717 comment_id=comment_id,
716 ),
718 ),
717 extra_environ=xhr_header,
719 extra_environ=xhr_header,
718 params={
720 params={
719 'csrf_token': csrf_token,
721 'csrf_token': csrf_token,
720 'text': test_text,
722 'text': test_text,
721 'version': '0',
723 'version': '0',
722 },
724 },
723 status=404,
725 status=404,
724
726
725 )
727 )
726 assert response.status_int == 404
728 assert response.status_int == 404
727
729
728 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
730 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
729 pull_request = pr_util.create_pull_request()
731 pull_request = pr_util.create_pull_request()
730 target_scm = pull_request.target_repo.scm_instance()
732 target_scm = pull_request.target_repo.scm_instance()
731 target_scm_name = target_scm.name
733 target_scm_name = target_scm.name
732
734
733 response = self.app.post(
735 response = self.app.post(
734 route_path(
736 route_path(
735 'pullrequest_comment_create',
737 'pullrequest_comment_create',
736 repo_name=target_scm_name,
738 repo_name=target_scm_name,
737 pull_request_id=pull_request.pull_request_id),
739 pull_request_id=pull_request.pull_request_id),
738 params={
740 params={
739 'csrf_token': csrf_token,
741 'csrf_token': csrf_token,
740 'text': 'init',
742 'text': 'init',
741 },
743 },
742 extra_environ=xhr_header,
744 extra_environ=xhr_header,
743 )
745 )
744 assert response.json
746 assert response.json
745 for comment_id in response.json.keys():
747 for comment_id in response.json.keys():
746 test_text = 'test'
748 test_text = 'test'
747 self.app.post(
749 self.app.post(
748 route_path(
750 route_path(
749 'pullrequest_comment_edit',
751 'pullrequest_comment_edit',
750 repo_name=target_scm_name,
752 repo_name=target_scm_name,
751 pull_request_id=pull_request.pull_request_id,
753 pull_request_id=pull_request.pull_request_id,
752 comment_id=comment_id,
754 comment_id=comment_id,
753 ),
755 ),
754 extra_environ=xhr_header,
756 extra_environ=xhr_header,
755 params={
757 params={
756 'csrf_token': csrf_token,
758 'csrf_token': csrf_token,
757 'text': test_text,
759 'text': test_text,
758 'version': '0',
760 'version': '0',
759 },
761 },
760
762
761 )
763 )
762 test_text_v2 = 'test_v2'
764 test_text_v2 = 'test_v2'
763 response = self.app.post(
765 response = self.app.post(
764 route_path(
766 route_path(
765 'pullrequest_comment_edit',
767 'pullrequest_comment_edit',
766 repo_name=target_scm_name,
768 repo_name=target_scm_name,
767 pull_request_id=pull_request.pull_request_id,
769 pull_request_id=pull_request.pull_request_id,
768 comment_id=comment_id,
770 comment_id=comment_id,
769 ),
771 ),
770 extra_environ=xhr_header,
772 extra_environ=xhr_header,
771 params={
773 params={
772 'csrf_token': csrf_token,
774 'csrf_token': csrf_token,
773 'text': test_text_v2,
775 'text': test_text_v2,
774 'version': '0',
776 'version': '0',
775 },
777 },
776 status=409,
778 status=409,
777 )
779 )
778 assert response.status_int == 409
780 assert response.status_int == 409
779
781
780 text_form_db = ChangesetComment.query().filter(
782 text_form_db = ChangesetComment.query().filter(
781 ChangesetComment.comment_id == comment_id).first().text
783 ChangesetComment.comment_id == comment_id).first().text
782
784
783 assert test_text == text_form_db
785 assert test_text == text_form_db
784 assert test_text_v2 != text_form_db
786 assert test_text_v2 != text_form_db
785
787
786 def test_comment_and_comment_edit_permissions_forbidden(
788 def test_comment_and_comment_edit_permissions_forbidden(
787 self, autologin_regular_user, user_regular, user_admin, pr_util,
789 self, autologin_regular_user, user_regular, user_admin, pr_util,
788 csrf_token, xhr_header):
790 csrf_token, xhr_header):
789 pull_request = pr_util.create_pull_request(
791 pull_request = pr_util.create_pull_request(
790 author=user_admin.username, enable_notifications=False)
792 author=user_admin.username, enable_notifications=False)
791 comment = CommentsModel().create(
793 comment = CommentsModel().create(
792 text='test',
794 text='test',
793 repo=pull_request.target_repo.scm_instance().name,
795 repo=pull_request.target_repo.scm_instance().name,
794 user=user_admin,
796 user=user_admin,
795 pull_request=pull_request,
797 pull_request=pull_request,
796 )
798 )
797 response = self.app.post(
799 response = self.app.post(
798 route_path(
800 route_path(
799 'pullrequest_comment_edit',
801 'pullrequest_comment_edit',
800 repo_name=pull_request.target_repo.scm_instance().name,
802 repo_name=pull_request.target_repo.scm_instance().name,
801 pull_request_id=pull_request.pull_request_id,
803 pull_request_id=pull_request.pull_request_id,
802 comment_id=comment.comment_id,
804 comment_id=comment.comment_id,
803 ),
805 ),
804 extra_environ=xhr_header,
806 extra_environ=xhr_header,
805 params={
807 params={
806 'csrf_token': csrf_token,
808 'csrf_token': csrf_token,
807 'text': 'test_text',
809 'text': 'test_text',
808 },
810 },
809 status=403,
811 status=403,
810 )
812 )
811 assert response.status_int == 403
813 assert response.status_int == 403
812
814
813 def test_create_pull_request(self, backend, csrf_token):
815 def test_create_pull_request(self, backend, csrf_token):
814 commits = [
816 commits = [
815 {'message': 'ancestor'},
817 {'message': 'ancestor'},
816 {'message': 'change'},
818 {'message': 'change'},
817 {'message': 'change2'},
819 {'message': 'change2'},
818 ]
820 ]
819 commit_ids = backend.create_master_repo(commits)
821 commit_ids = backend.create_master_repo(commits)
820 target = backend.create_repo(heads=['ancestor'])
822 target = backend.create_repo(heads=['ancestor'])
821 source = backend.create_repo(heads=['change2'])
823 source = backend.create_repo(heads=['change2'])
822
824
823 response = self.app.post(
825 response = self.app.post(
824 route_path('pullrequest_create', repo_name=source.repo_name),
826 route_path('pullrequest_create', repo_name=source.repo_name),
825 [
827 [
826 ('source_repo', source.repo_name),
828 ('source_repo', source.repo_name),
827 ('source_ref', 'branch:default:' + commit_ids['change2']),
829 ('source_ref', 'branch:default:' + commit_ids['change2']),
828 ('target_repo', target.repo_name),
830 ('target_repo', target.repo_name),
829 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
831 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
830 ('common_ancestor', commit_ids['ancestor']),
832 ('common_ancestor', commit_ids['ancestor']),
831 ('pullrequest_title', 'Title'),
833 ('pullrequest_title', 'Title'),
832 ('pullrequest_desc', 'Description'),
834 ('pullrequest_desc', 'Description'),
833 ('description_renderer', 'markdown'),
835 ('description_renderer', 'markdown'),
834 ('__start__', 'review_members:sequence'),
836 ('__start__', 'review_members:sequence'),
835 ('__start__', 'reviewer:mapping'),
837 ('__start__', 'reviewer:mapping'),
836 ('user_id', '1'),
838 ('user_id', '1'),
837 ('__start__', 'reasons:sequence'),
839 ('__start__', 'reasons:sequence'),
838 ('reason', 'Some reason'),
840 ('reason', 'Some reason'),
839 ('__end__', 'reasons:sequence'),
841 ('__end__', 'reasons:sequence'),
840 ('__start__', 'rules:sequence'),
842 ('__start__', 'rules:sequence'),
841 ('__end__', 'rules:sequence'),
843 ('__end__', 'rules:sequence'),
842 ('mandatory', 'False'),
844 ('mandatory', 'False'),
843 ('__end__', 'reviewer:mapping'),
845 ('__end__', 'reviewer:mapping'),
844 ('__end__', 'review_members:sequence'),
846 ('__end__', 'review_members:sequence'),
845 ('__start__', 'revisions:sequence'),
847 ('__start__', 'revisions:sequence'),
846 ('revisions', commit_ids['change']),
848 ('revisions', commit_ids['change']),
847 ('revisions', commit_ids['change2']),
849 ('revisions', commit_ids['change2']),
848 ('__end__', 'revisions:sequence'),
850 ('__end__', 'revisions:sequence'),
849 ('user', ''),
851 ('user', ''),
850 ('csrf_token', csrf_token),
852 ('csrf_token', csrf_token),
851 ],
853 ],
852 status=302)
854 status=302)
853
855
854 location = response.headers['Location']
856 location = response.headers['Location']
855 pull_request_id = location.rsplit('/', 1)[1]
857 pull_request_id = location.rsplit('/', 1)[1]
856 assert pull_request_id != 'new'
858 assert pull_request_id != 'new'
857 pull_request = PullRequest.get(int(pull_request_id))
859 pull_request = PullRequest.get(int(pull_request_id))
858
860
859 # check that we have now both revisions
861 # check that we have now both revisions
860 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
862 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
861 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
863 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
862 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
864 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
863 assert pull_request.target_ref == expected_target_ref
865 assert pull_request.target_ref == expected_target_ref
864
866
865 def test_reviewer_notifications(self, backend, csrf_token):
867 def test_reviewer_notifications(self, backend, csrf_token):
866 # We have to use the app.post for this test so it will create the
868 # We have to use the app.post for this test so it will create the
867 # notifications properly with the new PR
869 # notifications properly with the new PR
868 commits = [
870 commits = [
869 {'message': 'ancestor',
871 {'message': 'ancestor',
870 'added': [FileNode('file_A', content='content_of_ancestor')]},
872 'added': [FileNode(b'file_A', content=b'content_of_ancestor')]},
871 {'message': 'change',
873 {'message': 'change',
872 'added': [FileNode('file_a', content='content_of_change')]},
874 'added': [FileNode(b'file_a', content=b'content_of_change')]},
873 {'message': 'change-child'},
875 {'message': 'change-child'},
874 {'message': 'ancestor-child', 'parents': ['ancestor'],
876 {'message': 'ancestor-child', 'parents': ['ancestor'],
875 'added': [
877 'added': [ FileNode(b'file_B', content=b'content_of_ancestor_child')]},
876 FileNode('file_B', content='content_of_ancestor_child')]},
877 {'message': 'ancestor-child-2'},
878 {'message': 'ancestor-child-2'},
878 ]
879 ]
879 commit_ids = backend.create_master_repo(commits)
880 commit_ids = backend.create_master_repo(commits)
880 target = backend.create_repo(heads=['ancestor-child'])
881 target = backend.create_repo(heads=['ancestor-child'])
881 source = backend.create_repo(heads=['change'])
882 source = backend.create_repo(heads=['change'])
882
883
883 response = self.app.post(
884 response = self.app.post(
884 route_path('pullrequest_create', repo_name=source.repo_name),
885 route_path('pullrequest_create', repo_name=source.repo_name),
885 [
886 [
886 ('source_repo', source.repo_name),
887 ('source_repo', source.repo_name),
887 ('source_ref', 'branch:default:' + commit_ids['change']),
888 ('source_ref', 'branch:default:' + commit_ids['change']),
888 ('target_repo', target.repo_name),
889 ('target_repo', target.repo_name),
889 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
890 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
890 ('common_ancestor', commit_ids['ancestor']),
891 ('common_ancestor', commit_ids['ancestor']),
891 ('pullrequest_title', 'Title'),
892 ('pullrequest_title', 'Title'),
892 ('pullrequest_desc', 'Description'),
893 ('pullrequest_desc', 'Description'),
893 ('description_renderer', 'markdown'),
894 ('description_renderer', 'markdown'),
894 ('__start__', 'review_members:sequence'),
895 ('__start__', 'review_members:sequence'),
895 ('__start__', 'reviewer:mapping'),
896 ('__start__', 'reviewer:mapping'),
896 ('user_id', '2'),
897 ('user_id', '2'),
897 ('__start__', 'reasons:sequence'),
898 ('__start__', 'reasons:sequence'),
898 ('reason', 'Some reason'),
899 ('reason', 'Some reason'),
899 ('__end__', 'reasons:sequence'),
900 ('__end__', 'reasons:sequence'),
900 ('__start__', 'rules:sequence'),
901 ('__start__', 'rules:sequence'),
901 ('__end__', 'rules:sequence'),
902 ('__end__', 'rules:sequence'),
902 ('mandatory', 'False'),
903 ('mandatory', 'False'),
903 ('__end__', 'reviewer:mapping'),
904 ('__end__', 'reviewer:mapping'),
904 ('__end__', 'review_members:sequence'),
905 ('__end__', 'review_members:sequence'),
905 ('__start__', 'revisions:sequence'),
906 ('__start__', 'revisions:sequence'),
906 ('revisions', commit_ids['change']),
907 ('revisions', commit_ids['change']),
907 ('__end__', 'revisions:sequence'),
908 ('__end__', 'revisions:sequence'),
908 ('user', ''),
909 ('user', ''),
909 ('csrf_token', csrf_token),
910 ('csrf_token', csrf_token),
910 ],
911 ],
911 status=302)
912 status=302)
912
913
913 location = response.headers['Location']
914 location = response.headers['Location']
914
915
915 pull_request_id = location.rsplit('/', 1)[1]
916 pull_request_id = location.rsplit('/', 1)[1]
916 assert pull_request_id != 'new'
917 assert pull_request_id != 'new'
917 pull_request = PullRequest.get(int(pull_request_id))
918 pull_request = PullRequest.get(int(pull_request_id))
918
919
919 # Check that a notification was made
920 # Check that a notification was made
920 notifications = Notification.query()\
921 notifications = Notification.query()\
921 .filter(Notification.created_by == pull_request.author.user_id,
922 .filter(Notification.created_by == pull_request.author.user_id,
922 Notification.type_ == Notification.TYPE_PULL_REQUEST,
923 Notification.type_ == Notification.TYPE_PULL_REQUEST,
923 Notification.subject.contains(
924 Notification.subject.contains(
924 "requested a pull request review. !%s" % pull_request_id))
925 "requested a pull request review. !%s" % pull_request_id))
925 assert len(notifications.all()) == 1
926 assert len(notifications.all()) == 1
926
927
927 # Change reviewers and check that a notification was made
928 # Change reviewers and check that a notification was made
928 PullRequestModel().update_reviewers(
929 PullRequestModel().update_reviewers(
929 pull_request.pull_request_id, [
930 pull_request.pull_request_id, [
930 (1, [], False, 'reviewer', [])
931 (1, [], False, 'reviewer', [])
931 ],
932 ],
932 pull_request.author)
933 pull_request.author)
933 assert len(notifications.all()) == 2
934 assert len(notifications.all()) == 2
934
935
935 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):
936 commits = [
937 commits = [
937 {'message': 'ancestor',
938 {'message': 'ancestor',
938 'added': [FileNode('file_A', content='content_of_ancestor')]},
939 'added': [FileNode(b'file_A', content=b'content_of_ancestor')]},
939 {'message': 'change',
940 {'message': 'change',
940 'added': [FileNode('file_a', content='content_of_change')]},
941 'added': [FileNode(b'file_a', content=b'content_of_change')]},
941 {'message': 'change-child'},
942 {'message': 'change-child'},
942 {'message': 'ancestor-child', 'parents': ['ancestor'],
943 {'message': 'ancestor-child', 'parents': ['ancestor'],
943 'added': [
944 'added': [
944 FileNode('file_B', content='content_of_ancestor_child')]},
945 FileNode(b'file_B', content=b'content_of_ancestor_child')]},
945 {'message': 'ancestor-child-2'},
946 {'message': 'ancestor-child-2'},
946 ]
947 ]
947 commit_ids = backend.create_master_repo(commits)
948 commit_ids = backend.create_master_repo(commits)
948 target = backend.create_repo(heads=['ancestor-child'])
949 target = backend.create_repo(heads=['ancestor-child'])
949 source = backend.create_repo(heads=['change'])
950 source = backend.create_repo(heads=['change'])
950
951
951 response = self.app.post(
952 response = self.app.post(
952 route_path('pullrequest_create', repo_name=source.repo_name),
953 route_path('pullrequest_create', repo_name=source.repo_name),
953 [
954 [
954 ('source_repo', source.repo_name),
955 ('source_repo', source.repo_name),
955 ('source_ref', 'branch:default:' + commit_ids['change']),
956 ('source_ref', 'branch:default:' + commit_ids['change']),
956 ('target_repo', target.repo_name),
957 ('target_repo', target.repo_name),
957 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
958 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
958 ('common_ancestor', commit_ids['ancestor']),
959 ('common_ancestor', commit_ids['ancestor']),
959 ('pullrequest_title', 'Title'),
960 ('pullrequest_title', 'Title'),
960 ('pullrequest_desc', 'Description'),
961 ('pullrequest_desc', 'Description'),
961 ('description_renderer', 'markdown'),
962 ('description_renderer', 'markdown'),
962 ('__start__', 'review_members:sequence'),
963 ('__start__', 'review_members:sequence'),
963 ('__start__', 'reviewer:mapping'),
964 ('__start__', 'reviewer:mapping'),
964 ('user_id', '1'),
965 ('user_id', '1'),
965 ('__start__', 'reasons:sequence'),
966 ('__start__', 'reasons:sequence'),
966 ('reason', 'Some reason'),
967 ('reason', 'Some reason'),
967 ('__end__', 'reasons:sequence'),
968 ('__end__', 'reasons:sequence'),
968 ('__start__', 'rules:sequence'),
969 ('__start__', 'rules:sequence'),
969 ('__end__', 'rules:sequence'),
970 ('__end__', 'rules:sequence'),
970 ('mandatory', 'False'),
971 ('mandatory', 'False'),
971 ('__end__', 'reviewer:mapping'),
972 ('__end__', 'reviewer:mapping'),
972 ('__end__', 'review_members:sequence'),
973 ('__end__', 'review_members:sequence'),
973 ('__start__', 'revisions:sequence'),
974 ('__start__', 'revisions:sequence'),
974 ('revisions', commit_ids['change']),
975 ('revisions', commit_ids['change']),
975 ('__end__', 'revisions:sequence'),
976 ('__end__', 'revisions:sequence'),
976 ('user', ''),
977 ('user', ''),
977 ('csrf_token', csrf_token),
978 ('csrf_token', csrf_token),
978 ],
979 ],
979 status=302)
980 status=302)
980
981
981 location = response.headers['Location']
982 location = response.headers['Location']
982
983
983 pull_request_id = location.rsplit('/', 1)[1]
984 pull_request_id = location.rsplit('/', 1)[1]
984 assert pull_request_id != 'new'
985 assert pull_request_id != 'new'
985 pull_request = PullRequest.get(int(pull_request_id))
986 pull_request = PullRequest.get(int(pull_request_id))
986
987
987 # 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
988 # show the correct diff
989 # show the correct diff
989 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
990 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
990 assert pull_request.target_ref == expected_target_ref
991 assert pull_request.target_ref == expected_target_ref
991
992
992 # Check generated diff contents
993 # Check generated diff contents
993 response = response.follow()
994 response = response.follow()
994 response.mustcontain(no=['content_of_ancestor'])
995 response.mustcontain(no=['content_of_ancestor'])
995 response.mustcontain(no=['content_of_ancestor-child'])
996 response.mustcontain(no=['content_of_ancestor-child'])
996 response.mustcontain('content_of_change')
997 response.mustcontain('content_of_change')
997
998
998 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
999 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
999 # Clear any previous calls to rcextensions
1000 # Clear any previous calls to rcextensions
1000 rhodecode.EXTENSIONS.calls.clear()
1001 rhodecode.EXTENSIONS.calls.clear()
1001
1002
1002 pull_request = pr_util.create_pull_request(
1003 pull_request = pr_util.create_pull_request(
1003 approved=True, mergeable=True)
1004 approved=True, mergeable=True)
1004 pull_request_id = pull_request.pull_request_id
1005 pull_request_id = pull_request.pull_request_id
1005 repo_name = pull_request.target_repo.scm_instance().name,
1006 repo_name = pull_request.target_repo.scm_instance().name,
1006
1007
1007 url = route_path('pullrequest_merge',
1008 url = route_path('pullrequest_merge',
1008 repo_name=str(repo_name[0]),
1009 repo_name=str(repo_name[0]),
1009 pull_request_id=pull_request_id)
1010 pull_request_id=pull_request_id)
1010 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
1011 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
1011
1012
1012 pull_request = PullRequest.get(pull_request_id)
1013 pull_request = PullRequest.get(pull_request_id)
1013
1014
1014 assert response.status_int == 200
1015 assert response.status_int == 200
1015 assert pull_request.is_closed()
1016 assert pull_request.is_closed()
1016 assert_pull_request_status(
1017 assert_pull_request_status(
1017 pull_request, ChangesetStatus.STATUS_APPROVED)
1018 pull_request, ChangesetStatus.STATUS_APPROVED)
1018
1019
1019 # Check the relevant log entries were added
1020 # Check the relevant log entries were added
1020 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)
1021 actions = [log.action for log in user_logs]
1022 actions = [log.action for log in user_logs]
1022 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1023 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1023 expected_actions = [
1024 expected_actions = [
1024 u'repo.pull_request.close',
1025 'repo.pull_request.close',
1025 u'repo.pull_request.merge',
1026 'repo.pull_request.merge',
1026 u'repo.pull_request.comment.create'
1027 'repo.pull_request.comment.create'
1027 ]
1028 ]
1028 assert actions == expected_actions
1029 assert actions == expected_actions
1029
1030
1030 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)
1031 actions = [log for log in user_logs]
1032 actions = [log for log in user_logs]
1032 assert actions[-1].action == 'user.push'
1033 assert actions[-1].action == 'user.push'
1033 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1034 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1034
1035
1035 # Check post_push rcextension was really executed
1036 # Check post_push rcextension was really executed
1036 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1037 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1037 assert len(push_calls) == 1
1038 assert len(push_calls) == 1
1038 unused_last_call_args, last_call_kwargs = push_calls[0]
1039 unused_last_call_args, last_call_kwargs = push_calls[0]
1039 assert last_call_kwargs['action'] == 'push'
1040 assert last_call_kwargs['action'] == 'push'
1040 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1041 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1041
1042
1042 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1043 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1043 pull_request = pr_util.create_pull_request(mergeable=False)
1044 pull_request = pr_util.create_pull_request(mergeable=False)
1044 pull_request_id = pull_request.pull_request_id
1045 pull_request_id = pull_request.pull_request_id
1045 pull_request = PullRequest.get(pull_request_id)
1046 pull_request = PullRequest.get(pull_request_id)
1046
1047
1047 response = self.app.post(
1048 response = self.app.post(
1048 route_path('pullrequest_merge',
1049 route_path('pullrequest_merge',
1049 repo_name=pull_request.target_repo.scm_instance().name,
1050 repo_name=pull_request.target_repo.scm_instance().name,
1050 pull_request_id=pull_request.pull_request_id),
1051 pull_request_id=pull_request.pull_request_id),
1051 params={'csrf_token': csrf_token}).follow()
1052 params={'csrf_token': csrf_token}).follow()
1052
1053
1053 assert response.status_int == 200
1054 assert response.status_int == 200
1054 response.mustcontain(
1055 response.mustcontain(
1055 'Merge is not currently possible because of below failed checks.')
1056 'Merge is not currently possible because of below failed checks.')
1056 response.mustcontain('Server-side pull request merging is disabled.')
1057 response.mustcontain('Server-side pull request merging is disabled.')
1057
1058
1058 @pytest.mark.skip_backends('svn')
1059 @pytest.mark.skip_backends('svn')
1059 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):
1060 pull_request = pr_util.create_pull_request(mergeable=True)
1061 pull_request = pr_util.create_pull_request(mergeable=True)
1061 pull_request_id = pull_request.pull_request_id
1062 pull_request_id = pull_request.pull_request_id
1062 repo_name = pull_request.target_repo.scm_instance().name
1063 repo_name = pull_request.target_repo.scm_instance().name
1063
1064
1064 response = self.app.post(
1065 response = self.app.post(
1065 route_path('pullrequest_merge',
1066 route_path('pullrequest_merge',
1066 repo_name=repo_name, pull_request_id=pull_request_id),
1067 repo_name=repo_name, pull_request_id=pull_request_id),
1067 params={'csrf_token': csrf_token}).follow()
1068 params={'csrf_token': csrf_token}).follow()
1068
1069
1069 assert response.status_int == 200
1070 assert response.status_int == 200
1070
1071
1071 response.mustcontain(
1072 response.mustcontain(
1072 'Merge is not currently possible because of below failed checks.')
1073 'Merge is not currently possible because of below failed checks.')
1073 response.mustcontain('Pull request reviewer approval is pending.')
1074 response.mustcontain('Pull request reviewer approval is pending.')
1074
1075
1075 def test_merge_pull_request_renders_failure_reason(
1076 def test_merge_pull_request_renders_failure_reason(
1076 self, user_regular, csrf_token, pr_util):
1077 self, user_regular, csrf_token, pr_util):
1077 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1078 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1078 pull_request_id = pull_request.pull_request_id
1079 pull_request_id = pull_request.pull_request_id
1079 repo_name = pull_request.target_repo.scm_instance().name
1080 repo_name = pull_request.target_repo.scm_instance().name
1080
1081
1081 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1082 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1082 MergeFailureReason.PUSH_FAILED,
1083 MergeFailureReason.PUSH_FAILED,
1083 metadata={'target': 'shadow repo',
1084 metadata={'target': 'shadow repo',
1084 'merge_commit': 'xxx'})
1085 'merge_commit': 'xxx'})
1085 model_patcher = mock.patch.multiple(
1086 model_patcher = mock.patch.multiple(
1086 PullRequestModel,
1087 PullRequestModel,
1087 merge_repo=mock.Mock(return_value=merge_resp),
1088 merge_repo=mock.Mock(return_value=merge_resp),
1088 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1089 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1089
1090
1090 with model_patcher:
1091 with model_patcher:
1091 response = self.app.post(
1092 response = self.app.post(
1092 route_path('pullrequest_merge',
1093 route_path('pullrequest_merge',
1093 repo_name=repo_name,
1094 repo_name=repo_name,
1094 pull_request_id=pull_request_id),
1095 pull_request_id=pull_request_id),
1095 params={'csrf_token': csrf_token}, status=302)
1096 params={'csrf_token': csrf_token}, status=302)
1096
1097
1097 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1098 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1098 metadata={'target': 'shadow repo',
1099 metadata={'target': 'shadow repo',
1099 'merge_commit': 'xxx'})
1100 'merge_commit': 'xxx'})
1100 assert_session_flash(response, merge_resp.merge_status_message)
1101 assert_session_flash(response, merge_resp.merge_status_message)
1101
1102
1102 def test_update_source_revision(self, backend, csrf_token):
1103 def test_update_source_revision(self, backend, csrf_token):
1103 commits = [
1104 commits = [
1104 {'message': 'ancestor'},
1105 {'message': 'ancestor'},
1105 {'message': 'change'},
1106 {'message': 'change'},
1106 {'message': 'change-2'},
1107 {'message': 'change-2'},
1107 ]
1108 ]
1108 commit_ids = backend.create_master_repo(commits)
1109 commit_ids = backend.create_master_repo(commits)
1109 target = backend.create_repo(heads=['ancestor'])
1110 target = backend.create_repo(heads=['ancestor'])
1110 source = backend.create_repo(heads=['change'])
1111 source = backend.create_repo(heads=['change'])
1111
1112
1112 # create pr from a in source to A in target
1113 # create pr from a in source to A in target
1113 pull_request = PullRequest()
1114 pull_request = PullRequest()
1114
1115
1115 pull_request.source_repo = source
1116 pull_request.source_repo = source
1116 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1117 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1117 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1118 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1118
1119
1119 pull_request.target_repo = target
1120 pull_request.target_repo = target
1120 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1121 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1121 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1122 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1122
1123
1123 pull_request.revisions = [commit_ids['change']]
1124 pull_request.revisions = [commit_ids['change']]
1124 pull_request.title = u"Test"
1125 pull_request.title = "Test"
1125 pull_request.description = u"Description"
1126 pull_request.description = "Description"
1126 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1127 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1127 pull_request.pull_request_state = PullRequest.STATE_CREATED
1128 pull_request.pull_request_state = PullRequest.STATE_CREATED
1128 Session().add(pull_request)
1129 Session().add(pull_request)
1129 Session().commit()
1130 Session().commit()
1130 pull_request_id = pull_request.pull_request_id
1131 pull_request_id = pull_request.pull_request_id
1131
1132
1132 # source has ancestor - change - change-2
1133 # source has ancestor - change - change-2
1133 backend.pull_heads(source, heads=['change-2'])
1134 backend.pull_heads(source, heads=['change-2'])
1134 target_repo_name = target.repo_name
1135 target_repo_name = target.repo_name
1135
1136
1136 # update PR
1137 # update PR
1137 self.app.post(
1138 self.app.post(
1138 route_path('pullrequest_update',
1139 route_path('pullrequest_update',
1139 repo_name=target_repo_name, pull_request_id=pull_request_id),
1140 repo_name=target_repo_name, pull_request_id=pull_request_id),
1140 params={'update_commits': 'true', 'csrf_token': csrf_token})
1141 params={'update_commits': 'true', 'csrf_token': csrf_token})
1141
1142
1142 response = self.app.get(
1143 response = self.app.get(
1143 route_path('pullrequest_show',
1144 route_path('pullrequest_show',
1144 repo_name=target_repo_name,
1145 repo_name=target_repo_name,
1145 pull_request_id=pull_request.pull_request_id))
1146 pull_request_id=pull_request.pull_request_id))
1146
1147
1147 assert response.status_int == 200
1148 assert response.status_int == 200
1148 response.mustcontain('Pull request updated to')
1149 response.mustcontain('Pull request updated to')
1149 response.mustcontain('with 1 added, 0 removed commits.')
1150 response.mustcontain('with 1 added, 0 removed commits.')
1150
1151
1151 # check that we have now both revisions
1152 # check that we have now both revisions
1152 pull_request = PullRequest.get(pull_request_id)
1153 pull_request = PullRequest.get(pull_request_id)
1153 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1154 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1154
1155
1155 def test_update_target_revision(self, backend, csrf_token):
1156 def test_update_target_revision(self, backend, csrf_token):
1156 commits = [
1157 commits = [
1157 {'message': 'ancestor'},
1158 {'message': 'ancestor'},
1158 {'message': 'change'},
1159 {'message': 'change'},
1159 {'message': 'ancestor-new', 'parents': ['ancestor']},
1160 {'message': 'ancestor-new', 'parents': ['ancestor']},
1160 {'message': 'change-rebased'},
1161 {'message': 'change-rebased'},
1161 ]
1162 ]
1162 commit_ids = backend.create_master_repo(commits)
1163 commit_ids = backend.create_master_repo(commits)
1163 target = backend.create_repo(heads=['ancestor'])
1164 target = backend.create_repo(heads=['ancestor'])
1164 source = backend.create_repo(heads=['change'])
1165 source = backend.create_repo(heads=['change'])
1165
1166
1166 # create pr from a in source to A in target
1167 # create pr from a in source to A in target
1167 pull_request = PullRequest()
1168 pull_request = PullRequest()
1168
1169
1169 pull_request.source_repo = source
1170 pull_request.source_repo = source
1170 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1171 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1171 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1172 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1172
1173
1173 pull_request.target_repo = target
1174 pull_request.target_repo = target
1174 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1175 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1175 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1176 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1176
1177
1177 pull_request.revisions = [commit_ids['change']]
1178 pull_request.revisions = [commit_ids['change']]
1178 pull_request.title = u"Test"
1179 pull_request.title = "Test"
1179 pull_request.description = u"Description"
1180 pull_request.description = "Description"
1180 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1181 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1181 pull_request.pull_request_state = PullRequest.STATE_CREATED
1182 pull_request.pull_request_state = PullRequest.STATE_CREATED
1182
1183
1183 Session().add(pull_request)
1184 Session().add(pull_request)
1184 Session().commit()
1185 Session().commit()
1185 pull_request_id = pull_request.pull_request_id
1186 pull_request_id = pull_request.pull_request_id
1186
1187
1187 # target has ancestor - ancestor-new
1188 # target has ancestor - ancestor-new
1188 # source has ancestor - ancestor-new - change-rebased
1189 # source has ancestor - ancestor-new - change-rebased
1189 backend.pull_heads(target, heads=['ancestor-new'])
1190 backend.pull_heads(target, heads=['ancestor-new'])
1190 backend.pull_heads(source, heads=['change-rebased'])
1191 backend.pull_heads(source, heads=['change-rebased'])
1191 target_repo_name = target.repo_name
1192 target_repo_name = target.repo_name
1192
1193
1193 # update PR
1194 # update PR
1194 url = route_path('pullrequest_update',
1195 url = route_path('pullrequest_update',
1195 repo_name=target_repo_name,
1196 repo_name=target_repo_name,
1196 pull_request_id=pull_request_id)
1197 pull_request_id=pull_request_id)
1197 self.app.post(url,
1198 self.app.post(url,
1198 params={'update_commits': 'true', 'csrf_token': csrf_token},
1199 params={'update_commits': 'true', 'csrf_token': csrf_token},
1199 status=200)
1200 status=200)
1200
1201
1201 # check that we have now both revisions
1202 # check that we have now both revisions
1202 pull_request = PullRequest.get(pull_request_id)
1203 pull_request = PullRequest.get(pull_request_id)
1203 assert pull_request.revisions == [commit_ids['change-rebased']]
1204 assert pull_request.revisions == [commit_ids['change-rebased']]
1204 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1205 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1205 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1206 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1206
1207
1207 response = self.app.get(
1208 response = self.app.get(
1208 route_path('pullrequest_show',
1209 route_path('pullrequest_show',
1209 repo_name=target_repo_name,
1210 repo_name=target_repo_name,
1210 pull_request_id=pull_request.pull_request_id))
1211 pull_request_id=pull_request.pull_request_id))
1211 assert response.status_int == 200
1212 assert response.status_int == 200
1212 response.mustcontain('Pull request updated to')
1213 response.mustcontain('Pull request updated to')
1213 response.mustcontain('with 1 added, 1 removed commits.')
1214 response.mustcontain('with 1 added, 1 removed commits.')
1214
1215
1215 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):
1216 backend = backend_git
1217 backend = backend_git
1217 commits = [
1218 commits = [
1218 {'message': 'master-commit-1'},
1219 {'message': 'master-commit-1'},
1219 {'message': 'master-commit-2-change-1'},
1220 {'message': 'master-commit-2-change-1'},
1220 {'message': 'master-commit-3-change-2'},
1221 {'message': 'master-commit-3-change-2'},
1221
1222
1222 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1223 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1223 {'message': 'feat-commit-2'},
1224 {'message': 'feat-commit-2'},
1224 ]
1225 ]
1225 commit_ids = backend.create_master_repo(commits)
1226 commit_ids = backend.create_master_repo(commits)
1226 target = backend.create_repo(heads=['master-commit-3-change-2'])
1227 target = backend.create_repo(heads=['master-commit-3-change-2'])
1227 source = backend.create_repo(heads=['feat-commit-2'])
1228 source = backend.create_repo(heads=['feat-commit-2'])
1228
1229
1229 # create pr from a in source to A in target
1230 # create pr from a in source to A in target
1230 pull_request = PullRequest()
1231 pull_request = PullRequest()
1231 pull_request.source_repo = source
1232 pull_request.source_repo = source
1232
1233
1233 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1234 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1234 branch=backend.default_branch_name,
1235 branch=backend.default_branch_name,
1235 commit_id=commit_ids['master-commit-3-change-2'])
1236 commit_id=commit_ids['master-commit-3-change-2'])
1236
1237
1237 pull_request.target_repo = target
1238 pull_request.target_repo = target
1238 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1239 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1239 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'])
1240
1241
1241 pull_request.revisions = [
1242 pull_request.revisions = [
1242 commit_ids['feat-commit-1'],
1243 commit_ids['feat-commit-1'],
1243 commit_ids['feat-commit-2']
1244 commit_ids['feat-commit-2']
1244 ]
1245 ]
1245 pull_request.title = u"Test"
1246 pull_request.title = "Test"
1246 pull_request.description = u"Description"
1247 pull_request.description = "Description"
1247 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1248 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1248 pull_request.pull_request_state = PullRequest.STATE_CREATED
1249 pull_request.pull_request_state = PullRequest.STATE_CREATED
1249 Session().add(pull_request)
1250 Session().add(pull_request)
1250 Session().commit()
1251 Session().commit()
1251 pull_request_id = pull_request.pull_request_id
1252 pull_request_id = pull_request.pull_request_id
1252
1253
1253 # PR is created, now we simulate a force-push into target,
1254 # PR is created, now we simulate a force-push into target,
1254 # that drops a 2 last commits
1255 # that drops a 2 last commits
1255 vcsrepo = target.scm_instance()
1256 vcsrepo = target.scm_instance()
1256 vcsrepo.config.clear_section('hooks')
1257 vcsrepo.config.clear_section('hooks')
1257 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1258 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1258 target_repo_name = target.repo_name
1259 target_repo_name = target.repo_name
1259
1260
1260 # update PR
1261 # update PR
1261 url = route_path('pullrequest_update',
1262 url = route_path('pullrequest_update',
1262 repo_name=target_repo_name,
1263 repo_name=target_repo_name,
1263 pull_request_id=pull_request_id)
1264 pull_request_id=pull_request_id)
1264 self.app.post(url,
1265 self.app.post(url,
1265 params={'update_commits': 'true', 'csrf_token': csrf_token},
1266 params={'update_commits': 'true', 'csrf_token': csrf_token},
1266 status=200)
1267 status=200)
1267
1268
1268 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))
1269 assert response.status_int == 200
1270 assert response.status_int == 200
1270 response.mustcontain('Pull request updated to')
1271 response.mustcontain('Pull request updated to')
1271 response.mustcontain('with 0 added, 0 removed commits.')
1272 response.mustcontain('with 0 added, 0 removed commits.')
1272
1273
1273 def test_update_of_ancestor_reference(self, backend, csrf_token):
1274 def test_update_of_ancestor_reference(self, backend, csrf_token):
1274 commits = [
1275 commits = [
1275 {'message': 'ancestor'},
1276 {'message': 'ancestor'},
1276 {'message': 'change'},
1277 {'message': 'change'},
1277 {'message': 'change-2'},
1278 {'message': 'change-2'},
1278 {'message': 'ancestor-new', 'parents': ['ancestor']},
1279 {'message': 'ancestor-new', 'parents': ['ancestor']},
1279 {'message': 'change-rebased'},
1280 {'message': 'change-rebased'},
1280 ]
1281 ]
1281 commit_ids = backend.create_master_repo(commits)
1282 commit_ids = backend.create_master_repo(commits)
1282 target = backend.create_repo(heads=['ancestor'])
1283 target = backend.create_repo(heads=['ancestor'])
1283 source = backend.create_repo(heads=['change'])
1284 source = backend.create_repo(heads=['change'])
1284
1285
1285 # create pr from a in source to A in target
1286 # create pr from a in source to A in target
1286 pull_request = PullRequest()
1287 pull_request = PullRequest()
1287 pull_request.source_repo = source
1288 pull_request.source_repo = source
1288
1289
1289 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1290 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1290 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1291 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1291 pull_request.target_repo = target
1292 pull_request.target_repo = target
1292 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1293 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1293 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1294 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1294 pull_request.revisions = [commit_ids['change']]
1295 pull_request.revisions = [commit_ids['change']]
1295 pull_request.title = u"Test"
1296 pull_request.title = "Test"
1296 pull_request.description = u"Description"
1297 pull_request.description = "Description"
1297 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1298 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1298 pull_request.pull_request_state = PullRequest.STATE_CREATED
1299 pull_request.pull_request_state = PullRequest.STATE_CREATED
1299 Session().add(pull_request)
1300 Session().add(pull_request)
1300 Session().commit()
1301 Session().commit()
1301 pull_request_id = pull_request.pull_request_id
1302 pull_request_id = pull_request.pull_request_id
1302
1303
1303 # target has ancestor - ancestor-new
1304 # target has ancestor - ancestor-new
1304 # source has ancestor - ancestor-new - change-rebased
1305 # source has ancestor - ancestor-new - change-rebased
1305 backend.pull_heads(target, heads=['ancestor-new'])
1306 backend.pull_heads(target, heads=['ancestor-new'])
1306 backend.pull_heads(source, heads=['change-rebased'])
1307 backend.pull_heads(source, heads=['change-rebased'])
1307 target_repo_name = target.repo_name
1308 target_repo_name = target.repo_name
1308
1309
1309 # update PR
1310 # update PR
1310 self.app.post(
1311 self.app.post(
1311 route_path('pullrequest_update',
1312 route_path('pullrequest_update',
1312 repo_name=target_repo_name, pull_request_id=pull_request_id),
1313 repo_name=target_repo_name, pull_request_id=pull_request_id),
1313 params={'update_commits': 'true', 'csrf_token': csrf_token},
1314 params={'update_commits': 'true', 'csrf_token': csrf_token},
1314 status=200)
1315 status=200)
1315
1316
1316 # Expect the target reference to be updated correctly
1317 # Expect the target reference to be updated correctly
1317 pull_request = PullRequest.get(pull_request_id)
1318 pull_request = PullRequest.get(pull_request_id)
1318 assert pull_request.revisions == [commit_ids['change-rebased']]
1319 assert pull_request.revisions == [commit_ids['change-rebased']]
1319 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1320 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1320 branch=backend.default_branch_name,
1321 branch=backend.default_branch_name,
1321 commit_id=commit_ids['ancestor-new'])
1322 commit_id=commit_ids['ancestor-new'])
1322 assert pull_request.target_ref == expected_target_ref
1323 assert pull_request.target_ref == expected_target_ref
1323
1324
1324 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1325 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1325 branch_name = 'development'
1326 branch_name = 'development'
1326 commits = [
1327 commits = [
1327 {'message': 'initial-commit'},
1328 {'message': 'initial-commit'},
1328 {'message': 'old-feature'},
1329 {'message': 'old-feature'},
1329 {'message': 'new-feature', 'branch': branch_name},
1330 {'message': 'new-feature', 'branch': branch_name},
1330 ]
1331 ]
1331 repo = backend_git.create_repo(commits)
1332 repo = backend_git.create_repo(commits)
1332 repo_name = repo.repo_name
1333 repo_name = repo.repo_name
1333 commit_ids = backend_git.commit_ids
1334 commit_ids = backend_git.commit_ids
1334
1335
1335 pull_request = PullRequest()
1336 pull_request = PullRequest()
1336 pull_request.source_repo = repo
1337 pull_request.source_repo = repo
1337 pull_request.target_repo = repo
1338 pull_request.target_repo = repo
1338 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1339 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1339 branch=branch_name, commit_id=commit_ids['new-feature'])
1340 branch=branch_name, commit_id=commit_ids['new-feature'])
1340 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1341 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1341 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'])
1342 pull_request.revisions = [commit_ids['new-feature']]
1343 pull_request.revisions = [commit_ids['new-feature']]
1343 pull_request.title = u"Test"
1344 pull_request.title = "Test"
1344 pull_request.description = u"Description"
1345 pull_request.description = "Description"
1345 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1346 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1346 pull_request.pull_request_state = PullRequest.STATE_CREATED
1347 pull_request.pull_request_state = PullRequest.STATE_CREATED
1347 Session().add(pull_request)
1348 Session().add(pull_request)
1348 Session().commit()
1349 Session().commit()
1349
1350
1350 pull_request_id = pull_request.pull_request_id
1351 pull_request_id = pull_request.pull_request_id
1351
1352
1352 vcs = repo.scm_instance()
1353 vcs = repo.scm_instance()
1353 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1354 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1354 # NOTE(marcink): run GC to ensure the commits are gone
1355 # NOTE(marcink): run GC to ensure the commits are gone
1355 vcs.run_gc()
1356 vcs.run_gc()
1356
1357
1357 response = self.app.get(route_path(
1358 response = self.app.get(route_path(
1358 'pullrequest_show',
1359 'pullrequest_show',
1359 repo_name=repo_name,
1360 repo_name=repo_name,
1360 pull_request_id=pull_request_id))
1361 pull_request_id=pull_request_id))
1361
1362
1362 assert response.status_int == 200
1363 assert response.status_int == 200
1363
1364
1364 response.assert_response().element_contains(
1365 response.assert_response().element_contains(
1365 '#changeset_compare_view_content .alert strong',
1366 '#changeset_compare_view_content .alert strong',
1366 'Missing commits')
1367 'Missing commits')
1367 response.assert_response().element_contains(
1368 response.assert_response().element_contains(
1368 '#changeset_compare_view_content .alert',
1369 '#changeset_compare_view_content .alert',
1369 'This pull request cannot be displayed, because one or more'
1370 'This pull request cannot be displayed, because one or more'
1370 ' commits no longer exist in the source repository.')
1371 ' commits no longer exist in the source repository.')
1371
1372
1372 def test_strip_commits_from_pull_request(
1373 def test_strip_commits_from_pull_request(
1373 self, backend, pr_util, csrf_token):
1374 self, backend, pr_util, csrf_token):
1374 commits = [
1375 commits = [
1375 {'message': 'initial-commit'},
1376 {'message': 'initial-commit'},
1376 {'message': 'old-feature'},
1377 {'message': 'old-feature'},
1377 {'message': 'new-feature', 'parents': ['initial-commit']},
1378 {'message': 'new-feature', 'parents': ['initial-commit']},
1378 ]
1379 ]
1379 pull_request = pr_util.create_pull_request(
1380 pull_request = pr_util.create_pull_request(
1380 commits, target_head='initial-commit', source_head='new-feature',
1381 commits, target_head='initial-commit', source_head='new-feature',
1381 revisions=['new-feature'])
1382 revisions=['new-feature'])
1382
1383
1383 vcs = pr_util.source_repository.scm_instance()
1384 vcs = pr_util.source_repository.scm_instance()
1384 if backend.alias == 'git':
1385 if backend.alias == 'git':
1385 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1386 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1386 else:
1387 else:
1387 vcs.strip(pr_util.commit_ids['new-feature'])
1388 vcs.strip(pr_util.commit_ids['new-feature'])
1388
1389
1389 response = self.app.get(route_path(
1390 response = self.app.get(route_path(
1390 'pullrequest_show',
1391 'pullrequest_show',
1391 repo_name=pr_util.target_repository.repo_name,
1392 repo_name=pr_util.target_repository.repo_name,
1392 pull_request_id=pull_request.pull_request_id))
1393 pull_request_id=pull_request.pull_request_id))
1393
1394
1394 assert response.status_int == 200
1395 assert response.status_int == 200
1395
1396
1396 response.assert_response().element_contains(
1397 response.assert_response().element_contains(
1397 '#changeset_compare_view_content .alert strong',
1398 '#changeset_compare_view_content .alert strong',
1398 'Missing commits')
1399 'Missing commits')
1399 response.assert_response().element_contains(
1400 response.assert_response().element_contains(
1400 '#changeset_compare_view_content .alert',
1401 '#changeset_compare_view_content .alert',
1401 'This pull request cannot be displayed, because one or more'
1402 'This pull request cannot be displayed, because one or more'
1402 ' commits no longer exist in the source repository.')
1403 ' commits no longer exist in the source repository.')
1403 response.assert_response().element_contains(
1404 response.assert_response().element_contains(
1404 '#update_commits',
1405 '#update_commits',
1405 'Update commits')
1406 'Update commits')
1406
1407
1407 def test_strip_commits_and_update(
1408 def test_strip_commits_and_update(
1408 self, backend, pr_util, csrf_token):
1409 self, backend, pr_util, csrf_token):
1409 commits = [
1410 commits = [
1410 {'message': 'initial-commit'},
1411 {'message': 'initial-commit'},
1411 {'message': 'old-feature'},
1412 {'message': 'old-feature'},
1412 {'message': 'new-feature', 'parents': ['old-feature']},
1413 {'message': 'new-feature', 'parents': ['old-feature']},
1413 ]
1414 ]
1414 pull_request = pr_util.create_pull_request(
1415 pull_request = pr_util.create_pull_request(
1415 commits, target_head='old-feature', source_head='new-feature',
1416 commits, target_head='old-feature', source_head='new-feature',
1416 revisions=['new-feature'], mergeable=True)
1417 revisions=['new-feature'], mergeable=True)
1417 pr_id = pull_request.pull_request_id
1418 pr_id = pull_request.pull_request_id
1418 target_repo_name = pull_request.target_repo.repo_name
1419 target_repo_name = pull_request.target_repo.repo_name
1419
1420
1420 vcs = pr_util.source_repository.scm_instance()
1421 vcs = pr_util.source_repository.scm_instance()
1421 if backend.alias == 'git':
1422 if backend.alias == 'git':
1422 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1423 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1423 else:
1424 else:
1424 vcs.strip(pr_util.commit_ids['new-feature'])
1425 vcs.strip(pr_util.commit_ids['new-feature'])
1425
1426
1426 url = route_path('pullrequest_update',
1427 url = route_path('pullrequest_update',
1427 repo_name=target_repo_name,
1428 repo_name=target_repo_name,
1428 pull_request_id=pr_id)
1429 pull_request_id=pr_id)
1429 response = self.app.post(url,
1430 response = self.app.post(url,
1430 params={'update_commits': 'true',
1431 params={'update_commits': 'true',
1431 'csrf_token': csrf_token})
1432 'csrf_token': csrf_token})
1432
1433
1433 assert response.status_int == 200
1434 assert response.status_int == 200
1434 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}')
1435
1436
1436 # Make sure that after update, it won't raise 500 errors
1437 # Make sure that after update, it won't raise 500 errors
1437 response = self.app.get(route_path(
1438 response = self.app.get(route_path(
1438 'pullrequest_show',
1439 'pullrequest_show',
1439 repo_name=target_repo_name,
1440 repo_name=target_repo_name,
1440 pull_request_id=pr_id))
1441 pull_request_id=pr_id))
1441
1442
1442 assert response.status_int == 200
1443 assert response.status_int == 200
1443 response.assert_response().element_contains(
1444 response.assert_response().element_contains(
1444 '#changeset_compare_view_content .alert strong',
1445 '#changeset_compare_view_content .alert strong',
1445 'Missing commits')
1446 'Missing commits')
1446
1447
1447 def test_branch_is_a_link(self, pr_util):
1448 def test_branch_is_a_link(self, pr_util):
1448 pull_request = pr_util.create_pull_request()
1449 pull_request = pr_util.create_pull_request()
1449 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1450 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1450 pull_request.target_ref = 'branch:target:abcdef1234567890'
1451 pull_request.target_ref = 'branch:target:abcdef1234567890'
1451 Session().add(pull_request)
1452 Session().add(pull_request)
1452 Session().commit()
1453 Session().commit()
1453
1454
1454 response = self.app.get(route_path(
1455 response = self.app.get(route_path(
1455 'pullrequest_show',
1456 'pullrequest_show',
1456 repo_name=pull_request.target_repo.scm_instance().name,
1457 repo_name=pull_request.target_repo.scm_instance().name,
1457 pull_request_id=pull_request.pull_request_id))
1458 pull_request_id=pull_request.pull_request_id))
1458 assert response.status_int == 200
1459 assert response.status_int == 200
1459
1460
1460 source = response.assert_response().get_element('.pr-source-info')
1461 source = response.assert_response().get_element('.pr-source-info')
1461 source_parent = source.getparent()
1462 source_parent = source.getparent()
1462 assert len(source_parent) == 1
1463 assert len(source_parent) == 1
1463
1464
1464 target = response.assert_response().get_element('.pr-target-info')
1465 target = response.assert_response().get_element('.pr-target-info')
1465 target_parent = target.getparent()
1466 target_parent = target.getparent()
1466 assert len(target_parent) == 1
1467 assert len(target_parent) == 1
1467
1468
1468 expected_origin_link = route_path(
1469 expected_origin_link = route_path(
1469 'repo_commits',
1470 'repo_commits',
1470 repo_name=pull_request.source_repo.scm_instance().name,
1471 repo_name=pull_request.source_repo.scm_instance().name,
1471 params=dict(branch='origin'))
1472 params=dict(branch='origin'))
1472 expected_target_link = route_path(
1473 expected_target_link = route_path(
1473 'repo_commits',
1474 'repo_commits',
1474 repo_name=pull_request.target_repo.scm_instance().name,
1475 repo_name=pull_request.target_repo.scm_instance().name,
1475 params=dict(branch='target'))
1476 params=dict(branch='target'))
1476 assert source_parent.attrib['href'] == expected_origin_link
1477 assert source_parent.attrib['href'] == expected_origin_link
1477 assert target_parent.attrib['href'] == expected_target_link
1478 assert target_parent.attrib['href'] == expected_target_link
1478
1479
1479 def test_bookmark_is_not_a_link(self, pr_util):
1480 def test_bookmark_is_not_a_link(self, pr_util):
1480 pull_request = pr_util.create_pull_request()
1481 pull_request = pr_util.create_pull_request()
1481 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1482 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1482 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1483 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1483 Session().add(pull_request)
1484 Session().add(pull_request)
1484 Session().commit()
1485 Session().commit()
1485
1486
1486 response = self.app.get(route_path(
1487 response = self.app.get(route_path(
1487 'pullrequest_show',
1488 'pullrequest_show',
1488 repo_name=pull_request.target_repo.scm_instance().name,
1489 repo_name=pull_request.target_repo.scm_instance().name,
1489 pull_request_id=pull_request.pull_request_id))
1490 pull_request_id=pull_request.pull_request_id))
1490 assert response.status_int == 200
1491 assert response.status_int == 200
1491
1492
1492 source = response.assert_response().get_element('.pr-source-info')
1493 source = response.assert_response().get_element('.pr-source-info')
1493 assert source.text.strip() == 'bookmark:origin'
1494 assert source.text.strip() == 'bookmark:origin'
1494 assert source.getparent().attrib.get('href') is None
1495 assert source.getparent().attrib.get('href') is None
1495
1496
1496 target = response.assert_response().get_element('.pr-target-info')
1497 target = response.assert_response().get_element('.pr-target-info')
1497 assert target.text.strip() == 'bookmark:target'
1498 assert target.text.strip() == 'bookmark:target'
1498 assert target.getparent().attrib.get('href') is None
1499 assert target.getparent().attrib.get('href') is None
1499
1500
1500 def test_tag_is_not_a_link(self, pr_util):
1501 def test_tag_is_not_a_link(self, pr_util):
1501 pull_request = pr_util.create_pull_request()
1502 pull_request = pr_util.create_pull_request()
1502 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1503 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1503 pull_request.target_ref = 'tag:target:abcdef1234567890'
1504 pull_request.target_ref = 'tag:target:abcdef1234567890'
1504 Session().add(pull_request)
1505 Session().add(pull_request)
1505 Session().commit()
1506 Session().commit()
1506
1507
1507 response = self.app.get(route_path(
1508 response = self.app.get(route_path(
1508 'pullrequest_show',
1509 'pullrequest_show',
1509 repo_name=pull_request.target_repo.scm_instance().name,
1510 repo_name=pull_request.target_repo.scm_instance().name,
1510 pull_request_id=pull_request.pull_request_id))
1511 pull_request_id=pull_request.pull_request_id))
1511 assert response.status_int == 200
1512 assert response.status_int == 200
1512
1513
1513 source = response.assert_response().get_element('.pr-source-info')
1514 source = response.assert_response().get_element('.pr-source-info')
1514 assert source.text.strip() == 'tag:origin'
1515 assert source.text.strip() == 'tag:origin'
1515 assert source.getparent().attrib.get('href') is None
1516 assert source.getparent().attrib.get('href') is None
1516
1517
1517 target = response.assert_response().get_element('.pr-target-info')
1518 target = response.assert_response().get_element('.pr-target-info')
1518 assert target.text.strip() == 'tag:target'
1519 assert target.text.strip() == 'tag:target'
1519 assert target.getparent().attrib.get('href') is None
1520 assert target.getparent().attrib.get('href') is None
1520
1521
1521 @pytest.mark.parametrize('mergeable', [True, False])
1522 @pytest.mark.parametrize('mergeable', [True, False])
1522 def test_shadow_repository_link(
1523 def test_shadow_repository_link(
1523 self, mergeable, pr_util, http_host_only_stub):
1524 self, mergeable, pr_util, http_host_only_stub):
1524 """
1525 """
1525 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
1526 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
1527 the link should not be displayed.
1528 the link should not be displayed.
1528 """
1529 """
1529 pull_request = pr_util.create_pull_request(
1530 pull_request = pr_util.create_pull_request(
1530 mergeable=mergeable, enable_notifications=False)
1531 mergeable=mergeable, enable_notifications=False)
1531 target_repo = pull_request.target_repo.scm_instance()
1532 target_repo = pull_request.target_repo.scm_instance()
1532 pr_id = pull_request.pull_request_id
1533 pr_id = pull_request.pull_request_id
1533 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1534 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1534 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)
1535
1536
1536 response = self.app.get(route_path(
1537 response = self.app.get(route_path(
1537 'pullrequest_show',
1538 'pullrequest_show',
1538 repo_name=target_repo.name,
1539 repo_name=target_repo.name,
1539 pull_request_id=pr_id))
1540 pull_request_id=pr_id))
1540
1541
1541 if mergeable:
1542 if mergeable:
1542 response.assert_response().element_value_contains(
1543 response.assert_response().element_value_contains(
1543 'input.pr-mergeinfo', shadow_url)
1544 'input.pr-mergeinfo', shadow_url)
1544 response.assert_response().element_value_contains(
1545 response.assert_response().element_value_contains(
1545 'input.pr-mergeinfo ', 'pr-merge')
1546 'input.pr-mergeinfo ', 'pr-merge')
1546 else:
1547 else:
1547 response.assert_response().no_element_exists('.pr-mergeinfo')
1548 response.assert_response().no_element_exists('.pr-mergeinfo')
1548
1549
1549
1550
1550 @pytest.mark.usefixtures('app')
1551 @pytest.mark.usefixtures('app')
1551 @pytest.mark.backends("git", "hg")
1552 @pytest.mark.backends("git", "hg")
1552 class TestPullrequestsControllerDelete(object):
1553 class TestPullrequestsControllerDelete(object):
1553 def test_pull_request_delete_button_permissions_admin(
1554 def test_pull_request_delete_button_permissions_admin(
1554 self, autologin_user, user_admin, pr_util):
1555 self, autologin_user, user_admin, pr_util):
1555 pull_request = pr_util.create_pull_request(
1556 pull_request = pr_util.create_pull_request(
1556 author=user_admin.username, enable_notifications=False)
1557 author=user_admin.username, enable_notifications=False)
1557
1558
1558 response = self.app.get(route_path(
1559 response = self.app.get(route_path(
1559 'pullrequest_show',
1560 'pullrequest_show',
1560 repo_name=pull_request.target_repo.scm_instance().name,
1561 repo_name=pull_request.target_repo.scm_instance().name,
1561 pull_request_id=pull_request.pull_request_id))
1562 pull_request_id=pull_request.pull_request_id))
1562
1563
1563 response.mustcontain('id="delete_pullrequest"')
1564 response.mustcontain('id="delete_pullrequest"')
1564 response.mustcontain('Confirm to delete this pull request')
1565 response.mustcontain('Confirm to delete this pull request')
1565
1566
1566 def test_pull_request_delete_button_permissions_owner(
1567 def test_pull_request_delete_button_permissions_owner(
1567 self, autologin_regular_user, user_regular, pr_util):
1568 self, autologin_regular_user, user_regular, pr_util):
1568 pull_request = pr_util.create_pull_request(
1569 pull_request = pr_util.create_pull_request(
1569 author=user_regular.username, enable_notifications=False)
1570 author=user_regular.username, enable_notifications=False)
1570
1571
1571 response = self.app.get(route_path(
1572 response = self.app.get(route_path(
1572 'pullrequest_show',
1573 'pullrequest_show',
1573 repo_name=pull_request.target_repo.scm_instance().name,
1574 repo_name=pull_request.target_repo.scm_instance().name,
1574 pull_request_id=pull_request.pull_request_id))
1575 pull_request_id=pull_request.pull_request_id))
1575
1576
1576 response.mustcontain('id="delete_pullrequest"')
1577 response.mustcontain('id="delete_pullrequest"')
1577 response.mustcontain('Confirm to delete this pull request')
1578 response.mustcontain('Confirm to delete this pull request')
1578
1579
1579 def test_pull_request_delete_button_permissions_forbidden(
1580 def test_pull_request_delete_button_permissions_forbidden(
1580 self, autologin_regular_user, user_regular, user_admin, pr_util):
1581 self, autologin_regular_user, user_regular, user_admin, pr_util):
1581 pull_request = pr_util.create_pull_request(
1582 pull_request = pr_util.create_pull_request(
1582 author=user_admin.username, enable_notifications=False)
1583 author=user_admin.username, enable_notifications=False)
1583
1584
1584 response = self.app.get(route_path(
1585 response = self.app.get(route_path(
1585 'pullrequest_show',
1586 'pullrequest_show',
1586 repo_name=pull_request.target_repo.scm_instance().name,
1587 repo_name=pull_request.target_repo.scm_instance().name,
1587 pull_request_id=pull_request.pull_request_id))
1588 pull_request_id=pull_request.pull_request_id))
1588 response.mustcontain(no=['id="delete_pullrequest"'])
1589 response.mustcontain(no=['id="delete_pullrequest"'])
1589 response.mustcontain(no=['Confirm to delete this pull request'])
1590 response.mustcontain(no=['Confirm to delete this pull request'])
1590
1591
1591 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1592 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1592 self, autologin_regular_user, user_regular, user_admin, pr_util,
1593 self, autologin_regular_user, user_regular, user_admin, pr_util,
1593 user_util):
1594 user_util):
1594
1595
1595 pull_request = pr_util.create_pull_request(
1596 pull_request = pr_util.create_pull_request(
1596 author=user_admin.username, enable_notifications=False)
1597 author=user_admin.username, enable_notifications=False)
1597
1598
1598 user_util.grant_user_permission_to_repo(
1599 user_util.grant_user_permission_to_repo(
1599 pull_request.target_repo, user_regular,
1600 pull_request.target_repo, user_regular,
1600 'repository.write')
1601 'repository.write')
1601
1602
1602 response = self.app.get(route_path(
1603 response = self.app.get(route_path(
1603 'pullrequest_show',
1604 'pullrequest_show',
1604 repo_name=pull_request.target_repo.scm_instance().name,
1605 repo_name=pull_request.target_repo.scm_instance().name,
1605 pull_request_id=pull_request.pull_request_id))
1606 pull_request_id=pull_request.pull_request_id))
1606
1607
1607 response.mustcontain('id="open_edit_pullrequest"')
1608 response.mustcontain('id="open_edit_pullrequest"')
1608 response.mustcontain('id="delete_pullrequest"')
1609 response.mustcontain('id="delete_pullrequest"')
1609 response.mustcontain(no=['Confirm to delete this pull request'])
1610 response.mustcontain(no=['Confirm to delete this pull request'])
1610
1611
1611 def test_delete_comment_returns_404_if_comment_does_not_exist(
1612 def test_delete_comment_returns_404_if_comment_does_not_exist(
1612 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1613 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1613
1614
1614 pull_request = pr_util.create_pull_request(
1615 pull_request = pr_util.create_pull_request(
1615 author=user_admin.username, enable_notifications=False)
1616 author=user_admin.username, enable_notifications=False)
1616
1617
1617 self.app.post(
1618 self.app.post(
1618 route_path(
1619 route_path(
1619 'pullrequest_comment_delete',
1620 'pullrequest_comment_delete',
1620 repo_name=pull_request.target_repo.scm_instance().name,
1621 repo_name=pull_request.target_repo.scm_instance().name,
1621 pull_request_id=pull_request.pull_request_id,
1622 pull_request_id=pull_request.pull_request_id,
1622 comment_id=1024404),
1623 comment_id=1024404),
1623 extra_environ=xhr_header,
1624 extra_environ=xhr_header,
1624 params={'csrf_token': csrf_token},
1625 params={'csrf_token': csrf_token},
1625 status=404
1626 status=404
1626 )
1627 )
1627
1628
1628 def test_delete_comment(
1629 def test_delete_comment(
1629 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1630 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1630
1631
1631 pull_request = pr_util.create_pull_request(
1632 pull_request = pr_util.create_pull_request(
1632 author=user_admin.username, enable_notifications=False)
1633 author=user_admin.username, enable_notifications=False)
1633 comment = pr_util.create_comment()
1634 comment = pr_util.create_comment()
1634 comment_id = comment.comment_id
1635 comment_id = comment.comment_id
1635
1636
1636 response = self.app.post(
1637 response = self.app.post(
1637 route_path(
1638 route_path(
1638 'pullrequest_comment_delete',
1639 'pullrequest_comment_delete',
1639 repo_name=pull_request.target_repo.scm_instance().name,
1640 repo_name=pull_request.target_repo.scm_instance().name,
1640 pull_request_id=pull_request.pull_request_id,
1641 pull_request_id=pull_request.pull_request_id,
1641 comment_id=comment_id),
1642 comment_id=comment_id),
1642 extra_environ=xhr_header,
1643 extra_environ=xhr_header,
1643 params={'csrf_token': csrf_token},
1644 params={'csrf_token': csrf_token},
1644 status=200
1645 status=200
1645 )
1646 )
1646 assert response.text == 'true'
1647 assert response.text == 'true'
1647
1648
1648 @pytest.mark.parametrize('url_type', [
1649 @pytest.mark.parametrize('url_type', [
1649 'pullrequest_new',
1650 'pullrequest_new',
1650 'pullrequest_create',
1651 'pullrequest_create',
1651 'pullrequest_update',
1652 'pullrequest_update',
1652 'pullrequest_merge',
1653 'pullrequest_merge',
1653 ])
1654 ])
1654 def test_pull_request_is_forbidden_on_archived_repo(
1655 def test_pull_request_is_forbidden_on_archived_repo(
1655 self, autologin_user, backend, xhr_header, user_util, url_type):
1656 self, autologin_user, backend, xhr_header, user_util, url_type):
1656
1657
1657 # create a temporary repo
1658 # create a temporary repo
1658 source = user_util.create_repo(repo_type=backend.alias)
1659 source = user_util.create_repo(repo_type=backend.alias)
1659 repo_name = source.repo_name
1660 repo_name = source.repo_name
1660 repo = Repository.get_by_repo_name(repo_name)
1661 repo = Repository.get_by_repo_name(repo_name)
1661 repo.archived = True
1662 repo.archived = True
1662 Session().commit()
1663 Session().commit()
1663
1664
1664 response = self.app.get(
1665 response = self.app.get(
1665 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)
1666
1667
1667 msg = 'Action not supported for archived repository.'
1668 msg = 'Action not supported for archived repository.'
1668 assert_session_flash(response, msg)
1669 assert_session_flash(response, msg)
1669
1670
1670
1671
1671 def assert_pull_request_status(pull_request, expected_status):
1672 def assert_pull_request_status(pull_request, expected_status):
1672 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1673 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1673 assert status == expected_status
1674 assert status == expected_status
1674
1675
1675
1676
1676 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1677 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1677 @pytest.mark.usefixtures("autologin_user")
1678 @pytest.mark.usefixtures("autologin_user")
1678 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):
1679 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 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
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
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
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
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
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