test_pagure_repospanner.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2015-2018 - Copyright Red Hat Inc
  4. Authors:
  5. Patrick Uiterwijk <puiterwijk@redhat.com>
  6. """
  7. from __future__ import unicode_literals, absolute_import
  8. import datetime
  9. import munch
  10. import unittest
  11. import shutil
  12. import subprocess
  13. import sys
  14. import tempfile
  15. import time
  16. import os
  17. import six
  18. import json
  19. import pygit2
  20. import requests
  21. from requests.adapters import HTTPAdapter
  22. from requests.packages.urllib3.util.retry import Retry
  23. from mock import patch, MagicMock
  24. sys.path.insert(0, os.path.join(os.path.dirname(
  25. os.path.abspath(__file__)), '..'))
  26. import pagure.lib.query
  27. import pagure.cli.admin
  28. import tests
  29. REPOSPANNER_CONFIG_TEMPLATE = """
  30. ---
  31. ca:
  32. path: %(path)s/repospanner/pki
  33. admin:
  34. url: https://localhost.localdomain:%(gitport)s/
  35. ca: %(path)s/repospanner/pki/ca.crt
  36. cert: %(path)s/repospanner/pki/admin.crt
  37. key: %(path)s/repospanner/pki/admin.key
  38. storage:
  39. state: %(path)s/repospanner/state
  40. git:
  41. type: tree
  42. clustered: true
  43. directory: %(path)s/repospanner/git
  44. listen:
  45. rpc: 127.0.0.1:%(rpcport)s
  46. http: 127.0.0.1:%(gitport)s
  47. certificates:
  48. ca: %(path)s/repospanner/pki/ca.crt
  49. client:
  50. cert: %(path)s/repospanner/pki/repospanner.localhost.crt
  51. key: %(path)s/repospanner/pki/repospanner.localhost.key
  52. server:
  53. default:
  54. cert: %(path)s/repospanner/pki/repospanner.localhost.crt
  55. key: %(path)s/repospanner/pki/repospanner.localhost.key
  56. hooks:
  57. debug: true
  58. bubblewrap:
  59. enabled: false
  60. unshare:
  61. - net
  62. - ipc
  63. - pid
  64. - uts
  65. share_net: false
  66. mount_proc: true
  67. mount_dev: true
  68. uid:
  69. gid:
  70. hostname: myhostname
  71. bind:
  72. ro_bind:
  73. - - /usr
  74. - /usr
  75. - - %(codepath)s
  76. - %(codepath)s
  77. - - %(path)s
  78. - %(path)s
  79. - - %(crosspath)s
  80. - %(crosspath)s
  81. symlink:
  82. - - usr/lib64
  83. - /lib64
  84. - - usr/bin
  85. - /bin
  86. runner: %(hookrunner_bin)s
  87. user: 0
  88. """
  89. class PagureRepoSpannerTests(tests.Modeltests):
  90. """ Tests for repoSpanner integration of pagure """
  91. repospanner_binary = None
  92. repospanner_runlog = None
  93. repospanner_proc = None
  94. def run_cacmd(self, logfile, *args):
  95. """ Run a repoSpanner CA command. """
  96. subprocess.check_call(
  97. [self.repospanner_binary,
  98. '--config',
  99. os.path.join(self.path, 'repospanner', 'config.yml'),
  100. # NEVER use this in a production system! It makes repeatable keys
  101. 'ca'] + list(args) + ['--very-insecure-weak-keys'],
  102. stdout=logfile,
  103. stderr=subprocess.STDOUT,
  104. )
  105. def setUp(self):
  106. """ set up the environment. """
  107. possible_paths = [
  108. './repospanner',
  109. '/usr/bin/repospanner',
  110. ]
  111. for option in possible_paths:
  112. option = os.path.abspath(option)
  113. if os.path.exists(option):
  114. self.repospanner_binary = option
  115. break
  116. if not self.repospanner_binary:
  117. raise unittest.SkipTest('repoSpanner not found')
  118. hookrunbins = [
  119. os.path.join(
  120. os.path.dirname(self.repospanner_binary), 'repohookrunner'),
  121. os.path.join('/usr', 'libexec','repohookrunner'),
  122. ]
  123. found = False
  124. for hookrunbin in hookrunbins:
  125. if os.path.exists(hookrunbin):
  126. found = True
  127. break
  128. if not found:
  129. raise Exception('repoSpanner found, but repohookrunner not')
  130. repobridgebins = [
  131. os.path.join(
  132. os.path.dirname(self.repospanner_binary), 'repobridge'),
  133. os.path.join('/usr', 'libexec','repobridge'),
  134. ]
  135. found = False
  136. for repobridgebin in repobridgebins:
  137. if os.path.exists(repobridgebin):
  138. found = True
  139. break
  140. if not found:
  141. raise Exception('repoSpanner found, but repobridge not')
  142. self.config_values['repobridge_binary'] = repobridgebin
  143. codepath = os.path.normpath(
  144. os.path.join(
  145. os.path.dirname(os.path.abspath(__file__)),
  146. "../"))
  147. # Only run the setUp() function if we are actually going ahead and run
  148. # this test. The reason being that otherwise, setUp will set up a
  149. # database, but because we "error out" from setUp, the tearDown()
  150. # function never gets called, leaving it behind.
  151. super(PagureRepoSpannerTests, self).setUp()
  152. # TODO: Find free ports
  153. configvals = {
  154. 'path': self.path,
  155. 'crosspath': tests.tests_state["path"],
  156. 'gitport': 8443 + sys.version_info.major,
  157. 'rpcport': 8445 + sys.version_info.major,
  158. 'codepath': codepath,
  159. 'hookrunner_bin': hookrunbin,
  160. }
  161. os.mkdir(os.path.join(self.path, 'repospanner'))
  162. cfgpath = os.path.join(self.path, 'repospanner', 'config.yml')
  163. with open(cfgpath, 'w') as cfg:
  164. cfg.write(REPOSPANNER_CONFIG_TEMPLATE % configvals)
  165. with open(os.path.join(self.path, 'repospanner', 'keylog'),
  166. 'w') as keylog:
  167. # Create the CA
  168. self.run_cacmd(keylog, 'init', 'localdomain',
  169. '--no-name-constraint', '--random-cn')
  170. # Create the node cert
  171. self.run_cacmd(keylog, 'node', 'localhost', 'repospanner')
  172. # Create the admin cert
  173. self.run_cacmd(keylog, 'leaf', 'admin',
  174. '--admin', '--region', '*', '--repo', '*')
  175. # Create the Pagure cert
  176. self.run_cacmd(keylog, 'leaf', 'pagure',
  177. '--read', '--write',
  178. '--region', '*', '--repo', '*')
  179. with open(os.path.join(self.path, 'repospanner', 'spawnlog'),
  180. 'w') as spawnlog:
  181. # Initialize state
  182. subprocess.check_call(
  183. [self.repospanner_binary,
  184. '--config', cfgpath,
  185. 'serve', '--spawn'],
  186. stdout=spawnlog,
  187. stderr=subprocess.STDOUT,
  188. )
  189. self.repospanner_runlog = open(
  190. os.path.join(self.path, 'repospanner', 'runlog'), 'w')
  191. try:
  192. self.repospanner_proc = subprocess.Popen(
  193. [self.repospanner_binary,
  194. '--config', cfgpath,
  195. 'serve', '--debug'],
  196. stdout=self.repospanner_runlog,
  197. stderr=subprocess.STDOUT,
  198. )
  199. except:
  200. # Make sure to clean up repoSpanner, since we did start it
  201. self.tearDown()
  202. raise
  203. attempts = 0
  204. while True:
  205. try:
  206. # Wait for the instance to become available
  207. resp = requests.get(
  208. 'https://repospanner.localhost.localdomain:%d/'
  209. % configvals['gitport'],
  210. verify=os.path.join(self.path, 'repospanner', 'pki', 'ca.crt'),
  211. cert=(
  212. os.path.join(self.path, 'repospanner', 'pki', 'pagure.crt'),
  213. os.path.join(self.path, 'repospanner', 'pki', 'pagure.key'),
  214. )
  215. )
  216. resp.raise_for_status()
  217. print('repoSpanner identification: %s' % resp.text)
  218. break
  219. except:
  220. if attempts < 5:
  221. attempts += 1
  222. time.sleep(1)
  223. continue
  224. # Make sure to clean up repoSpanner, since we did start it
  225. self.tearDown()
  226. raise
  227. def tearDown(self):
  228. """ Tear down the repoSpanner instance. """
  229. if self.repospanner_proc:
  230. # Tear down
  231. self.repospanner_proc.terminate()
  232. exitcode = self.repospanner_proc.wait()
  233. if exitcode != 0:
  234. print('repoSpanner exit code: %d' % exitcode)
  235. if self.repospanner_runlog:
  236. self.repospanner_runlog.close()
  237. super(PagureRepoSpannerTests, self).tearDown()
  238. class PagureRepoSpannerTestsNewRepoDefault(PagureRepoSpannerTests):
  239. config_values = {
  240. 'repospanner_new_repo': "'default'",
  241. 'authbackend': 'test_auth',
  242. }
  243. @patch('pagure.ui.app.admin_session_timedout')
  244. def test_new_project(self, ast):
  245. """ Test creating a new repo by default on repoSpanner works. """
  246. ast.return_value = False
  247. user = tests.FakeUser(username='foo')
  248. with tests.user_set(self.app.application, user):
  249. output = self.app.get('/new/')
  250. self.assertEqual(output.status_code, 200)
  251. output_text = output.get_data(as_text=True)
  252. self.assertIn(
  253. '<strong>Create new Project</strong>', output_text)
  254. data = {
  255. 'name': 'project-1',
  256. 'description': 'Project #1',
  257. 'create_readme': 'y',
  258. 'csrf_token': self.get_csrf(),
  259. }
  260. output = self.app.post('/new/', data=data, follow_redirects=True)
  261. self.assertEqual(output.status_code, 200)
  262. output_text = output.get_data(as_text=True)
  263. self.assertIn(
  264. '<div class="projectinfo my-3">\nProject #1',
  265. output_text)
  266. self.assertIn(
  267. '<title>Overview - project-1 - Pagure</title>', output_text)
  268. self.assertIn('Added the README', output_text)
  269. output = self.app.get('/project-1/settings')
  270. self.assertIn(
  271. 'This repository is on repoSpanner region default',
  272. output.get_data(as_text=True))
  273. with tests.user_set(self.app.application, tests.FakeUser(username='pingou')):
  274. data = {
  275. 'csrf_token': self.get_csrf(),
  276. }
  277. output = self.app.post(
  278. '/do_fork/project-1', data=data,
  279. follow_redirects=True)
  280. self.assertEqual(output.status_code, 200)
  281. output_text = output.get_data(as_text=True)
  282. self.assertIn(
  283. '<div class="projectinfo my-3">\nProject #1',
  284. output_text)
  285. self.assertIn(
  286. '<title>Overview - project-1 - Pagure</title>', output_text)
  287. self.assertIn('Added the README', output_text)
  288. output = self.app.get('/fork/pingou/project-1/settings')
  289. self.assertIn(
  290. 'This repository is on repoSpanner region default',
  291. output.get_data(as_text=True))
  292. # Verify that only pseudo repos exist, and no on-disk repos got created
  293. repodirlist = os.listdir(os.path.join(self.path, 'repos'))
  294. self.assertEqual(repodirlist, ['pseudo'])
  295. @patch.dict('pagure.config.config', {
  296. 'ALLOW_HTTP_PULL_PUSH': True,
  297. 'ALLOW_HTTP_PUSH': True,
  298. 'HTTP_REPO_ACCESS_GITOLITE': False,
  299. })
  300. def test_http_pull(self):
  301. """ Test that the HTTP pull endpoint works for repoSpanner. """
  302. tests.create_projects(self.session)
  303. tests.create_tokens(self.session)
  304. tests.create_tokens_acl(self.session)
  305. self.create_project_full('clonetest', {"create_readme": "y"})
  306. # Verify the new project is indeed on repoSpanner
  307. project = pagure.lib.query._get_project(self.session, 'clonetest')
  308. self.assertTrue(project.is_on_repospanner)
  309. # Unfortunately, actually testing a git clone would need the app to
  310. # run on a TCP port, which the test environment doesn't do.
  311. output = self.app.get('/clonetest.git/info/refs?service=git-upload-pack')
  312. self.assertEqual(output.status_code, 200)
  313. output_text = output.get_data(as_text=True)
  314. self.assertIn("# service=git-upload-pack", output_text)
  315. self.assertIn("symref=HEAD:refs/heads/master", output_text)
  316. self.assertIn(" refs/heads/master\x00", output_text)
  317. output = self.app.post(
  318. '/clonetest.git/git-upload-pack',
  319. headers={'Content-Type': 'application/x-git-upload-pack-request'},
  320. )
  321. self.assertEqual(output.status_code, 400)
  322. output_text = output.get_data(as_text=True)
  323. self.assertIn("Error processing your request", output_text)
  324. @patch.dict('pagure.config.config', {
  325. 'ALLOW_HTTP_PULL_PUSH': True,
  326. 'ALLOW_HTTP_PUSH': True,
  327. 'HTTP_REPO_ACCESS_GITOLITE': False,
  328. })
  329. def test_http_push(self):
  330. """ Test that the HTTP push endpoint works for repoSpanner. """
  331. tests.create_projects(self.session)
  332. tests.create_tokens(self.session)
  333. tests.create_tokens_acl(self.session)
  334. self.create_project_full('clonetest', {"create_readme": "y"})
  335. # Verify the new project is indeed on repoSpanner
  336. project = pagure.lib.query._get_project(self.session, 'clonetest')
  337. self.assertTrue(project.is_on_repospanner)
  338. # Unfortunately, actually testing a git clone would need the app to
  339. # run on a TCP port, which the test environment doesn't do.
  340. output = self.app.get(
  341. '/clonetest.git/info/refs?service=git-upload-pack',
  342. environ_overrides={'REMOTE_USER': 'pingou'},
  343. )
  344. self.assertEqual(output.status_code, 200)
  345. output_text = output.get_data(as_text=True)
  346. self.assertIn("# service=git-upload-pack", output_text)
  347. self.assertIn("symref=HEAD:refs/heads/master", output_text)
  348. self.assertIn(" refs/heads/master\x00", output_text)
  349. @patch('pagure.ui.app.admin_session_timedout')
  350. def test_hooks(self, ast):
  351. """ Test hook setting and running works. """
  352. ast.return_value = False
  353. pagure.cli.admin.session = self.session
  354. # Upload the hook script to repoSpanner
  355. args = munch.Munch({'region': 'default'})
  356. hookid = pagure.cli.admin.do_upload_repospanner_hooks(args)
  357. user = tests.FakeUser(username='foo')
  358. with tests.user_set(self.app.application, user):
  359. data = {
  360. 'name': 'project-1',
  361. 'description': 'Project #1',
  362. 'create_readme': 'y',
  363. 'csrf_token': self.get_csrf(),
  364. }
  365. output = self.app.post('/new/', data=data, follow_redirects=True)
  366. self.assertEqual(output.status_code, 200)
  367. output_text = output.get_data(as_text=True)
  368. self.assertIn(
  369. '<div class="projectinfo my-3">\nProject #1',
  370. output_text)
  371. self.assertIn(
  372. '<title>Overview - project-1 - Pagure</title>', output_text)
  373. self.assertIn('Added the README', output_text)
  374. output = self.app.get('/project-1/settings')
  375. self.assertIn(
  376. 'This repository is on repoSpanner region default',
  377. output.get_data(as_text=True))
  378. # Check file before the commit:
  379. output = self.app.get('/project-1/raw/master/f/README.md')
  380. self.assertEqual(output.status_code, 200)
  381. output_text = output.get_data(as_text=True)
  382. self.assertEqual(output_text, '# project-1\n\nProject #1')
  383. # Set the hook
  384. args = munch.Munch({'hook': hookid})
  385. projects = pagure.cli.admin.do_ensure_project_hooks(args)
  386. self.assertEqual(["project-1"], projects)
  387. with tests.user_set(self.app.application, user):
  388. # Set editing Denied
  389. self.set_auth_status(False)
  390. # Try to make an edit in the repo
  391. data = {
  392. 'content': 'foo\n bar\n baz',
  393. 'commit_title': 'test commit',
  394. 'commit_message': 'Online commit',
  395. 'email': 'foo@bar.com',
  396. 'branch': 'master',
  397. 'csrf_token': self.get_csrf(),
  398. }
  399. output = self.app.post(
  400. '/project-1/edit/master/f/README.md', data=data,
  401. follow_redirects=True)
  402. self.assertEqual(output.status_code, 200)
  403. output_text = output.get_data(as_text=True)
  404. self.assertIn(
  405. "Remote hook declined the push: ",
  406. output_text
  407. )
  408. self.assertIn(
  409. "Denied push for ref &#39;refs/heads/master&#39; for user &#39;foo&#39;",
  410. output_text
  411. )
  412. # Check file after the commit:
  413. output = self.app.get('/project-1/raw/master/f/README.md')
  414. self.assertEqual(output.status_code, 200)
  415. output_text = output.get_data(as_text=True)
  416. self.assertEqual(output_text, '# project-1\n\nProject #1')
  417. # Set editing Allowed
  418. self.set_auth_status(True)
  419. # Try to make an edit in the repo
  420. data = {
  421. 'content': 'foo\n bar\n baz',
  422. 'commit_title': 'test commit',
  423. 'commit_message': 'Online commit',
  424. 'email': 'foo@bar.com',
  425. 'branch': 'master',
  426. 'csrf_token': self.get_csrf(),
  427. }
  428. output = self.app.post(
  429. '/project-1/edit/master/f/README.md', data=data,
  430. follow_redirects=True)
  431. self.assertEqual(output.status_code, 200)
  432. output_text = output.get_data(as_text=True)
  433. self.assertIn(
  434. '<title>Commits - project-1 - Pagure</title>', output_text)
  435. # Check file after the commit:
  436. output = self.app.get('/project-1/raw/master/f/README.md')
  437. self.assertEqual(output.status_code, 200)
  438. output_text = output.get_data(as_text=True)
  439. self.assertEqual(output_text, 'foo\n bar\n baz')
  440. @patch.dict('pagure.config.config', {'PAGURE_ADMIN_USERS': ['pingou'],
  441. 'ALLOW_ADMIN_IGNORE_EXISTING_REPOS': True})
  442. @patch('pagure.ui.app.admin_session_timedout')
  443. def test_adopt_project(self, ast):
  444. """ Test adopting a project in repoSpanner works. """
  445. ast.return_value = False
  446. user = tests.FakeUser(username='foo')
  447. with tests.user_set(self.app.application, user):
  448. output = self.app.get('/new/')
  449. self.assertEqual(output.status_code, 200)
  450. output_text = output.get_data(as_text=True)
  451. self.assertIn(
  452. '<strong>Create new Project</strong>', output_text)
  453. data = {
  454. 'name': 'project-1',
  455. 'description': 'Project #1',
  456. 'create_readme': 'y',
  457. 'csrf_token': self.get_csrf(),
  458. }
  459. output = self.app.post('/new/', data=data, follow_redirects=True)
  460. self.assertEqual(output.status_code, 200)
  461. output_text = output.get_data(as_text=True)
  462. self.assertIn(
  463. '<div class="projectinfo my-3">\nProject #1',
  464. output_text)
  465. self.assertIn(
  466. '<title>Overview - project-1 - Pagure</title>', output_text)
  467. self.assertIn('Added the README', output_text)
  468. output = self.app.get('/project-1/settings')
  469. self.assertIn(
  470. 'This repository is on repoSpanner region default',
  471. output.get_data(as_text=True))
  472. # Delete the project instance so that the actual repo remains
  473. project = pagure.lib.query._get_project(self.session, 'project-1')
  474. self.session.delete(project)
  475. self.session.commit()
  476. shutil.rmtree(os.path.join(self.path, 'repos', 'pseudo'))
  477. user = tests.FakeUser(username='pingou')
  478. with tests.user_set(self.app.application, user):
  479. output = self.app.get('/project-1/')
  480. self.assertEqual(output.status_code, 404)
  481. data = {
  482. 'name': 'project-1',
  483. 'description': 'Recreated project #1',
  484. 'create_readme': 'false',
  485. 'csrf_token': self.get_csrf(),
  486. }
  487. output = self.app.post('/new/', data=data, follow_redirects=True)
  488. self.assertEqual(output.status_code, 200)
  489. output_text = output.get_data(as_text=True)
  490. self.assertIn(
  491. 'Repo pagure/main/project-1 already exists',
  492. output_text)
  493. data['ignore_existing_repos'] = 'y'
  494. output = self.app.post('/new/', data=data, follow_redirects=True)
  495. self.assertEqual(output.status_code, 200)
  496. output_text = output.get_data(as_text=True)
  497. self.assertIn(
  498. '<div class="projectinfo my-3">\nRecreated project #1',
  499. output_text)
  500. self.assertIn(
  501. '<title>Overview - project-1 - Pagure</title>', output_text)
  502. self.assertIn('Added the README', output_text)
  503. if __name__ == '__main__':
  504. unittest.main(verbosity=2)