##// END OF EJS Templates
tests: protocol ops, added --pull and --pull --stream clone tests
dan -
r3525:442219c2 default
parent child Browse files
Show More
@@ -1,469 +1,483 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 Test suite for making push/pull operations, on specially modified INI files
23 23
24 24 .. important::
25 25
26 26 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
27 27 to redirect things to stderr instead of stdout.
28 28 """
29 29
30 30
31 31 import time
32 32
33 33 import pytest
34 34
35 35 from rhodecode.lib import rc_cache
36 36 from rhodecode.model.auth_token import AuthTokenModel
37 37 from rhodecode.model.db import Repository, UserIpMap, CacheKey
38 38 from rhodecode.model.meta import Session
39 39 from rhodecode.model.repo import RepoModel
40 40 from rhodecode.model.user import UserModel
41 41 from rhodecode.tests import (GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN)
42 42
43 43 from rhodecode.tests.vcs_operations import (
44 44 Command, _check_proper_clone, _check_proper_git_push,
45 45 _add_files_and_push, HG_REPO_WITH_GROUP, GIT_REPO_WITH_GROUP)
46 46
47 47
48 48 @pytest.mark.usefixtures("disable_locking", "disable_anonymous_user")
49 49 class TestVCSOperations(object):
50 50
51 51 def test_clone_hg_repo_by_admin(self, rc_web_server, tmpdir):
52 52 clone_url = rc_web_server.repo_clone_url(HG_REPO)
53 53 stdout, stderr = Command('/tmp').execute(
54 54 'hg clone', clone_url, tmpdir.strpath)
55 55 _check_proper_clone(stdout, stderr, 'hg')
56 56
57 def test_clone_hg_repo_by_admin_pull_protocol(self, rc_web_server, tmpdir):
58 clone_url = rc_web_server.repo_clone_url(HG_REPO)
59 stdout, stderr = Command('/tmp').execute(
60 'hg clone --pull', clone_url, tmpdir.strpath)
61 _check_proper_clone(stdout, stderr, 'hg')
62
63 def test_clone_hg_repo_by_admin_pull_stream_protocol(self, rc_web_server, tmpdir):
64 clone_url = rc_web_server.repo_clone_url(HG_REPO)
65 stdout, stderr = Command('/tmp').execute(
66 'hg clone --pull --stream', clone_url, tmpdir.strpath)
67 assert '225 files to transfer, 1.04 MB of data' in stdout
68 assert 'transferred 1.04 MB' in stdout
69 assert '114 files updated,' in stdout
70
57 71 def test_clone_git_repo_by_admin(self, rc_web_server, tmpdir):
58 72 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
59 73 cmd = Command('/tmp')
60 74 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
61 75 _check_proper_clone(stdout, stderr, 'git')
62 76 cmd.assert_returncode_success()
63 77
64 78 def test_clone_git_repo_by_admin_with_git_suffix(self, rc_web_server, tmpdir):
65 79 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
66 80 cmd = Command('/tmp')
67 81 stdout, stderr = cmd.execute('git clone', clone_url+".git", tmpdir.strpath)
68 82 _check_proper_clone(stdout, stderr, 'git')
69 83 cmd.assert_returncode_success()
70 84
71 85 def test_clone_hg_repo_by_id_by_admin(self, rc_web_server, tmpdir):
72 86 repo_id = Repository.get_by_repo_name(HG_REPO).repo_id
73 87 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
74 88 stdout, stderr = Command('/tmp').execute(
75 89 'hg clone', clone_url, tmpdir.strpath)
76 90 _check_proper_clone(stdout, stderr, 'hg')
77 91
78 92 def test_clone_git_repo_by_id_by_admin(self, rc_web_server, tmpdir):
79 93 repo_id = Repository.get_by_repo_name(GIT_REPO).repo_id
80 94 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
81 95 cmd = Command('/tmp')
82 96 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
83 97 _check_proper_clone(stdout, stderr, 'git')
84 98 cmd.assert_returncode_success()
85 99
86 100 def test_clone_hg_repo_with_group_by_admin(self, rc_web_server, tmpdir):
87 101 clone_url = rc_web_server.repo_clone_url(HG_REPO_WITH_GROUP)
88 102 stdout, stderr = Command('/tmp').execute(
89 103 'hg clone', clone_url, tmpdir.strpath)
90 104 _check_proper_clone(stdout, stderr, 'hg')
91 105
92 106 def test_clone_git_repo_with_group_by_admin(self, rc_web_server, tmpdir):
93 107 clone_url = rc_web_server.repo_clone_url(GIT_REPO_WITH_GROUP)
94 108 cmd = Command('/tmp')
95 109 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
96 110 _check_proper_clone(stdout, stderr, 'git')
97 111 cmd.assert_returncode_success()
98 112
99 113 def test_clone_git_repo_shallow_by_admin(self, rc_web_server, tmpdir):
100 114 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
101 115 cmd = Command('/tmp')
102 116 stdout, stderr = cmd.execute(
103 117 'git clone --depth=1', clone_url, tmpdir.strpath)
104 118
105 119 assert '' == stdout
106 120 assert 'Cloning into' in stderr
107 121 cmd.assert_returncode_success()
108 122
109 123 def test_clone_wrong_credentials_hg(self, rc_web_server, tmpdir):
110 124 clone_url = rc_web_server.repo_clone_url(HG_REPO, passwd='bad!')
111 125 stdout, stderr = Command('/tmp').execute(
112 126 'hg clone', clone_url, tmpdir.strpath)
113 127 assert 'abort: authorization failed' in stderr
114 128
115 129 def test_clone_wrong_credentials_git(self, rc_web_server, tmpdir):
116 130 clone_url = rc_web_server.repo_clone_url(GIT_REPO, passwd='bad!')
117 131 stdout, stderr = Command('/tmp').execute(
118 132 'git clone', clone_url, tmpdir.strpath)
119 133 assert 'fatal: Authentication failed' in stderr
120 134
121 135 def test_clone_git_dir_as_hg(self, rc_web_server, tmpdir):
122 136 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
123 137 stdout, stderr = Command('/tmp').execute(
124 138 'hg clone', clone_url, tmpdir.strpath)
125 139 assert 'HTTP Error 404: Not Found' in stderr
126 140
127 141 def test_clone_hg_repo_as_git(self, rc_web_server, tmpdir):
128 142 clone_url = rc_web_server.repo_clone_url(HG_REPO)
129 143 stdout, stderr = Command('/tmp').execute(
130 144 'git clone', clone_url, tmpdir.strpath)
131 145 assert 'not found' in stderr
132 146
133 147 def test_clone_non_existing_path_hg(self, rc_web_server, tmpdir):
134 148 clone_url = rc_web_server.repo_clone_url('trololo')
135 149 stdout, stderr = Command('/tmp').execute(
136 150 'hg clone', clone_url, tmpdir.strpath)
137 151 assert 'HTTP Error 404: Not Found' in stderr
138 152
139 153 def test_clone_non_existing_path_git(self, rc_web_server, tmpdir):
140 154 clone_url = rc_web_server.repo_clone_url('trololo')
141 155 stdout, stderr = Command('/tmp').execute('git clone', clone_url)
142 156 assert 'not found' in stderr
143 157
144 158 def test_clone_hg_with_slashes(self, rc_web_server, tmpdir):
145 159 clone_url = rc_web_server.repo_clone_url('//' + HG_REPO)
146 160 stdout, stderr = Command('/tmp').execute('hg clone', clone_url, tmpdir.strpath)
147 161 assert 'HTTP Error 404: Not Found' in stderr
148 162
149 163 def test_clone_git_with_slashes(self, rc_web_server, tmpdir):
150 164 clone_url = rc_web_server.repo_clone_url('//' + GIT_REPO)
151 165 stdout, stderr = Command('/tmp').execute('git clone', clone_url)
152 166 assert 'not found' in stderr
153 167
154 168 def test_clone_existing_path_hg_not_in_database(
155 169 self, rc_web_server, tmpdir, fs_repo_only):
156 170
157 171 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
158 172 clone_url = rc_web_server.repo_clone_url(db_name)
159 173 stdout, stderr = Command('/tmp').execute(
160 174 'hg clone', clone_url, tmpdir.strpath)
161 175 assert 'HTTP Error 404: Not Found' in stderr
162 176
163 177 def test_clone_existing_path_git_not_in_database(
164 178 self, rc_web_server, tmpdir, fs_repo_only):
165 179 db_name = fs_repo_only('not-in-db-git', repo_type='git')
166 180 clone_url = rc_web_server.repo_clone_url(db_name)
167 181 stdout, stderr = Command('/tmp').execute(
168 182 'git clone', clone_url, tmpdir.strpath)
169 183 assert 'not found' in stderr
170 184
171 185 def test_clone_existing_path_hg_not_in_database_different_scm(
172 186 self, rc_web_server, tmpdir, fs_repo_only):
173 187 db_name = fs_repo_only('not-in-db-git', repo_type='git')
174 188 clone_url = rc_web_server.repo_clone_url(db_name)
175 189 stdout, stderr = Command('/tmp').execute(
176 190 'hg clone', clone_url, tmpdir.strpath)
177 191 assert 'HTTP Error 404: Not Found' in stderr
178 192
179 193 def test_clone_existing_path_git_not_in_database_different_scm(
180 194 self, rc_web_server, tmpdir, fs_repo_only):
181 195 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
182 196 clone_url = rc_web_server.repo_clone_url(db_name)
183 197 stdout, stderr = Command('/tmp').execute(
184 198 'git clone', clone_url, tmpdir.strpath)
185 199 assert 'not found' in stderr
186 200
187 201 def test_clone_non_existing_store_path_hg(self, rc_web_server, tmpdir, user_util):
188 202 repo = user_util.create_repo()
189 203 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
190 204
191 205 # Damage repo by removing it's folder
192 206 RepoModel()._delete_filesystem_repo(repo)
193 207
194 208 stdout, stderr = Command('/tmp').execute(
195 209 'hg clone', clone_url, tmpdir.strpath)
196 210 assert 'HTTP Error 404: Not Found' in stderr
197 211
198 212 def test_clone_non_existing_store_path_git(self, rc_web_server, tmpdir, user_util):
199 213 repo = user_util.create_repo(repo_type='git')
200 214 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
201 215
202 216 # Damage repo by removing it's folder
203 217 RepoModel()._delete_filesystem_repo(repo)
204 218
205 219 stdout, stderr = Command('/tmp').execute(
206 220 'git clone', clone_url, tmpdir.strpath)
207 221 assert 'not found' in stderr
208 222
209 223 def test_push_new_file_hg(self, rc_web_server, tmpdir):
210 224 clone_url = rc_web_server.repo_clone_url(HG_REPO)
211 225 stdout, stderr = Command('/tmp').execute(
212 226 'hg clone', clone_url, tmpdir.strpath)
213 227
214 228 stdout, stderr = _add_files_and_push(
215 229 'hg', tmpdir.strpath, clone_url=clone_url)
216 230
217 231 assert 'pushing to' in stdout
218 232 assert 'size summary' in stdout
219 233
220 234 def test_push_new_file_git(self, rc_web_server, tmpdir):
221 235 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
222 236 stdout, stderr = Command('/tmp').execute(
223 237 'git clone', clone_url, tmpdir.strpath)
224 238
225 239 # commit some stuff into this repo
226 240 stdout, stderr = _add_files_and_push(
227 241 'git', tmpdir.strpath, clone_url=clone_url)
228 242
229 243 _check_proper_git_push(stdout, stderr)
230 244
231 245 def test_push_invalidates_cache(self, rc_web_server, tmpdir):
232 246 hg_repo = Repository.get_by_repo_name(HG_REPO)
233 247
234 248 # init cache objects
235 249 CacheKey.delete_all_cache()
236 250 cache_namespace_uid = 'cache_push_test.{}'.format(hg_repo.repo_id)
237 251 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
238 252 repo_id=hg_repo.repo_id)
239 253
240 254 inv_context_manager = rc_cache.InvalidationContext(
241 255 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
242 256
243 257 with inv_context_manager as invalidation_context:
244 258 # __enter__ will create and register cache objects
245 259 pass
246 260
247 261 # clone to init cache
248 262 clone_url = rc_web_server.repo_clone_url(hg_repo.repo_name)
249 263 stdout, stderr = Command('/tmp').execute(
250 264 'hg clone', clone_url, tmpdir.strpath)
251 265
252 266 cache_keys = hg_repo.cache_keys
253 267 assert cache_keys != []
254 268 for key in cache_keys:
255 269 assert key.cache_active is True
256 270
257 271 # PUSH that should trigger invalidation cache
258 272 stdout, stderr = _add_files_and_push(
259 273 'hg', tmpdir.strpath, clone_url=clone_url, files_no=1)
260 274
261 275 # flush...
262 276 Session().commit()
263 277 hg_repo = Repository.get_by_repo_name(HG_REPO)
264 278 cache_keys = hg_repo.cache_keys
265 279 assert cache_keys != []
266 280 for key in cache_keys:
267 281 # keys should be marked as not active
268 282 assert key.cache_active is False
269 283
270 284 def test_push_wrong_credentials_hg(self, rc_web_server, tmpdir):
271 285 clone_url = rc_web_server.repo_clone_url(HG_REPO)
272 286 stdout, stderr = Command('/tmp').execute(
273 287 'hg clone', clone_url, tmpdir.strpath)
274 288
275 289 push_url = rc_web_server.repo_clone_url(
276 290 HG_REPO, user='bad', passwd='name')
277 291 stdout, stderr = _add_files_and_push(
278 292 'hg', tmpdir.strpath, clone_url=push_url)
279 293
280 294 assert 'abort: authorization failed' in stderr
281 295
282 296 def test_push_wrong_credentials_git(self, rc_web_server, tmpdir):
283 297 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
284 298 stdout, stderr = Command('/tmp').execute(
285 299 'git clone', clone_url, tmpdir.strpath)
286 300
287 301 push_url = rc_web_server.repo_clone_url(
288 302 GIT_REPO, user='bad', passwd='name')
289 303 stdout, stderr = _add_files_and_push(
290 304 'git', tmpdir.strpath, clone_url=push_url)
291 305
292 306 assert 'fatal: Authentication failed' in stderr
293 307
294 308 def test_push_back_to_wrong_url_hg(self, rc_web_server, tmpdir):
295 309 clone_url = rc_web_server.repo_clone_url(HG_REPO)
296 310 stdout, stderr = Command('/tmp').execute(
297 311 'hg clone', clone_url, tmpdir.strpath)
298 312
299 313 stdout, stderr = _add_files_and_push(
300 314 'hg', tmpdir.strpath,
301 315 clone_url=rc_web_server.repo_clone_url('not-existing'))
302 316
303 317 assert 'HTTP Error 404: Not Found' in stderr
304 318
305 319 def test_push_back_to_wrong_url_git(self, rc_web_server, tmpdir):
306 320 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
307 321 stdout, stderr = Command('/tmp').execute(
308 322 'git clone', clone_url, tmpdir.strpath)
309 323
310 324 stdout, stderr = _add_files_and_push(
311 325 'git', tmpdir.strpath,
312 326 clone_url=rc_web_server.repo_clone_url('not-existing'))
313 327
314 328 assert 'not found' in stderr
315 329
316 330 def test_ip_restriction_hg(self, rc_web_server, tmpdir):
317 331 user_model = UserModel()
318 332 try:
319 333 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
320 334 Session().commit()
321 335 time.sleep(2)
322 336 clone_url = rc_web_server.repo_clone_url(HG_REPO)
323 337 stdout, stderr = Command('/tmp').execute(
324 338 'hg clone', clone_url, tmpdir.strpath)
325 339 assert 'abort: HTTP Error 403: Forbidden' in stderr
326 340 finally:
327 341 # release IP restrictions
328 342 for ip in UserIpMap.getAll():
329 343 UserIpMap.delete(ip.ip_id)
330 344 Session().commit()
331 345
332 346 time.sleep(2)
333 347
334 348 stdout, stderr = Command('/tmp').execute(
335 349 'hg clone', clone_url, tmpdir.strpath)
336 350 _check_proper_clone(stdout, stderr, 'hg')
337 351
338 352 def test_ip_restriction_git(self, rc_web_server, tmpdir):
339 353 user_model = UserModel()
340 354 try:
341 355 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
342 356 Session().commit()
343 357 time.sleep(2)
344 358 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
345 359 stdout, stderr = Command('/tmp').execute(
346 360 'git clone', clone_url, tmpdir.strpath)
347 361 msg = "The requested URL returned error: 403"
348 362 assert msg in stderr
349 363 finally:
350 364 # release IP restrictions
351 365 for ip in UserIpMap.getAll():
352 366 UserIpMap.delete(ip.ip_id)
353 367 Session().commit()
354 368
355 369 time.sleep(2)
356 370
357 371 cmd = Command('/tmp')
358 372 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
359 373 cmd.assert_returncode_success()
360 374 _check_proper_clone(stdout, stderr, 'git')
361 375
362 376 def test_clone_by_auth_token(
363 377 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
364 378 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
365 379 'egg:rhodecode-enterprise-ce#rhodecode'])
366 380
367 381 user = user_util.create_user()
368 382 token = user.auth_tokens[1]
369 383
370 384 clone_url = rc_web_server.repo_clone_url(
371 385 HG_REPO, user=user.username, passwd=token)
372 386
373 387 stdout, stderr = Command('/tmp').execute(
374 388 'hg clone', clone_url, tmpdir.strpath)
375 389 _check_proper_clone(stdout, stderr, 'hg')
376 390
377 391 def test_clone_by_auth_token_expired(
378 392 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
379 393 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
380 394 'egg:rhodecode-enterprise-ce#rhodecode'])
381 395
382 396 user = user_util.create_user()
383 397 auth_token = AuthTokenModel().create(
384 398 user.user_id, 'test-token', -10, AuthTokenModel.cls.ROLE_VCS)
385 399 token = auth_token.api_key
386 400
387 401 clone_url = rc_web_server.repo_clone_url(
388 402 HG_REPO, user=user.username, passwd=token)
389 403
390 404 stdout, stderr = Command('/tmp').execute(
391 405 'hg clone', clone_url, tmpdir.strpath)
392 406 assert 'abort: authorization failed' in stderr
393 407
394 408 def test_clone_by_auth_token_bad_role(
395 409 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
396 410 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
397 411 'egg:rhodecode-enterprise-ce#rhodecode'])
398 412
399 413 user = user_util.create_user()
400 414 auth_token = AuthTokenModel().create(
401 415 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API)
402 416 token = auth_token.api_key
403 417
404 418 clone_url = rc_web_server.repo_clone_url(
405 419 HG_REPO, user=user.username, passwd=token)
406 420
407 421 stdout, stderr = Command('/tmp').execute(
408 422 'hg clone', clone_url, tmpdir.strpath)
409 423 assert 'abort: authorization failed' in stderr
410 424
411 425 def test_clone_by_auth_token_user_disabled(
412 426 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
413 427 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
414 428 'egg:rhodecode-enterprise-ce#rhodecode'])
415 429 user = user_util.create_user()
416 430 user.active = False
417 431 Session().add(user)
418 432 Session().commit()
419 433 token = user.auth_tokens[1]
420 434
421 435 clone_url = rc_web_server.repo_clone_url(
422 436 HG_REPO, user=user.username, passwd=token)
423 437
424 438 stdout, stderr = Command('/tmp').execute(
425 439 'hg clone', clone_url, tmpdir.strpath)
426 440 assert 'abort: authorization failed' in stderr
427 441
428 442 def test_clone_by_auth_token_with_scope(
429 443 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
430 444 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
431 445 'egg:rhodecode-enterprise-ce#rhodecode'])
432 446 user = user_util.create_user()
433 447 auth_token = AuthTokenModel().create(
434 448 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_VCS)
435 449 token = auth_token.api_key
436 450
437 451 # manually set scope
438 452 auth_token.repo = Repository.get_by_repo_name(HG_REPO)
439 453 Session().add(auth_token)
440 454 Session().commit()
441 455
442 456 clone_url = rc_web_server.repo_clone_url(
443 457 HG_REPO, user=user.username, passwd=token)
444 458
445 459 stdout, stderr = Command('/tmp').execute(
446 460 'hg clone', clone_url, tmpdir.strpath)
447 461 _check_proper_clone(stdout, stderr, 'hg')
448 462
449 463 def test_clone_by_auth_token_with_wrong_scope(
450 464 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
451 465 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
452 466 'egg:rhodecode-enterprise-ce#rhodecode'])
453 467 user = user_util.create_user()
454 468 auth_token = AuthTokenModel().create(
455 469 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_VCS)
456 470 token = auth_token.api_key
457 471
458 472 # manually set scope
459 473 auth_token.repo = Repository.get_by_repo_name(GIT_REPO)
460 474 Session().add(auth_token)
461 475 Session().commit()
462 476
463 477 clone_url = rc_web_server.repo_clone_url(
464 478 HG_REPO, user=user.username, passwd=token)
465 479
466 480 stdout, stderr = Command('/tmp').execute(
467 481 'hg clone', clone_url, tmpdir.strpath)
468 482 assert 'abort: authorization failed' in stderr
469 483
General Comments 0
You need to be logged in to leave comments. Login now