test_pagure_repospanner.py 20 KB

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