##// END OF EJS Templates
repo-groups: fixed a regression for updating nested repository groups
marcink -
r2241:74a4825b stable
parent child Browse files
Show More
@@ -1,183 +1,183 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import deform
23 23
24 24 from pyramid.view import view_config
25 25 from pyramid.httpexceptions import HTTPFound
26 26
27 27 from rhodecode.apps._base import RepoGroupAppView
28 28 from rhodecode.forms import RcForm
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib import audit_logger
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, HasPermissionAll,
33 33 HasRepoGroupPermissionAny, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
34 34 from rhodecode.model.db import Session, RepoGroup
35 35 from rhodecode.model.scm import RepoGroupList
36 36 from rhodecode.model.repo_group import RepoGroupModel
37 37 from rhodecode.model.validation_schema.schemas import repo_group_schema
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class RepoGroupSettingsView(RepoGroupAppView):
43 43 def load_default_context(self):
44 44 c = self._get_local_tmpl_context()
45 45 c.repo_group = self.db_repo_group
46 46 no_parrent = not c.repo_group.parent_group
47 47 can_create_in_root = self._can_create_repo_group()
48 48
49 49 show_root_location = False
50 50 if no_parrent or can_create_in_root:
51 51 # we're global admin, we're ok and we can create TOP level groups
52 52 # or in case this group is already at top-level we also allow
53 53 # creation in root
54 54 show_root_location = True
55 55
56 56 acl_groups = RepoGroupList(
57 57 RepoGroup.query().all(),
58 58 perm_set=['group.admin'])
59 59 c.repo_groups = RepoGroup.groups_choices(
60 60 groups=acl_groups,
61 61 show_empty_group=show_root_location)
62 62 # filter out current repo group
63 63 exclude_group_ids = [c.repo_group.group_id]
64 64 c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids,
65 65 c.repo_groups)
66 66 c.repo_groups_choices = map(lambda k: k[0], c.repo_groups)
67 67
68 68 parent_group = c.repo_group.parent_group
69 69
70 70 add_parent_group = (parent_group and (
71 71 parent_group.group_id not in c.repo_groups_choices))
72 72 if add_parent_group:
73 73 c.repo_groups_choices.append(parent_group.group_id)
74 74 c.repo_groups.append(RepoGroup._generate_choice(parent_group))
75 75
76 76 self._register_global_c(c)
77 77 return c
78 78
79 79 def _can_create_repo_group(self, parent_group_id=None):
80 80 is_admin = HasPermissionAll('hg.admin')('group create controller')
81 81 create_repo_group = HasPermissionAll(
82 82 'hg.repogroup.create.true')('group create controller')
83 83 if is_admin or (create_repo_group and not parent_group_id):
84 84 # we're global admin, or we have global repo group create
85 85 # permission
86 86 # we're ok and we can create TOP level groups
87 87 return True
88 88 elif parent_group_id:
89 89 # we check the permission if we can write to parent group
90 90 group = RepoGroup.get(parent_group_id)
91 91 group_name = group.group_name if group else None
92 92 if HasRepoGroupPermissionAny('group.admin')(
93 93 group_name, 'check if user is an admin of group'):
94 94 # we're an admin of passed in group, we're ok.
95 95 return True
96 96 else:
97 97 return False
98 98 return False
99 99
100 100 def _get_schema(self, c, old_values=None):
101 101 return repo_group_schema.RepoGroupSettingsSchema().bind(
102 102 repo_group_repo_group_options=c.repo_groups_choices,
103 103 repo_group_repo_group_items=c.repo_groups,
104 104
105 105 # user caller
106 106 user=self._rhodecode_user,
107 107 old_values=old_values
108 108 )
109 109
110 110 @LoginRequired()
111 111 @HasRepoGroupPermissionAnyDecorator('group.admin')
112 112 @view_config(
113 113 route_name='edit_repo_group', request_method='GET',
114 114 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
115 115 def edit_settings(self):
116 116 c = self.load_default_context()
117 117 c.active = 'settings'
118 118
119 119 defaults = RepoGroupModel()._get_defaults(self.db_repo_group_name)
120 120 defaults['repo_group_owner'] = defaults['user']
121 121
122 122 schema = self._get_schema(c)
123 123 c.form = RcForm(schema, appstruct=defaults)
124 124 return self._get_template_context(c)
125 125
126 126 @LoginRequired()
127 127 @HasRepoGroupPermissionAnyDecorator('group.admin')
128 128 @CSRFRequired()
129 129 @view_config(
130 130 route_name='edit_repo_group', request_method='POST',
131 131 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
132 132 def edit_settings_update(self):
133 133 _ = self.request.translate
134 134 c = self.load_default_context()
135 135 c.active = 'settings'
136 136
137 137 old_repo_group_name = self.db_repo_group_name
138 138 new_repo_group_name = old_repo_group_name
139 139
140 140 old_values = RepoGroupModel()._get_defaults(self.db_repo_group_name)
141 141 schema = self._get_schema(c, old_values=old_values)
142 142
143 143 c.form = RcForm(schema)
144 144 pstruct = self.request.POST.items()
145 145
146 146 try:
147 147 schema_data = c.form.validate(pstruct)
148 148 except deform.ValidationFailure as err_form:
149 149 return self._get_template_context(c)
150 150
151 151 # data is now VALID, proceed with updates
152 152 # save validated data back into the updates dict
153 153 validated_updates = dict(
154 154 group_name=schema_data['repo_group']['repo_group_name_without_group'],
155 155 group_parent_id=schema_data['repo_group']['repo_group_id'],
156 156 user=schema_data['repo_group_owner'],
157 157 group_description=schema_data['repo_group_description'],
158 158 enable_locking=schema_data['repo_group_enable_locking'],
159 159 )
160 160
161 161 try:
162 162 RepoGroupModel().update(self.db_repo_group, validated_updates)
163 163
164 164 audit_logger.store_web(
165 165 'repo_group.edit', action_data={'old_data': old_values},
166 166 user=c.rhodecode_user)
167 167
168 168 Session().commit()
169 169
170 170 # use the new full name for redirect once we know we updated
171 171 # the name on filesystem and in DB
172 new_repo_group_name = schema_data['repo_group_name']
172 new_repo_group_name = schema_data['repo_group']['repo_group_name_with_group']
173 173
174 174 h.flash(_('Repository Group `{}` updated successfully').format(
175 175 old_repo_group_name), category='success')
176 176
177 177 except Exception:
178 178 log.exception("Exception during update or repository group")
179 179 h.flash(_('Error occurred during update of repository group %s')
180 180 % old_repo_group_name, category='error')
181 181
182 182 raise HTTPFound(
183 183 h.route_path('edit_repo_group', repo_group_name=new_repo_group_name))
@@ -1,284 +1,298 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import colander
23 23 import deform.widget
24 24
25 25 from rhodecode.translation import _
26 26 from rhodecode.model.validation_schema import validators, preparers, types
27 27
28 28
29 29 def get_group_and_repo(repo_name):
30 30 from rhodecode.model.repo_group import RepoGroupModel
31 31 return RepoGroupModel()._get_group_name_and_parent(
32 32 repo_name, get_object=True)
33 33
34 34
35 def get_repo_group(repo_group_id):
36 from rhodecode.model.repo_group import RepoGroup
37 return RepoGroup.get(repo_group_id), RepoGroup.CHOICES_SEPARATOR
38
39
35 40 @colander.deferred
36 41 def deferred_can_write_to_group_validator(node, kw):
37 42 old_values = kw.get('old_values') or {}
38 43 request_user = kw.get('user')
39 44
40 45 def can_write_group_validator(node, value):
41 46 from rhodecode.lib.auth import (
42 47 HasPermissionAny, HasRepoGroupPermissionAny)
43 48 from rhodecode.model.repo_group import RepoGroupModel
44 49
45 50 messages = {
46 51 'invalid_parent_repo_group':
47 52 _(u"Parent repository group `{}` does not exist"),
48 53 # permissions denied we expose as not existing, to prevent
49 54 # resource discovery
50 55 'permission_denied_parent_group':
51 56 _(u"Parent repository group `{}` does not exist"),
52 57 'permission_denied_root':
53 58 _(u"You do not have the permission to store "
54 59 u"repository groups in the root location.")
55 60 }
56 61
57 62 value = value['repo_group_name']
58 63 parent_group_name = value
59 64
60 65 is_root_location = value is types.RootLocation
61 66
62 67 # NOT initialized validators, we must call them
63 68 can_create_repo_groups_at_root = HasPermissionAny(
64 69 'hg.admin', 'hg.repogroup.create.true')
65 70
66 71 if is_root_location:
67 72 if can_create_repo_groups_at_root(user=request_user):
68 73 # we can create repo group inside tool-level. No more checks
69 74 # are required
70 75 return
71 76 else:
72 77 raise colander.Invalid(node, messages['permission_denied_root'])
73 78
74 79 # check if the parent repo group actually exists
75 80 parent_group = None
76 81 if parent_group_name:
77 82 parent_group = RepoGroupModel().get_by_group_name(parent_group_name)
78 83 if value and not parent_group:
79 84 raise colander.Invalid(
80 85 node, messages['invalid_parent_repo_group'].format(
81 86 parent_group_name))
82 87
83 88 # check if we have permissions to create new groups under
84 89 # parent repo group
85 90 # create repositories with write permission on group is set to true
86 91 create_on_write = HasPermissionAny(
87 92 'hg.create.write_on_repogroup.true')(user=request_user)
88 93
89 94 group_admin = HasRepoGroupPermissionAny('group.admin')(
90 95 parent_group_name, 'can write into group validator', user=request_user)
91 96 group_write = HasRepoGroupPermissionAny('group.write')(
92 97 parent_group_name, 'can write into group validator', user=request_user)
93 98
94 99 # creation by write access is currently disabled. Needs thinking if
95 100 # we want to allow this...
96 101 forbidden = not (group_admin or (group_write and create_on_write and 0))
97 102
98 103 if parent_group and forbidden:
99 104 msg = messages['permission_denied_parent_group'].format(
100 105 parent_group_name)
101 106 raise colander.Invalid(node, msg)
102 107
103 108 return can_write_group_validator
104 109
105 110
106 111 @colander.deferred
107 112 def deferred_repo_group_owner_validator(node, kw):
108 113
109 114 def repo_owner_validator(node, value):
110 115 from rhodecode.model.db import User
111 116 existing = User.get_by_username(value)
112 117 if not existing:
113 118 msg = _(u'Repo group owner with id `{}` does not exists').format(
114 119 value)
115 120 raise colander.Invalid(node, msg)
116 121
117 122 return repo_owner_validator
118 123
119 124
120 125 @colander.deferred
121 126 def deferred_unique_name_validator(node, kw):
122 127 request_user = kw.get('user')
123 128 old_values = kw.get('old_values') or {}
124 129
125 130 def unique_name_validator(node, value):
126 131 from rhodecode.model.db import Repository, RepoGroup
127 132 name_changed = value != old_values.get('group_name')
128 133
129 134 existing = Repository.get_by_repo_name(value)
130 135 if name_changed and existing:
131 136 msg = _(u'Repository with name `{}` already exists').format(value)
132 137 raise colander.Invalid(node, msg)
133 138
134 139 existing_group = RepoGroup.get_by_group_name(value)
135 140 if name_changed and existing_group:
136 141 msg = _(u'Repository group with name `{}` already exists').format(
137 142 value)
138 143 raise colander.Invalid(node, msg)
139 144 return unique_name_validator
140 145
141 146
142 147 @colander.deferred
143 148 def deferred_repo_group_name_validator(node, kw):
144 149 return validators.valid_name_validator
145 150
146 151
147 152 @colander.deferred
148 153 def deferred_repo_group_validator(node, kw):
149 154 options = kw.get(
150 155 'repo_group_repo_group_options')
151 156 return colander.OneOf([x for x in options])
152 157
153 158
154 159 @colander.deferred
155 160 def deferred_repo_group_widget(node, kw):
156 161 items = kw.get('repo_group_repo_group_items')
157 162 return deform.widget.Select2Widget(values=items)
158 163
159 164
160 165 class GroupType(colander.Mapping):
161 166 def _validate(self, node, value):
162 167 try:
163 168 return dict(repo_group_name=value)
164 169 except Exception as e:
165 170 raise colander.Invalid(
166 171 node, '"${val}" is not a mapping type: ${err}'.format(
167 172 val=value, err=e))
168 173
169 174 def deserialize(self, node, cstruct):
170 175 if cstruct is colander.null:
171 176 return cstruct
172 177
173 178 appstruct = super(GroupType, self).deserialize(node, cstruct)
174 179 validated_name = appstruct['repo_group_name']
175 180
176 181 # inject group based on once deserialized data
177 182 (repo_group_name_without_group,
178 183 parent_group_name,
179 184 parent_group) = get_group_and_repo(validated_name)
180 185
186 appstruct['repo_group_name_with_group'] = validated_name
181 187 appstruct['repo_group_name_without_group'] = repo_group_name_without_group
182 188 appstruct['repo_group_name'] = parent_group_name or types.RootLocation
183 189 if parent_group:
184 190 appstruct['repo_group_id'] = parent_group.group_id
185 191
186 192 return appstruct
187 193
188 194
189 195 class GroupSchema(colander.SchemaNode):
190 196 schema_type = GroupType
191 197 validator = deferred_can_write_to_group_validator
192 198 missing = colander.null
193 199
194 200
195 201 class RepoGroup(GroupSchema):
196 202 repo_group_name = colander.SchemaNode(
197 203 types.GroupNameType())
198 204 repo_group_id = colander.SchemaNode(
199 205 colander.String(), missing=None)
200 206 repo_group_name_without_group = colander.SchemaNode(
201 207 colander.String(), missing=None)
202 208
203 209
204 210 class RepoGroupAccessSchema(colander.MappingSchema):
205 211 repo_group = RepoGroup()
206 212
207 213
208 214 class RepoGroupNameUniqueSchema(colander.MappingSchema):
209 215 unique_repo_group_name = colander.SchemaNode(
210 216 colander.String(),
211 217 validator=deferred_unique_name_validator)
212 218
213 219
214 220 class RepoGroupSchema(colander.Schema):
215 221
216 222 repo_group_name = colander.SchemaNode(
217 223 types.GroupNameType(),
218 224 validator=deferred_repo_group_name_validator)
219 225
220 226 repo_group_owner = colander.SchemaNode(
221 227 colander.String(),
222 228 validator=deferred_repo_group_owner_validator)
223 229
224 230 repo_group_description = colander.SchemaNode(
225 231 colander.String(), missing='', widget=deform.widget.TextAreaWidget())
226 232
227 233 repo_group_copy_permissions = colander.SchemaNode(
228 234 types.StringBooleanType(),
229 235 missing=False, widget=deform.widget.CheckboxWidget())
230 236
231 237 repo_group_enable_locking = colander.SchemaNode(
232 238 types.StringBooleanType(),
233 239 missing=False, widget=deform.widget.CheckboxWidget())
234 240
235 241 def deserialize(self, cstruct):
236 242 """
237 243 Custom deserialize that allows to chain validation, and verify
238 244 permissions, and as last step uniqueness
239 245 """
240 246
241 247 appstruct = super(RepoGroupSchema, self).deserialize(cstruct)
242 248 validated_name = appstruct['repo_group_name']
243 249
244 250 # second pass to validate permissions to repo_group
245 251 second = RepoGroupAccessSchema().bind(**self.bindings)
246 252 appstruct_second = second.deserialize({'repo_group': validated_name})
247 253 # save result
248 254 appstruct['repo_group'] = appstruct_second['repo_group']
249 255
250 256 # thirds to validate uniqueness
251 257 third = RepoGroupNameUniqueSchema().bind(**self.bindings)
252 258 third.deserialize({'unique_repo_group_name': validated_name})
253 259
254 260 return appstruct
255 261
256 262
257 263 class RepoGroupSettingsSchema(RepoGroupSchema):
258 264 repo_group = colander.SchemaNode(
259 265 colander.Integer(),
260 266 validator=deferred_repo_group_validator,
261 267 widget=deferred_repo_group_widget,
262 268 missing='')
263 269
264 270 def deserialize(self, cstruct):
265 271 """
266 272 Custom deserialize that allows to chain validation, and verify
267 273 permissions, and as last step uniqueness
268 274 """
269 275
270 276 # first pass, to validate given data
271 277 appstruct = super(RepoGroupSchema, self).deserialize(cstruct)
272 278 validated_name = appstruct['repo_group_name']
273 279
280 # because of repoSchema adds repo-group as an ID, we inject it as
281 # full name here because validators require it, it's unwrapped later
282 # so it's safe to use and final name is going to be without group anyway
283
284 group, separator = get_repo_group(appstruct['repo_group'])
285 if group:
286 validated_name = separator.join([group.group_name, validated_name])
287
274 288 # second pass to validate permissions to repo_group
275 289 second = RepoGroupAccessSchema().bind(**self.bindings)
276 290 appstruct_second = second.deserialize({'repo_group': validated_name})
277 291 # save result
278 292 appstruct['repo_group'] = appstruct_second['repo_group']
279 293
280 294 # thirds to validate uniqueness
281 295 third = RepoGroupNameUniqueSchema().bind(**self.bindings)
282 296 third.deserialize({'unique_repo_group_name': validated_name})
283 297
284 298 return appstruct
@@ -1,116 +1,118 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 22 import pytest
23 23
24 24 from rhodecode.model.validation_schema import types
25 25 from rhodecode.model.validation_schema.schemas import repo_group_schema
26 26
27 27
28 28 class TestRepoGroupSchema(object):
29 29
30 30 @pytest.mark.parametrize('given, expected', [
31 31 ('my repo', 'my-repo'),
32 32 (' hello world mike ', 'hello-world-mike'),
33 33
34 34 ('//group1/group2//', 'group1/group2'),
35 35 ('//group1///group2//', 'group1/group2'),
36 36 ('///group1/group2///group3', 'group1/group2/group3'),
37 37 ('word g1/group2///group3', 'word-g1/group2/group3'),
38 38
39 39 ('grou p1/gro;,,##up2//.../group3', 'grou-p1/group2/group3'),
40 40
41 41 ('group,,,/,,,/1/2/3', 'group/1/2/3'),
42 42 ('grou[]p1/gro;up2///gro up3', 'group1/group2/gro-up3'),
43 43 (u'grou[]p1/gro;up2///gro up3/Δ…Δ‡', u'group1/group2/gro-up3/Δ…Δ‡'),
44 44 ])
45 45 def test_deserialize_repo_name(self, app, user_admin, given, expected):
46 46 schema = repo_group_schema.RepoGroupSchema().bind()
47 47 assert schema.get('repo_group_name').deserialize(given) == expected
48 48
49 49 def test_deserialize(self, app, user_admin):
50 50 schema = repo_group_schema.RepoGroupSchema().bind(
51 51 user=user_admin
52 52 )
53 53
54 54 schema_data = schema.deserialize(dict(
55 55 repo_group_name='my_schema_group',
56 56 repo_group_owner=user_admin.username
57 57 ))
58 58
59 59 assert schema_data['repo_group_name'] == u'my_schema_group'
60 60 assert schema_data['repo_group'] == {
61 61 'repo_group_id': None,
62 62 'repo_group_name': types.RootLocation,
63 'repo_group_name_with_group': u'my_schema_group',
63 64 'repo_group_name_without_group': u'my_schema_group'}
64 65
65 66 @pytest.mark.parametrize('given, err_key, expected_exc', [
66 67 ('xxx/my_schema_group', 'repo_group', 'Parent repository group `xxx` does not exist'),
67 68 ('', 'repo_group_name', 'Name must start with a letter or number. Got ``'),
68 69 ])
69 70 def test_deserialize_with_bad_group_name(
70 71 self, app, user_admin, given, err_key, expected_exc):
71 72 schema = repo_group_schema.RepoGroupSchema().bind(
72 73 repo_type_options=['hg'],
73 74 user=user_admin
74 75 )
75 76
76 77 with pytest.raises(colander.Invalid) as excinfo:
77 78 schema.deserialize(dict(
78 79 repo_group_name=given,
79 80 repo_group_owner=user_admin.username
80 81 ))
81 82
82 83 assert excinfo.value.asdict()[err_key] == expected_exc
83 84
84 85 def test_deserialize_with_group_name(self, app, user_admin, test_repo_group):
85 86 schema = repo_group_schema.RepoGroupSchema().bind(
86 87 user=user_admin
87 88 )
88 89
89 90 full_name = test_repo_group.group_name + u'/my_schema_group'
90 91 schema_data = schema.deserialize(dict(
91 92 repo_group_name=full_name,
92 93 repo_group_owner=user_admin.username
93 94 ))
94 95
95 96 assert schema_data['repo_group_name'] == full_name
96 97 assert schema_data['repo_group'] == {
97 98 'repo_group_id': test_repo_group.group_id,
98 99 'repo_group_name': test_repo_group.group_name,
100 'repo_group_name_with_group': full_name,
99 101 'repo_group_name_without_group': u'my_schema_group'}
100 102
101 103 def test_deserialize_with_group_name_regular_user_no_perms(
102 104 self, app, user_regular, test_repo_group):
103 105 schema = repo_group_schema.RepoGroupSchema().bind(
104 106 user=user_regular
105 107 )
106 108
107 109 full_name = test_repo_group.group_name + u'/my_schema_group'
108 110 with pytest.raises(colander.Invalid) as excinfo:
109 111 schema.deserialize(dict(
110 112 repo_group_name=full_name,
111 113 repo_group_owner=user_regular.username
112 114 ))
113 115
114 116 expected = 'Parent repository group `{}` does not exist'.format(
115 117 test_repo_group.group_name)
116 118 assert excinfo.value.asdict()['repo_group'] == expected
General Comments 0
You need to be logged in to leave comments. Login now