__init__.py 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2015-2018 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. """
  7. from __future__ import unicode_literals, absolute_import
  8. import imp
  9. import json
  10. import logging
  11. import os
  12. import re
  13. import resource
  14. import shutil
  15. import subprocess
  16. import sys
  17. import tempfile
  18. import time
  19. import unittest
  20. from io import open, StringIO
  21. logging.basicConfig(stream=sys.stderr)
  22. from bs4 import BeautifulSoup
  23. from contextlib import contextmanager
  24. from datetime import date
  25. from datetime import datetime
  26. from datetime import timedelta
  27. from functools import wraps
  28. from six.moves.urllib.parse import urlparse, parse_qs
  29. import mock
  30. import pygit2
  31. import redis
  32. import six
  33. from bs4 import BeautifulSoup
  34. from celery.app.task import EagerResult
  35. from sqlalchemy import create_engine
  36. from sqlalchemy.orm import sessionmaker
  37. from sqlalchemy.orm import scoped_session
  38. if six.PY2:
  39. # Always enable performance counting for tests
  40. os.environ['PAGURE_PERFREPO'] = 'true'
  41. sys.path.insert(0, os.path.join(os.path.dirname(
  42. os.path.abspath(__file__)), '..'))
  43. import pagure
  44. import pagure.api
  45. from pagure.api.ci import jenkins
  46. import pagure.flask_app
  47. import pagure.lib.git
  48. import pagure.lib.model
  49. import pagure.lib.query
  50. import pagure.lib.tasks_mirror
  51. import pagure.perfrepo as perfrepo
  52. from pagure.config import config as pagure_config, reload_config
  53. from pagure.lib.repo import PagureRepo
  54. HERE = os.path.join(os.path.dirname(os.path.abspath(__file__)))
  55. LOG = logging.getLogger(__name__)
  56. LOG.setLevel(logging.INFO)
  57. PAGLOG = logging.getLogger('pagure')
  58. PAGLOG.setLevel(logging.CRITICAL)
  59. PAGLOG.handlers = []
  60. if 'PYTHONPATH' not in os.environ:
  61. os.environ['PYTHONPATH'] = os.path.normpath(os.path.join(HERE, '../'))
  62. CONFIG_TEMPLATE = """
  63. GIT_FOLDER = '%(path)s/repos'
  64. ENABLE_DOCS = %(enable_docs)s
  65. ENABLE_TICKETS = %(enable_tickets)s
  66. REMOTE_GIT_FOLDER = '%(path)s/remotes'
  67. DB_URL = '%(dburl)s'
  68. ALLOW_PROJECT_DOWAIT = True
  69. PAGURE_CI_SERVICES = ['jenkins']
  70. EMAIL_SEND = False
  71. TESTING = True
  72. GIT_FOLDER = '%(path)s/repos'
  73. REQUESTS_FOLDER = '%(path)s/repos/requests'
  74. TICKETS_FOLDER = %(tickets_folder)r
  75. DOCS_FOLDER = %(docs_folder)r
  76. REPOSPANNER_PSEUDO_FOLDER = '%(path)s/repos/pseudo'
  77. ATTACHMENTS_FOLDER = '%(path)s/attachments'
  78. BROKER_URL = 'redis+socket://%(global_path)s/broker'
  79. CELERY_CONFIG = {
  80. "task_always_eager": True,
  81. #"task_eager_propagates": True,
  82. }
  83. GIT_AUTH_BACKEND = '%(authbackend)s'
  84. TEST_AUTH_STATUS = '%(path)s/testauth_status.json'
  85. REPOBRIDGE_BINARY = '%(repobridge_binary)s'
  86. REPOSPANNER_NEW_REPO = %(repospanner_new_repo)s
  87. REPOSPANNER_NEW_REPO_ADMIN_OVERRIDE = %(repospanner_admin_override)s
  88. REPOSPANNER_NEW_FORK = %(repospanner_new_fork)s
  89. REPOSPANNER_ADMIN_MIGRATION = %(repospanner_admin_migration)s
  90. REPOSPANNER_REGIONS = {
  91. 'default': {'url': 'https://repospanner.localhost.localdomain:%(repospanner_gitport)s',
  92. 'repo_prefix': 'pagure/',
  93. 'hook': None,
  94. 'ca': '%(path)s/repospanner/pki/ca.crt',
  95. 'admin_cert': {'cert': '%(path)s/repospanner/pki/admin.crt',
  96. 'key': '%(path)s/repospanner/pki/admin.key'},
  97. 'push_cert': {'cert': '%(path)s/repospanner/pki/pagure.crt',
  98. 'key': '%(path)s/repospanner/pki/pagure.key'}}
  99. }
  100. """
  101. # The Celery docs warn against using task_always_eager:
  102. # http://docs.celeryproject.org/en/latest/userguide/testing.html
  103. # but that warning is only valid when testing the async nature of the task, not
  104. # what the task actually does.
  105. LOG.info('BUILD_ID: %s', os.environ.get('BUILD_ID'))
  106. WAIT_REGEX = re.compile(r"""var _url = '(\/wait\/[a-z0-9-]+\??.*)'""")
  107. def get_wait_target(html):
  108. """ This parses the window.location out of the HTML for the wait page. """
  109. found = WAIT_REGEX.findall(html)
  110. if len(found) == 0:
  111. raise Exception("Not able to get wait target in %s" % html)
  112. return found[-1]
  113. def get_post_target(html):
  114. """ This parses the wait page form to get the POST url. """
  115. soup = BeautifulSoup(html, 'html.parser')
  116. form = soup.find(id='waitform')
  117. if not form:
  118. raise Exception("Not able to get the POST url in %s" % html)
  119. return form.get('action')
  120. def get_post_args(html):
  121. """ This parses the wait page for the hidden arguments of the form. """
  122. soup = BeautifulSoup(html, 'html.parser')
  123. output = {}
  124. inputs = soup.find_all('input')
  125. if not inputs:
  126. raise Exception("Not able to get the POST arguments in %s" % html)
  127. for inp in inputs:
  128. if inp.get('type') == 'hidden':
  129. output[inp.get('name')] = inp.get('value')
  130. return output
  131. def create_maybe_waiter(method, getter):
  132. def maybe_waiter(*args, **kwargs):
  133. """ A wrapper for self.app.get()/.post() that will resolve wait's """
  134. result = method(*args, **kwargs)
  135. # Handle the POST wait case
  136. form_url = None
  137. form_args = None
  138. try:
  139. result_text = result.get_data(as_text=True)
  140. except UnicodeDecodeError:
  141. return result
  142. if 'id="waitform"' in result_text:
  143. form_url = get_post_target(result_text)
  144. form_args = get_post_args(result_text)
  145. form_args['csrf_token'] = result_text.split(
  146. 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
  147. count = 0
  148. while 'We are waiting for your task to finish.' in result_text:
  149. # Resolve wait page
  150. target_url = get_wait_target(result_text)
  151. if count > 10:
  152. time.sleep(0.5)
  153. else:
  154. time.sleep(0.1)
  155. result = getter(target_url, follow_redirects=True)
  156. try:
  157. result_text = result.get_data(as_text=True)
  158. except UnicodeDecodeError:
  159. return result
  160. if count > 50:
  161. raise Exception('Had to wait too long')
  162. else:
  163. if form_url and form_args:
  164. return method(form_url, data=form_args, follow_redirects=True)
  165. return result
  166. return maybe_waiter
  167. @contextmanager
  168. def user_set(APP, user, keep_get_user=False):
  169. """ Set the provided user as fas_user in the provided application."""
  170. # Hack used to remove the before_request function set by
  171. # flask.ext.fas_openid.FAS which otherwise kills our effort to set a
  172. # flask.g.fas_user.
  173. from flask import appcontext_pushed, g
  174. keep = []
  175. for meth in APP.before_request_funcs[None]:
  176. if 'flask_fas_openid.FAS' in str(meth):
  177. continue
  178. keep.append(meth)
  179. APP.before_request_funcs[None] = keep
  180. def handler(sender, **kwargs):
  181. g.fas_user = user
  182. g.fas_session_id = b'123'
  183. g.authenticated = True
  184. old_get_user = pagure.flask_app._get_user
  185. if not keep_get_user:
  186. pagure.flask_app._get_user = mock.MagicMock(
  187. return_value=pagure.lib.model.User())
  188. with appcontext_pushed.connected_to(handler, APP):
  189. yield
  190. pagure.flask_app._get_user = old_get_user
  191. tests_state = {
  192. "path": tempfile.mkdtemp(prefix='pagure-tests-'),
  193. "broker": None,
  194. "broker_client": None,
  195. "results": {},
  196. }
  197. def _populate_db(session):
  198. # Create a couple of users
  199. item = pagure.lib.model.User(
  200. user='pingou',
  201. fullname='PY C',
  202. password=b'foo',
  203. default_email='bar@pingou.com',
  204. )
  205. session.add(item)
  206. item = pagure.lib.model.UserEmail(
  207. user_id=1,
  208. email='bar@pingou.com')
  209. session.add(item)
  210. item = pagure.lib.model.UserEmail(
  211. user_id=1,
  212. email='foo@pingou.com')
  213. session.add(item)
  214. item = pagure.lib.model.User(
  215. user='foo',
  216. fullname='foo bar',
  217. password=b'foo',
  218. default_email='foo@bar.com',
  219. )
  220. session.add(item)
  221. item = pagure.lib.model.UserEmail(
  222. user_id=2,
  223. email='foo@bar.com')
  224. session.add(item)
  225. session.commit()
  226. def store_eager_results(*args, **kwargs):
  227. """A wrapper for EagerResult that stores the instance."""
  228. result = EagerResult(*args, **kwargs)
  229. tests_state["results"][result.id] = result
  230. return result
  231. def setUp():
  232. # In order to save time during local test execution, we create sqlite DB
  233. # file only once and then we populate it and empty it for every test case
  234. # (as opposed to creating DB file for every test case).
  235. session = pagure.lib.model.create_tables(
  236. 'sqlite:///%s/db.sqlite' % tests_state["path"],
  237. acls=pagure_config.get('ACLS', {}),
  238. )
  239. tests_state["db_session"] = session
  240. # Create a broker
  241. broker_url = os.path.join(tests_state["path"], 'broker')
  242. tests_state["broker"] = broker = subprocess.Popen(
  243. ['/usr/bin/redis-server', '--unixsocket', broker_url, '--port',
  244. '0', '--loglevel', 'warning', '--logfile', '/dev/null'],
  245. stdout=None, stderr=None)
  246. broker.poll()
  247. if broker.returncode is not None:
  248. raise Exception('Broker failed to start')
  249. tests_state["broker_client"] = redis.Redis(unix_socket_path=broker_url)
  250. # Store the EagerResults to be able to retrieve them later
  251. tests_state["eg_patcher"] = mock.patch('celery.app.task.EagerResult')
  252. eg_mock = tests_state["eg_patcher"].start()
  253. eg_mock.side_effect = store_eager_results
  254. def tearDown():
  255. tests_state["db_session"].close()
  256. tests_state["eg_patcher"].stop()
  257. broker = tests_state["broker"]
  258. broker.kill()
  259. broker.wait()
  260. shutil.rmtree(tests_state["path"])
  261. class SimplePagureTest(unittest.TestCase):
  262. """
  263. Simple Test class that does not set a broker/worker
  264. """
  265. populate_db = True
  266. config_values = {}
  267. @mock.patch('pagure.lib.notify.fedmsg_publish', mock.MagicMock())
  268. def __init__(self, method_name='runTest'):
  269. """ Constructor. """
  270. unittest.TestCase.__init__(self, method_name)
  271. self.session = None
  272. self.path = None
  273. self.gitrepo = None
  274. self.gitrepos = None
  275. def perfMaxWalks(self, max_walks, max_steps):
  276. """ Check that we have not performed too many walks/steps. """
  277. num_walks = 0
  278. num_steps = 0
  279. for reqstat in perfrepo.REQUESTS:
  280. for walk in reqstat['walks'].values():
  281. num_walks += 1
  282. num_steps += walk['steps']
  283. self.assertLessEqual(num_walks, max_walks,
  284. '%s git repo walks performed, at most %s allowed'
  285. % (num_walks, max_walks))
  286. self.assertLessEqual(num_steps, max_steps,
  287. '%s git repo steps performed, at most %s allowed'
  288. % (num_steps, max_steps))
  289. def perfReset(self):
  290. """ Reset perfrepo stats. """
  291. perfrepo.reset_stats()
  292. perfrepo.REQUESTS = []
  293. def setUp(self):
  294. if self.path:
  295. # This prevents test state leakage.
  296. # This should be None if the previous runs' tearDown didn't finish,
  297. # leaving behind a self.path.
  298. # If we continue in this case, not only did the previous worker and
  299. # redis instances not exit, we also might accidentally use the
  300. # old database connection.
  301. # @pingou, don't delete this again... :)
  302. raise Exception('Previous test failed!')
  303. self.perfReset()
  304. self.path = tempfile.mkdtemp(prefix='pagure-tests-path-')
  305. LOG.debug('Testdir: %s', self.path)
  306. for folder in ['repos', 'forks', 'releases', 'remotes', 'attachments']:
  307. os.mkdir(os.path.join(self.path, folder))
  308. if hasattr(pagure.lib.query, 'REDIS') and pagure.lib.query.REDIS:
  309. pagure.lib.query.REDIS.connection_pool.disconnect()
  310. pagure.lib.query.REDIS = None
  311. # Database
  312. self._prepare_db()
  313. # Write a config file
  314. config_values = {
  315. 'path': self.path,
  316. 'dburl': self.dbpath,
  317. 'enable_docs': True,
  318. 'docs_folder': '%s/repos/docs' % self.path,
  319. 'enable_tickets': True,
  320. 'tickets_folder': '%s/repos/tickets' % self.path,
  321. 'global_path': tests_state["path"],
  322. 'authbackend': 'gitolite3',
  323. 'repobridge_binary': '/usr/libexec/repobridge',
  324. 'repospanner_gitport': str(8443 + sys.version_info.major),
  325. 'repospanner_new_repo': 'None',
  326. 'repospanner_admin_override': 'False',
  327. 'repospanner_new_fork': 'True',
  328. 'repospanner_admin_migration': 'False',
  329. }
  330. config_values.update(self.config_values)
  331. self.config_values = config_values
  332. config_path = os.path.join(self.path, 'config')
  333. if not os.path.exists(config_path):
  334. with open(config_path, 'w') as f:
  335. f.write(CONFIG_TEMPLATE % self.config_values)
  336. os.environ["PAGURE_CONFIG"] = config_path
  337. pagure_config.update(reload_config())
  338. imp.reload(pagure.lib.tasks)
  339. imp.reload(pagure.lib.tasks_mirror)
  340. imp.reload(pagure.lib.tasks_services)
  341. self._app = pagure.flask_app.create_app({'DB_URL': self.dbpath})
  342. self.app = self._app.test_client()
  343. self.gr_patcher = mock.patch('pagure.lib.tasks.get_result')
  344. gr_mock = self.gr_patcher.start()
  345. gr_mock.side_effect = lambda tid: tests_state["results"][tid]
  346. # Refresh the DB session
  347. self.session = pagure.lib.query.create_session(self.dbpath)
  348. def tearDown(self):
  349. self.gr_patcher.stop()
  350. self.session.rollback()
  351. self._clear_database()
  352. # Remove testdir
  353. try:
  354. shutil.rmtree(self.path)
  355. except:
  356. # Sometimes there is a race condition that makes deleting the folder
  357. # fail during the first attempt. So just try a second time if that's
  358. # the case.
  359. shutil.rmtree(self.path)
  360. self.path = None
  361. del self.app
  362. del self._app
  363. def shortDescription(self):
  364. doc = self.__str__() + ": " + self._testMethodDoc
  365. return doc or None
  366. def _prepare_db(self):
  367. self.dbpath = 'sqlite:///%s' % os.path.join(
  368. tests_state["path"], 'db.sqlite')
  369. self.session = tests_state["db_session"]
  370. pagure.lib.model.create_default_status(
  371. self.session, acls=pagure_config.get('ACLS', {}))
  372. if self.populate_db:
  373. _populate_db(self.session)
  374. def _clear_database(self):
  375. tables = reversed(pagure.lib.model_base.BASE.metadata.sorted_tables)
  376. if self.dbpath.startswith('postgresql'):
  377. self.session.execute("TRUNCATE %s CASCADE" % ", ".join(
  378. [t.name for t in tables]))
  379. elif self.dbpath.startswith('sqlite'):
  380. for table in tables:
  381. self.session.execute("DELETE FROM %s" % table.name)
  382. elif self.dbpath.startswith('mysql'):
  383. self.session.execute("SET FOREIGN_KEY_CHECKS = 0")
  384. for table in tables:
  385. self.session.execute("TRUNCATE %s" % table.name)
  386. self.session.execute("SET FOREIGN_KEY_CHECKS = 1")
  387. self.session.commit()
  388. def set_auth_status(self, value):
  389. """ Set the return value for the test auth """
  390. with open(os.path.join(self.path, 'testauth_status.json'), 'w') as statusfile:
  391. statusfile.write(six.u(json.dumps(value)))
  392. def get_csrf(self, url='/new', output=None):
  393. """Retrieve a CSRF token from given URL."""
  394. if output is None:
  395. output = self.app.get(url)
  396. self.assertEqual(output.status_code, 200)
  397. return output.get_data(as_text=True).split(
  398. 'name="csrf_token" type="hidden" value="')[1].split('">')[0]
  399. def get_wtforms_version(self):
  400. """Returns the wtforms version as a tuple."""
  401. import wtforms
  402. wtforms_v = wtforms.__version__.split('.')
  403. for idx, val in enumerate(wtforms_v):
  404. try:
  405. val = int(val)
  406. except ValueError:
  407. pass
  408. wtforms_v[idx] = val
  409. return tuple(wtforms_v)
  410. def assertURLEqual(self, url_1, url_2):
  411. url_parsed_1 = list(urlparse(url_1))
  412. url_parsed_1[4] = parse_qs(url_parsed_1[4])
  413. url_parsed_2 = list(urlparse(url_2))
  414. url_parsed_2[4] = parse_qs(url_parsed_2[4])
  415. return self.assertListEqual(url_parsed_1, url_parsed_2)
  416. def assertJSONEqual(self, json_1, json_2):
  417. return self.assertEqual(json.loads(json_1), json.loads(json_2))
  418. class Modeltests(SimplePagureTest):
  419. """ Model tests. """
  420. def setUp(self): # pylint: disable=invalid-name
  421. """ Set up the environnment, ran before every tests. """
  422. # Clean up test performance info
  423. super(Modeltests, self).setUp()
  424. self.app.get = create_maybe_waiter(self.app.get, self.app.get)
  425. self.app.post = create_maybe_waiter(self.app.post, self.app.get)
  426. # Refresh the DB session
  427. self.session = pagure.lib.query.create_session(self.dbpath)
  428. def tearDown(self): # pylint: disable=invalid-name
  429. """ Remove the test.db database if there is one. """
  430. tests_state["broker_client"].flushall()
  431. super(Modeltests, self).tearDown()
  432. def create_project_full(self, projectname, extra=None):
  433. """ Create a project via the API.
  434. This makes sure that the repo is fully setup the way a normal new
  435. project would be, with hooks and all setup.
  436. """
  437. headers = {'Authorization': 'token aaabbbcccddd'}
  438. data = {
  439. 'name': projectname,
  440. 'description': 'A test repo',
  441. }
  442. if extra:
  443. data.update(extra)
  444. # Valid request
  445. output = self.app.post(
  446. '/api/0/new/', data=data, headers=headers)
  447. self.assertEqual(output.status_code, 200)
  448. data = json.loads(output.get_data(as_text=True))
  449. self.assertDictEqual(
  450. data,
  451. {'message': 'Project "%s" created' % projectname}
  452. )
  453. class FakeGroup(object): # pylint: disable=too-few-public-methods
  454. """ Fake object used to make the FakeUser object closer to the
  455. expectations.
  456. """
  457. def __init__(self, name):
  458. """ Constructor.
  459. :arg name: the name given to the name attribute of this object.
  460. """
  461. self.name = name
  462. self.group_type = 'cla'
  463. class FakeUser(object): # pylint: disable=too-few-public-methods
  464. """ Fake user used to test the fedocallib library. """
  465. def __init__(self, groups=None, username='username', cla_done=True, id=None):
  466. """ Constructor.
  467. :arg groups: list of the groups in which this fake user is
  468. supposed to be.
  469. """
  470. if isinstance(groups, six.string_types):
  471. groups = [groups]
  472. self.id = id
  473. self.groups = groups or []
  474. self.user = username
  475. self.username = username
  476. self.name = username
  477. self.email = 'foo@bar.com'
  478. self.default_email = 'foo@bar.com'
  479. self.approved_memberships = [
  480. FakeGroup('packager'),
  481. FakeGroup('design-team')
  482. ]
  483. self.dic = {}
  484. self.dic['timezone'] = 'Europe/Paris'
  485. self.login_time = datetime.utcnow()
  486. self.cla_done = cla_done
  487. def __getitem__(self, key):
  488. return self.dic[key]
  489. def create_locks(session, project):
  490. for ltype in ('WORKER', 'WORKER_TICKET', 'WORKER_REQUEST'):
  491. lock = pagure.lib.model.ProjectLock(
  492. project_id=project.id,
  493. lock_type=ltype)
  494. session.add(lock)
  495. def create_projects(session, is_fork=False, user_id=1, hook_token_suffix=''):
  496. """ Create some projects in the database. """
  497. item = pagure.lib.model.Project(
  498. user_id=user_id, # pingou
  499. name='test',
  500. is_fork=is_fork,
  501. parent_id=1 if is_fork else None,
  502. description='test project #1',
  503. hook_token='aaabbbccc' + hook_token_suffix,
  504. )
  505. item.close_status = ['Invalid', 'Insufficient data', 'Fixed', 'Duplicate']
  506. session.add(item)
  507. session.flush()
  508. create_locks(session, item)
  509. item = pagure.lib.model.Project(
  510. user_id=user_id, # pingou
  511. name='test2',
  512. is_fork=is_fork,
  513. parent_id=2 if is_fork else None,
  514. description='test project #2',
  515. hook_token='aaabbbddd' + hook_token_suffix,
  516. )
  517. item.close_status = ['Invalid', 'Insufficient data', 'Fixed', 'Duplicate']
  518. session.add(item)
  519. session.flush()
  520. create_locks(session, item)
  521. item = pagure.lib.model.Project(
  522. user_id=user_id, # pingou
  523. name='test3',
  524. is_fork=is_fork,
  525. parent_id=3 if is_fork else None,
  526. description='namespaced test project',
  527. hook_token='aaabbbeee' + hook_token_suffix,
  528. namespace='somenamespace',
  529. )
  530. item.close_status = ['Invalid', 'Insufficient data', 'Fixed', 'Duplicate']
  531. session.add(item)
  532. session.flush()
  533. create_locks(session, item)
  534. session.commit()
  535. def create_projects_git(folder, bare=False):
  536. """ Create some projects in the database. """
  537. repos = []
  538. for project in ['test.git', 'test2.git',
  539. os.path.join('somenamespace', 'test3.git')]:
  540. repo_path = os.path.join(folder, project)
  541. repos.append(repo_path)
  542. if not os.path.exists(repo_path):
  543. os.makedirs(repo_path)
  544. pygit2.init_repository(repo_path, bare=bare)
  545. return repos
  546. def create_tokens(session, user_id=1, project_id=1):
  547. """ Create some tokens for the project in the database. """
  548. item = pagure.lib.model.Token(
  549. id='aaabbbcccddd',
  550. user_id=user_id,
  551. project_id=project_id,
  552. expiration=datetime.utcnow() + timedelta(days=30)
  553. )
  554. session.add(item)
  555. item = pagure.lib.model.Token(
  556. id='foo_token',
  557. user_id=user_id,
  558. project_id=project_id,
  559. expiration=datetime.utcnow() + timedelta(days=30)
  560. )
  561. session.add(item)
  562. item = pagure.lib.model.Token(
  563. id='expired_token',
  564. user_id=user_id,
  565. project_id=project_id,
  566. expiration=datetime.utcnow() - timedelta(days=1)
  567. )
  568. session.add(item)
  569. session.commit()
  570. def create_tokens_acl(session, token_id='aaabbbcccddd', acl_name=None):
  571. """ Create some ACLs for the token. If acl_name is not set, the token will
  572. have all the ACLs enabled.
  573. """
  574. if acl_name is None:
  575. for aclid in range(len(pagure_config['ACLS'])):
  576. token_acl = pagure.lib.model.TokenAcl(
  577. token_id=token_id,
  578. acl_id=aclid + 1,
  579. )
  580. session.add(token_acl)
  581. else:
  582. acl = session.query(pagure.lib.model.ACL).filter_by(
  583. name=acl_name).one()
  584. token_acl = pagure.lib.model.TokenAcl(
  585. token_id=token_id,
  586. acl_id=acl.id,
  587. )
  588. session.add(token_acl)
  589. session.commit()
  590. def _clone_and_top_commits(folder, branch, branch_ref=False):
  591. """ Clone the repository, checkout the specified branch and return
  592. the top commit of that branch if there is one.
  593. Returns the repo, the path to the clone and the top commit(s) in a tuple
  594. or the repo, the path to the clone and the reference to the branch
  595. object if branch_ref is True.
  596. """
  597. if not os.path.exists(folder):
  598. os.makedirs(folder)
  599. brepo = pygit2.init_repository(folder, bare=True)
  600. newfolder = tempfile.mkdtemp(prefix='pagure-tests')
  601. repo = pygit2.clone_repository(folder, newfolder)
  602. branch_ref_obj = None
  603. if "origin/%s" % branch in repo.listall_branches(pygit2.GIT_BRANCH_ALL):
  604. branch_ref_obj = pagure.lib.git.get_branch_ref(repo, branch)
  605. repo.checkout(branch_ref_obj)
  606. if branch_ref:
  607. return (repo, newfolder, branch_ref_obj)
  608. parents = []
  609. commit = None
  610. try:
  611. if branch_ref_obj:
  612. commit = repo[branch_ref_obj.peel().hex]
  613. else:
  614. commit = repo.revparse_single('HEAD')
  615. except KeyError:
  616. pass
  617. if commit:
  618. parents = [commit.oid.hex]
  619. return (repo, newfolder, parents)
  620. def add_content_git_repo(folder, branch='master', append=None):
  621. """ Create some content for the specified git repo. """
  622. repo, newfolder, parents = _clone_and_top_commits(folder, branch)
  623. # Create a file in that git repo
  624. filename = os.path.join(newfolder, 'sources')
  625. content = 'foo\n bar'
  626. if os.path.exists(filename):
  627. content = 'foo\n bar\nbaz'
  628. if append:
  629. content += append
  630. with open(filename, 'w') as stream:
  631. stream.write(content)
  632. repo.index.add('sources')
  633. repo.index.write()
  634. # Commits the files added
  635. tree = repo.index.write_tree()
  636. author = pygit2.Signature(
  637. 'Alice Author', 'alice@authors.tld')
  638. committer = pygit2.Signature(
  639. 'Cecil Committer', 'cecil@committers.tld')
  640. commit = repo.create_commit(
  641. 'refs/heads/%s' % branch, # the name of the reference to update
  642. author,
  643. committer,
  644. 'Add sources file for testing',
  645. # binary string representing the tree object ID
  646. tree,
  647. # list of binary strings representing parents of the new commit
  648. parents,
  649. )
  650. if commit:
  651. parents = [commit.hex]
  652. subfolder = os.path.join('folder1', 'folder2')
  653. if not os.path.exists(os.path.join(newfolder, subfolder)):
  654. os.makedirs(os.path.join(newfolder, subfolder))
  655. # Create a file in that git repo
  656. with open(os.path.join(newfolder, subfolder, 'file'), 'w') as stream:
  657. stream.write('foo\n bar\nbaz')
  658. repo.index.add(os.path.join(subfolder, 'file'))
  659. with open(os.path.join(newfolder, subfolder, u'fileŠ'), 'w') as stream:
  660. stream.write('foo\n bar\nbaz')
  661. repo.index.add(os.path.join(subfolder, u'fileŠ'))
  662. repo.index.write()
  663. # Commits the files added
  664. tree = repo.index.write_tree()
  665. author = pygit2.Signature(
  666. 'Alice Author', 'alice@authors.tld')
  667. committer = pygit2.Signature(
  668. 'Cecil Committer', 'cecil@committers.tld')
  669. commit =repo.create_commit(
  670. 'refs/heads/%s' % branch, # the name of the reference to update
  671. author,
  672. committer,
  673. 'Add some directory and a file for more testing',
  674. # binary string representing the tree object ID
  675. tree,
  676. # list of binary strings representing parents of the new commit
  677. parents
  678. )
  679. # Push to origin
  680. ori_remote = repo.remotes[0]
  681. master_ref = repo.lookup_reference(
  682. 'HEAD' if branch == 'master' else 'refs/heads/%s' % branch).resolve()
  683. refname = '%s:%s' % (master_ref.name, master_ref.name)
  684. PagureRepo.push(ori_remote, refname)
  685. shutil.rmtree(newfolder)
  686. def add_readme_git_repo(folder, readme_name='README.rst', branch='master'):
  687. """ Create a README file for the specified git repo. """
  688. repo, newfolder, parents = _clone_and_top_commits(folder, branch)
  689. if readme_name == 'README.rst':
  690. content = """Pagure
  691. ======
  692. :Author: Pierre-Yves Chibon <pingou@pingoured.fr>
  693. Pagure is a light-weight git-centered forge based on pygit2.
  694. Currently, Pagure offers a web-interface for git repositories, a ticket
  695. system and possibilities to create new projects, fork existing ones and
  696. create/merge pull-requests across or within projects.
  697. Homepage: https://github.com/pypingou/pagure
  698. Dev instance: http://209.132.184.222/ (/!\\ May change unexpectedly, it's a dev instance ;-))
  699. """
  700. else:
  701. content = """Pagure
  702. ======
  703. This is a placeholder """ + readme_name + """
  704. that should never get displayed on the website if there is a README.rst in the repo.
  705. """
  706. # Create a file in that git repo
  707. with open(os.path.join(newfolder, readme_name), 'w') as stream:
  708. stream.write(content)
  709. repo.index.add(readme_name)
  710. repo.index.write()
  711. # Commits the files added
  712. tree = repo.index.write_tree()
  713. author = pygit2.Signature(
  714. 'Alice Author', 'alice@authors.tld')
  715. committer = pygit2.Signature(
  716. 'Cecil Committer', 'cecil@committers.tld')
  717. branch_ref = "refs/heads/%s" % branch
  718. repo.create_commit(
  719. branch_ref, # the name of the reference to update
  720. author,
  721. committer,
  722. 'Add a README file',
  723. # binary string representing the tree object ID
  724. tree,
  725. # list of binary strings representing parents of the new commit
  726. parents
  727. )
  728. # Push to origin
  729. ori_remote = repo.remotes[0]
  730. PagureRepo.push(ori_remote, '%s:%s' % (branch_ref, branch_ref))
  731. shutil.rmtree(newfolder)
  732. def add_commit_git_repo(folder, ncommits=10, filename='sources',
  733. branch='master'):
  734. """ Create some more commits for the specified git repo. """
  735. repo, newfolder, branch_ref_obj = _clone_and_top_commits(
  736. folder, branch, branch_ref=True)
  737. for index in range(ncommits):
  738. # Create a file in that git repo
  739. with open(os.path.join(newfolder, filename), 'a') as stream:
  740. stream.write('Row %s\n' % index)
  741. repo.index.add(filename)
  742. repo.index.write()
  743. parents = []
  744. commit = None
  745. try:
  746. if branch_ref_obj:
  747. commit = repo[branch_ref_obj.peel().hex]
  748. else:
  749. commit = repo.revparse_single('HEAD')
  750. except (KeyError, AttributeError):
  751. pass
  752. if commit:
  753. parents = [commit.oid.hex]
  754. # Commits the files added
  755. tree = repo.index.write_tree()
  756. author = pygit2.Signature(
  757. 'Alice Author', 'alice@authors.tld')
  758. committer = pygit2.Signature(
  759. 'Cecil Committer', 'cecil@committers.tld')
  760. branch_ref = "refs/heads/%s" % branch
  761. repo.create_commit(
  762. branch_ref,
  763. author,
  764. committer,
  765. 'Add row %s to %s file' % (index, filename),
  766. # binary string representing the tree object ID
  767. tree,
  768. # list of binary strings representing parents of the new commit
  769. parents,
  770. )
  771. branch_ref_obj = pagure.lib.git.get_branch_ref(repo, branch)
  772. # Push to origin
  773. ori_remote = repo.remotes[0]
  774. PagureRepo.push(ori_remote, '%s:%s' % (branch_ref, branch_ref))
  775. shutil.rmtree(newfolder)
  776. def add_content_to_git(
  777. folder, branch='master', filename='sources', content='foo',
  778. message=None):
  779. """ Create some more commits for the specified git repo. """
  780. repo, newfolder, branch_ref_obj = _clone_and_top_commits(
  781. folder, branch, branch_ref=True)
  782. # Create a file in that git repo
  783. with open(os.path.join(newfolder, filename), 'a', encoding="utf-8") as stream:
  784. stream.write('%s\n' % content)
  785. repo.index.add(filename)
  786. repo.index.write()
  787. parents = []
  788. commit = None
  789. try:
  790. if branch_ref_obj:
  791. commit = repo[branch_ref_obj.peel().hex]
  792. else:
  793. commit = repo.revparse_single('HEAD')
  794. except (KeyError, AttributeError):
  795. pass
  796. if commit:
  797. parents = [commit.oid.hex]
  798. # Commits the files added
  799. tree = repo.index.write_tree()
  800. author = pygit2.Signature(
  801. 'Alice Author', 'alice@authors.tld')
  802. committer = pygit2.Signature(
  803. 'Cecil Committer', 'cecil@committers.tld')
  804. branch_ref = "refs/heads/%s" % branch
  805. message = message or 'Add content to file %s' % (filename)
  806. repo.create_commit(
  807. branch_ref, # the name of the reference to update
  808. author,
  809. committer,
  810. message,
  811. # binary string representing the tree object ID
  812. tree,
  813. # list of binary strings representing parents of the new commit
  814. parents,
  815. )
  816. # Push to origin
  817. ori_remote = repo.remotes[0]
  818. PagureRepo.push(ori_remote, '%s:%s' % (branch_ref, branch_ref))
  819. shutil.rmtree(newfolder)
  820. def add_binary_git_repo(folder, filename):
  821. """ Create a fake image file for the specified git repo. """
  822. repo, newfolder, parents = _clone_and_top_commits(folder, 'master')
  823. content = b"""\x00\x00\x01\x00\x01\x00\x18\x18\x00\x00\x01\x00 \x00\x88
  824. \t\x00\x00\x16\x00\x00\x00(\x00\x00\x00\x18\x00x00\x00\x01\x00 \x00\x00\x00
  825. \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
  826. 00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa7lM\x01\xa6kM\t\xa6kM\x01
  827. \xa4fF\x04\xa2dE\x95\xa2cD8\xa1a
  828. """
  829. # Create a file in that git repo
  830. with open(os.path.join(newfolder, filename), 'wb') as stream:
  831. stream.write(content)
  832. repo.index.add(filename)
  833. repo.index.write()
  834. # Commits the files added
  835. tree = repo.index.write_tree()
  836. author = pygit2.Signature(
  837. 'Alice Author', 'alice@authors.tld')
  838. committer = pygit2.Signature(
  839. 'Cecil Committer', 'cecil@committers.tld')
  840. repo.create_commit(
  841. 'refs/heads/master', # the name of the reference to update
  842. author,
  843. committer,
  844. 'Add a fake image file',
  845. # binary string representing the tree object ID
  846. tree,
  847. # list of binary strings representing parents of the new commit
  848. parents
  849. )
  850. # Push to origin
  851. ori_remote = repo.remotes[0]
  852. master_ref = repo.lookup_reference('HEAD').resolve()
  853. refname = '%s:%s' % (master_ref.name, master_ref.name)
  854. PagureRepo.push(ori_remote, refname)
  855. shutil.rmtree(newfolder)
  856. def remove_file_git_repo(folder, filename, branch='master'):
  857. """ Delete the specified file on the give git repo and branch. """
  858. repo, newfolder, parents = _clone_and_top_commits(folder, branch)
  859. # Remove file
  860. repo.index.remove(filename)
  861. # Write the change and commit it
  862. tree = repo.index.write_tree()
  863. author = pygit2.Signature(
  864. 'Alice Author', 'alice@authors.tld')
  865. committer = pygit2.Signature(
  866. 'Cecil Committer', 'cecil@committers.tld')
  867. branch_ref = "refs/heads/%s" % branch
  868. repo.create_commit(
  869. branch_ref, # the name of the reference to update
  870. author,
  871. committer,
  872. 'Remove file %s' % filename,
  873. # binary string representing the tree object ID
  874. tree,
  875. # list of binary strings representing parents of the new commit
  876. parents
  877. )
  878. # Push to origin
  879. ori_remote = repo.remotes[0]
  880. PagureRepo.push(ori_remote, '%s:%s' % (branch_ref, branch_ref))
  881. shutil.rmtree(newfolder)
  882. @contextmanager
  883. def capture_output(merge_stderr=True):
  884. oldout, olderr = sys.stdout, sys.stderr
  885. try:
  886. out = StringIO()
  887. err = StringIO()
  888. if merge_stderr:
  889. sys.stdout = sys.stderr = out
  890. yield out
  891. else:
  892. sys.stdout, sys.stderr = out, err
  893. yield out, err
  894. finally:
  895. sys.stdout, sys.stderr = oldout, olderr
  896. def get_alerts(html):
  897. soup = BeautifulSoup(html, "html.parser")
  898. alerts = []
  899. for element in soup.find_all("div", class_="alert"):
  900. severity = None
  901. for class_ in element["class"]:
  902. if not class_.startswith("alert-"):
  903. continue
  904. if class_ == "alert-dismissible":
  905. continue
  906. severity = class_[len("alert-"):]
  907. break
  908. element.find("button").decompose() # close button
  909. alerts.append(dict(
  910. severity=severity,
  911. text="".join(element.stripped_strings)
  912. ))
  913. return alerts
  914. def definitely_wait(result):
  915. """ Helper function for definitely waiting in _maybe_wait. """
  916. result.wait()
  917. if __name__ == '__main__':
  918. SUITE = unittest.TestLoader().loadTestsFromTestCase(Modeltests)
  919. unittest.TextTestRunner(verbosity=2).run(SUITE)