__init__.py 34 KB

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