__init__.py 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286
  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(
  42. 0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
  43. )
  44. import pagure
  45. import pagure.api
  46. from pagure.api.ci import jenkins
  47. import pagure.flask_app
  48. import pagure.lib.git
  49. import pagure.lib.login
  50. import pagure.lib.model
  51. import pagure.lib.query
  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://repospanner.localhost.localdomain:%(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. LOGGING = {
  103. "version": 1,
  104. "disable_existing_loggers": False,
  105. "formatters": {
  106. "standard": {
  107. "format": "%%(asctime)s [%%(levelname)s] %%(name)s: %%(message)s"
  108. },
  109. },
  110. "handlers": {
  111. "console": {
  112. "formatter": "standard",
  113. "class": "logging.StreamHandler",
  114. "stream": "ext://sys.stderr",
  115. },
  116. },
  117. # The root logger configuration; this is a catch-all configuration
  118. # that applies to all log messages not handled by a different logger
  119. "root": {"level": "WARN", "handlers": ["console"]},
  120. }
  121. NOGITHOOKS = %(nogithooks)s
  122. """
  123. # The Celery docs warn against using task_always_eager:
  124. # http://docs.celeryproject.org/en/latest/userguide/testing.html
  125. # but that warning is only valid when testing the async nature of the task, not
  126. # what the task actually does.
  127. LOG.info("BUILD_ID: %s", os.environ.get("BUILD_ID"))
  128. WAIT_REGEX = re.compile(r"""var _url = '(\/wait\/[a-z0-9-]+\??.*)'""")
  129. def get_wait_target(html):
  130. """ This parses the window.location out of the HTML for the wait page. """
  131. found = WAIT_REGEX.findall(html)
  132. if len(found) == 0:
  133. raise Exception("Not able to get wait target in %s" % html)
  134. return found[-1]
  135. def get_post_target(html):
  136. """ This parses the wait page form to get the POST url. """
  137. soup = BeautifulSoup(html, "html.parser")
  138. form = soup.find(id="waitform")
  139. if not form:
  140. raise Exception("Not able to get the POST url in %s" % html)
  141. return form.get("action")
  142. def get_post_args(html):
  143. """ This parses the wait page for the hidden arguments of the form. """
  144. soup = BeautifulSoup(html, "html.parser")
  145. output = {}
  146. inputs = soup.find_all("input")
  147. if not inputs:
  148. raise Exception("Not able to get the POST arguments in %s" % html)
  149. for inp in inputs:
  150. if inp.get("type") == "hidden":
  151. output[inp.get("name")] = inp.get("value")
  152. return output
  153. def create_maybe_waiter(method, getter):
  154. def maybe_waiter(*args, **kwargs):
  155. """ A wrapper for self.app.get()/.post() that will resolve wait's """
  156. result = method(*args, **kwargs)
  157. # Handle the POST wait case
  158. form_url = None
  159. form_args = None
  160. try:
  161. result_text = result.get_data(as_text=True)
  162. except UnicodeDecodeError:
  163. return result
  164. if 'id="waitform"' in result_text:
  165. form_url = get_post_target(result_text)
  166. form_args = get_post_args(result_text)
  167. form_args["csrf_token"] = result_text.split(
  168. 'name="csrf_token" type="hidden" value="'
  169. )[1].split('">')[0]
  170. count = 0
  171. while "We are waiting for your task to finish." in result_text:
  172. # Resolve wait page
  173. target_url = get_wait_target(result_text)
  174. if count > 10:
  175. time.sleep(0.5)
  176. else:
  177. time.sleep(0.1)
  178. result = getter(target_url, follow_redirects=True)
  179. try:
  180. result_text = result.get_data(as_text=True)
  181. except UnicodeDecodeError:
  182. return result
  183. if count > 50:
  184. raise Exception("Had to wait too long")
  185. else:
  186. if form_url and form_args:
  187. return method(form_url, data=form_args, follow_redirects=True)
  188. return result
  189. return maybe_waiter
  190. @contextmanager
  191. def user_set(APP, user, keep_get_user=False):
  192. """ Set the provided user as fas_user in the provided application."""
  193. # Hack used to remove the before_request function set by
  194. # flask.ext.fas_openid.FAS which otherwise kills our effort to set a
  195. # flask.g.fas_user.
  196. from flask import appcontext_pushed, g
  197. keep = []
  198. for meth in APP.before_request_funcs[None]:
  199. if "flask_fas_openid.FAS" in str(meth):
  200. continue
  201. keep.append(meth)
  202. APP.before_request_funcs[None] = keep
  203. def handler(sender, **kwargs):
  204. g.fas_user = user
  205. g.fas_session_id = b"123"
  206. g.authenticated = True
  207. old_get_user = pagure.flask_app._get_user
  208. if not keep_get_user:
  209. pagure.flask_app._get_user = mock.MagicMock(
  210. return_value=pagure.lib.model.User()
  211. )
  212. with appcontext_pushed.connected_to(handler, APP):
  213. yield
  214. pagure.flask_app._get_user = old_get_user
  215. def create_user(session, username, fullname, emails):
  216. """Create an user with the provided information.
  217. Note that `emails` should be a list of emails.
  218. """
  219. user = pagure.lib.model.User(
  220. user=username,
  221. fullname=fullname,
  222. password=pagure.lib.login.generate_hashed_value("foo"),
  223. default_email=emails[0],
  224. )
  225. session.add(user)
  226. session.flush()
  227. for email in emails:
  228. item = pagure.lib.model.UserEmail(user_id=user.id, email=email)
  229. session.add(item)
  230. session.commit()
  231. def _populate_db(session):
  232. # Create a couple of users
  233. create_user(
  234. session, "pingou", "PY C", ["bar@pingou.com", "foo@pingou.com"]
  235. )
  236. create_user(session, "foo", "foo bar", ["foo@bar.com"])
  237. class SimplePagureTest(unittest.TestCase):
  238. """
  239. Simple Test class that does not set a broker/worker
  240. """
  241. populate_db = True
  242. config_values = {}
  243. def store_eager_results(self, *args, **kwargs):
  244. """A wrapper for EagerResult that stores the instance."""
  245. result = EagerResult(*args, **kwargs)
  246. self.results[result.id] = result
  247. return result
  248. @mock.patch("pagure.lib.notify.fedmsg_publish", mock.MagicMock())
  249. def __init__(self, method_name="runTest"):
  250. """ Constructor. """
  251. unittest.TestCase.__init__(self, method_name)
  252. self.session = None
  253. self.path = None
  254. self.gitrepo = None
  255. self.gitrepos = None
  256. self.results = {}
  257. def perfMaxWalks(self, max_walks, max_steps):
  258. """ Check that we have not performed too many walks/steps. """
  259. num_walks = 0
  260. num_steps = 0
  261. for reqstat in perfrepo.REQUESTS:
  262. for walk in reqstat["walks"].values():
  263. num_walks += 1
  264. num_steps += walk["steps"]
  265. self.assertLessEqual(
  266. num_walks,
  267. max_walks,
  268. "%s git repo walks performed, at most %s allowed"
  269. % (num_walks, max_walks),
  270. )
  271. self.assertLessEqual(
  272. num_steps,
  273. max_steps,
  274. "%s git repo steps performed, at most %s allowed"
  275. % (num_steps, max_steps),
  276. )
  277. def perfReset(self):
  278. """ Reset perfrepo stats. """
  279. perfrepo.reset_stats()
  280. perfrepo.REQUESTS = []
  281. def setUp(self):
  282. self.dbfolder = tempfile.mkdtemp(prefix="pagure-tests-")
  283. self.dbpath = "sqlite:///%s/db.sqlite" % self.dbfolder
  284. session = pagure.lib.model.create_tables(
  285. self.dbpath,
  286. acls=pagure_config.get("ACLS", {}),
  287. )
  288. self.db_session = session
  289. # Create a broker
  290. broker_url = os.path.join(self.dbfolder, "broker")
  291. self.broker = broker = subprocess.Popen(
  292. [
  293. "/usr/bin/redis-server",
  294. "--unixsocket",
  295. broker_url,
  296. "--port",
  297. "0",
  298. "--loglevel",
  299. "warning",
  300. "--logfile",
  301. "/dev/null",
  302. ],
  303. stdout=None,
  304. stderr=None,
  305. )
  306. broker.poll()
  307. if broker.returncode is not None:
  308. raise Exception("Broker failed to start")
  309. self.broker_client = redis.Redis(unix_socket_path=broker_url)
  310. # Store the EagerResults to be able to retrieve them later
  311. self.eg_patcher = mock.patch("celery.app.task.EagerResult")
  312. eg_mock = self.eg_patcher.start()
  313. eg_mock.side_effect = self.store_eager_results
  314. self.perfReset()
  315. self.path = tempfile.mkdtemp(prefix="pagure-tests-path-")
  316. LOG.debug("Testdir: %s", self.path)
  317. for folder in ["repos", "forks", "releases", "remotes", "attachments"]:
  318. os.mkdir(os.path.join(self.path, folder))
  319. if hasattr(pagure.lib.query, "REDIS") and pagure.lib.query.REDIS:
  320. pagure.lib.query.REDIS.connection_pool.disconnect()
  321. pagure.lib.query.REDIS = None
  322. # Database
  323. self._prepare_db()
  324. # Write a config file
  325. config_values = {
  326. "path": self.path,
  327. "dburl": self.dbpath,
  328. "enable_docs": True,
  329. "docs_folder": "%s/repos/docs" % self.path,
  330. "enable_tickets": True,
  331. "tickets_folder": "%s/repos/tickets" % self.path,
  332. "global_path": self.dbfolder,
  333. "authbackend": "gitolite3",
  334. "repobridge_binary": "/usr/libexec/repobridge",
  335. "repospanner_gitport": str(8443 + sys.version_info.major),
  336. "repospanner_new_repo": "None",
  337. "repospanner_admin_override": "False",
  338. "repospanner_new_fork": "True",
  339. "repospanner_admin_migration": "False",
  340. "nogithooks": False,
  341. }
  342. config_values.update(self.config_values)
  343. self.config_values = config_values
  344. self.config_path = os.path.join(self.path, "config")
  345. if not os.path.exists(self.config_path):
  346. with open(self.config_path, "w") as f:
  347. f.write(CONFIG_TEMPLATE % self.config_values)
  348. os.environ["PAGURE_CONFIG"] = self.config_path
  349. pagure_config.update(reload_config())
  350. imp.reload(pagure.lib.tasks)
  351. imp.reload(pagure.lib.tasks_mirror)
  352. imp.reload(pagure.lib.tasks_services)
  353. self._app = pagure.flask_app.create_app({"DB_URL": self.dbpath})
  354. self.app = self._app.test_client()
  355. self.gr_patcher = mock.patch("pagure.lib.tasks.get_result")
  356. gr_mock = self.gr_patcher.start()
  357. gr_mock.side_effect = lambda tid: self.results[tid]
  358. # Refresh the DB session
  359. self.session = pagure.lib.query.create_session(self.dbpath)
  360. def tearDown(self):
  361. self.gr_patcher.stop()
  362. self.session.rollback()
  363. self._clear_database()
  364. self.db_session.close()
  365. self.eg_patcher.stop()
  366. self.broker.kill()
  367. self.broker.wait()
  368. # Remove testdir
  369. try:
  370. shutil.rmtree(self.path)
  371. except:
  372. # Sometimes there is a race condition that makes deleting the folder
  373. # fail during the first attempt. So just try a second time if that's
  374. # the case.
  375. shutil.rmtree(self.path)
  376. try:
  377. shutil.rmtree(self.dbfolder)
  378. except:
  379. pass
  380. self.path = None
  381. self.dbpath = None
  382. del self.app
  383. del self._app
  384. def shortDescription(self):
  385. doc = self.__str__() + ": " + self._testMethodDoc
  386. return doc or None
  387. def _prepare_db(self):
  388. self.session = self.db_session
  389. pagure.lib.model.create_default_status(
  390. self.session, acls=pagure_config.get("ACLS", {})
  391. )
  392. if self.populate_db:
  393. _populate_db(self.session)
  394. def _clear_database(self):
  395. tables = reversed(pagure.lib.model_base.BASE.metadata.sorted_tables)
  396. # This seems to be needed to load the tables in memory or so and
  397. # without this, clearing the database fails with the error:
  398. # ``sqlite3.OperationalError: foreign key mismatch - "board_statuses"
  399. # referencing "boards"``
  400. # for reasons that are really not quite understood...
  401. [t for t in tables]
  402. if self.dbpath.startswith("postgresql"):
  403. self.session.execute(
  404. "TRUNCATE %s CASCADE" % ", ".join([t.name for t in tables])
  405. )
  406. elif self.dbpath.startswith("sqlite"):
  407. for table in tables:
  408. self.session.execute("DELETE FROM %s" % table.name)
  409. elif self.dbpath.startswith("mysql"):
  410. self.session.execute("SET FOREIGN_KEY_CHECKS = 0")
  411. for table in tables:
  412. self.session.execute("TRUNCATE %s" % table.name)
  413. self.session.execute("SET FOREIGN_KEY_CHECKS = 1")
  414. self.session.commit()
  415. def set_auth_status(self, value):
  416. """ Set the return value for the test auth """
  417. with open(
  418. os.path.join(self.path, "testauth_status.json"), "w"
  419. ) as statusfile:
  420. statusfile.write(six.u(json.dumps(value)))
  421. def get_csrf(self, url="/new", output=None):
  422. """Retrieve a CSRF token from given URL."""
  423. if output is None:
  424. output = self.app.get(url)
  425. self.assertEqual(output.status_code, 200)
  426. return (
  427. output.get_data(as_text=True)
  428. .split('name="csrf_token" type="hidden" value="')[1]
  429. .split('">')[0]
  430. )
  431. def get_wtforms_version(self):
  432. """Returns the wtforms version as a tuple."""
  433. import wtforms
  434. wtforms_v = wtforms.__version__.split(".")
  435. for idx, val in enumerate(wtforms_v):
  436. try:
  437. val = int(val)
  438. except ValueError:
  439. pass
  440. wtforms_v[idx] = val
  441. return tuple(wtforms_v)
  442. def get_arrow_version(self):
  443. """ Returns the arrow version as a tuple."""
  444. import arrow
  445. arrow_v = arrow.__version__.split(".")
  446. for idx, val in enumerate(arrow_v):
  447. try:
  448. val = int(val)
  449. except ValueError:
  450. pass
  451. arrow_v[idx] = val
  452. return tuple(arrow_v)
  453. def assertURLEqual(self, url_1, url_2):
  454. url_parsed_1 = list(urlparse(url_1))
  455. url_parsed_1[4] = parse_qs(url_parsed_1[4])
  456. url_parsed_2 = list(urlparse(url_2))
  457. url_parsed_2[4] = parse_qs(url_parsed_2[4])
  458. return self.assertListEqual(url_parsed_1, url_parsed_2)
  459. def assertJSONEqual(self, json_1, json_2):
  460. return self.assertEqual(json.loads(json_1), json.loads(json_2))
  461. class Modeltests(SimplePagureTest):
  462. """ Model tests. """
  463. def setUp(self): # pylint: disable=invalid-name
  464. """ Set up the environnment, ran before every tests. """
  465. # Clean up test performance info
  466. super(Modeltests, self).setUp()
  467. self.app.get = create_maybe_waiter(self.app.get, self.app.get)
  468. self.app.post = create_maybe_waiter(self.app.post, self.app.get)
  469. # Refresh the DB session
  470. self.session = pagure.lib.query.create_session(self.dbpath)
  471. def tearDown(self): # pylint: disable=invalid-name
  472. """ Remove the test.db database if there is one. """
  473. self.broker_client.flushall()
  474. super(Modeltests, self).tearDown()
  475. def create_project_full(self, projectname, extra=None):
  476. """Create a project via the API.
  477. This makes sure that the repo is fully setup the way a normal new
  478. project would be, with hooks and all setup.
  479. """
  480. headers = {"Authorization": "token aaabbbcccddd"}
  481. data = {"name": projectname, "description": "A test repo"}
  482. if extra:
  483. data.update(extra)
  484. # Valid request
  485. output = self.app.post("/api/0/new/", data=data, headers=headers)
  486. self.assertEqual(output.status_code, 200)
  487. data = json.loads(output.get_data(as_text=True))
  488. self.assertDictEqual(
  489. data, {"message": 'Project "%s" created' % projectname}
  490. )
  491. class FakeGroup(object): # pylint: disable=too-few-public-methods
  492. """Fake object used to make the FakeUser object closer to the
  493. expectations.
  494. """
  495. def __init__(self, name):
  496. """Constructor.
  497. :arg name: the name given to the name attribute of this object.
  498. """
  499. self.name = name
  500. self.group_type = "cla"
  501. class FakeUser(object): # pylint: disable=too-few-public-methods
  502. """ Fake user used to test the fedocallib library. """
  503. def __init__(
  504. self, groups=None, username="username", cla_done=True, id=None
  505. ):
  506. """Constructor.
  507. :arg groups: list of the groups in which this fake user is
  508. supposed to be.
  509. """
  510. if isinstance(groups, six.string_types):
  511. groups = [groups]
  512. self.id = id
  513. self.groups = groups or []
  514. self.user = username
  515. self.username = username
  516. self.name = username
  517. self.email = "foo@bar.com"
  518. self.default_email = "foo@bar.com"
  519. self.approved_memberships = [
  520. FakeGroup("packager"),
  521. FakeGroup("design-team"),
  522. ]
  523. self.dic = {}
  524. self.dic["timezone"] = "Europe/Paris"
  525. self.login_time = datetime.utcnow()
  526. self.cla_done = cla_done
  527. def __getitem__(self, key):
  528. return self.dic[key]
  529. def create_locks(session, project):
  530. for ltype in ("WORKER", "WORKER_TICKET", "WORKER_REQUEST"):
  531. lock = pagure.lib.model.ProjectLock(
  532. project_id=project.id, lock_type=ltype
  533. )
  534. session.add(lock)
  535. def create_projects(session, is_fork=False, user_id=1, hook_token_suffix=""):
  536. """ Create some projects in the database. """
  537. item = pagure.lib.model.Project(
  538. user_id=user_id, # pingou
  539. name="test",
  540. is_fork=is_fork,
  541. parent_id=1 if is_fork else None,
  542. description="test project #1",
  543. hook_token="aaabbbccc" + hook_token_suffix,
  544. )
  545. item.close_status = ["Invalid", "Insufficient data", "Fixed", "Duplicate"]
  546. session.add(item)
  547. session.flush()
  548. create_locks(session, item)
  549. item = pagure.lib.model.Project(
  550. user_id=user_id, # pingou
  551. name="test2",
  552. is_fork=is_fork,
  553. parent_id=2 if is_fork else None,
  554. description="test project #2",
  555. hook_token="aaabbbddd" + hook_token_suffix,
  556. )
  557. item.close_status = ["Invalid", "Insufficient data", "Fixed", "Duplicate"]
  558. session.add(item)
  559. session.flush()
  560. create_locks(session, item)
  561. item = pagure.lib.model.Project(
  562. user_id=user_id, # pingou
  563. name="test3",
  564. is_fork=is_fork,
  565. parent_id=3 if is_fork else None,
  566. description="namespaced test project",
  567. hook_token="aaabbbeee" + hook_token_suffix,
  568. namespace="somenamespace",
  569. )
  570. item.close_status = ["Invalid", "Insufficient data", "Fixed", "Duplicate"]
  571. session.add(item)
  572. session.flush()
  573. create_locks(session, item)
  574. session.commit()
  575. def create_projects_git(folder, bare=False):
  576. """ Create some projects in the database. """
  577. repos = []
  578. for project in [
  579. "test.git",
  580. "test2.git",
  581. os.path.join("somenamespace", "test3.git"),
  582. ]:
  583. repo_path = os.path.join(folder, project)
  584. repos.append(repo_path)
  585. if not os.path.exists(repo_path):
  586. os.makedirs(repo_path)
  587. pygit2.init_repository(repo_path, bare=bare)
  588. return repos
  589. def create_tokens(session, user_id=1, project_id=1, suffix=None):
  590. """ Create some tokens for the project in the database. """
  591. token = "aaabbbcccddd"
  592. if suffix:
  593. token += suffix
  594. item = pagure.lib.model.Token(
  595. id=token,
  596. user_id=user_id,
  597. project_id=project_id,
  598. expiration=datetime.utcnow() + timedelta(days=30),
  599. )
  600. session.add(item)
  601. token = "foo_token"
  602. if suffix:
  603. token += suffix
  604. item = pagure.lib.model.Token(
  605. id=token,
  606. user_id=user_id,
  607. project_id=project_id,
  608. expiration=datetime.utcnow() + timedelta(days=30),
  609. )
  610. session.add(item)
  611. token = "expired_token"
  612. if suffix:
  613. token += suffix
  614. item = pagure.lib.model.Token(
  615. id=token,
  616. user_id=user_id,
  617. project_id=project_id,
  618. expiration=datetime.utcnow() - timedelta(days=1),
  619. )
  620. session.add(item)
  621. session.commit()
  622. def create_tokens_acl(session, token_id="aaabbbcccddd", acl_name=None):
  623. """Create some ACLs for the token. If acl_name is not set, the token will
  624. have all the ACLs enabled.
  625. """
  626. if acl_name is None:
  627. for aclid in range(len(pagure_config["ACLS"])):
  628. token_acl = pagure.lib.model.TokenAcl(
  629. token_id=token_id, acl_id=aclid + 1
  630. )
  631. session.add(token_acl)
  632. else:
  633. acl = (
  634. session.query(pagure.lib.model.ACL).filter_by(name=acl_name).one()
  635. )
  636. token_acl = pagure.lib.model.TokenAcl(token_id=token_id, acl_id=acl.id)
  637. session.add(token_acl)
  638. session.commit()
  639. def _clone_and_top_commits(folder, branch, branch_ref=False):
  640. """Clone the repository, checkout the specified branch and return
  641. the top commit of that branch if there is one.
  642. Returns the repo, the path to the clone and the top commit(s) in a tuple
  643. or the repo, the path to the clone and the reference to the branch
  644. object if branch_ref is True.
  645. """
  646. if not os.path.exists(folder):
  647. os.makedirs(folder)
  648. brepo = pygit2.init_repository(folder, bare=True)
  649. newfolder = tempfile.mkdtemp(prefix="pagure-tests")
  650. repo = pygit2.clone_repository(folder, newfolder)
  651. branch_ref_obj = None
  652. if "origin/%s" % branch in repo.listall_branches(pygit2.GIT_BRANCH_ALL):
  653. branch_ref_obj = pagure.lib.git.get_branch_ref(repo, branch)
  654. repo.checkout(branch_ref_obj)
  655. if branch_ref:
  656. return (repo, newfolder, branch_ref_obj)
  657. parents = []
  658. commit = None
  659. try:
  660. if branch_ref_obj:
  661. commit = repo[branch_ref_obj.peel().hex]
  662. else:
  663. commit = repo.revparse_single("HEAD")
  664. except KeyError:
  665. pass
  666. if commit:
  667. parents = [commit.oid.hex]
  668. return (repo, newfolder, parents)
  669. def add_content_git_repo(folder, branch="master", append=None):
  670. """ Create some content for the specified git repo. """
  671. repo, newfolder, parents = _clone_and_top_commits(folder, branch)
  672. # Create a file in that git repo
  673. filename = os.path.join(newfolder, "sources")
  674. content = "foo\n bar"
  675. if os.path.exists(filename):
  676. content = "foo\n bar\nbaz"
  677. if append:
  678. content += append
  679. with open(filename, "w") as stream:
  680. stream.write(content)
  681. repo.index.add("sources")
  682. repo.index.write()
  683. # Commits the files added
  684. tree = repo.index.write_tree()
  685. author = pygit2.Signature("Alice Author", "alice@authors.tld")
  686. committer = pygit2.Signature("Cecil Committer", "cecil@committers.tld")
  687. commit = repo.create_commit(
  688. "refs/heads/%s" % branch, # the name of the reference to update
  689. author,
  690. committer,
  691. "Add sources file for testing",
  692. # binary string representing the tree object ID
  693. tree,
  694. # list of binary strings representing parents of the new commit
  695. parents,
  696. )
  697. if commit:
  698. parents = [commit.hex]
  699. subfolder = os.path.join("folder1", "folder2")
  700. if not os.path.exists(os.path.join(newfolder, subfolder)):
  701. os.makedirs(os.path.join(newfolder, subfolder))
  702. # Create a file in that git repo
  703. with open(os.path.join(newfolder, subfolder, "file"), "w") as stream:
  704. stream.write("foo\n bar\nbaz")
  705. repo.index.add(os.path.join(subfolder, "file"))
  706. with open(os.path.join(newfolder, subfolder, "fileŠ"), "w") as stream:
  707. stream.write("foo\n bar\nbaz")
  708. repo.index.add(os.path.join(subfolder, "fileŠ"))
  709. repo.index.write()
  710. # Commits the files added
  711. tree = repo.index.write_tree()
  712. author = pygit2.Signature("Alice Author", "alice@authors.tld")
  713. committer = pygit2.Signature("Cecil Committer", "cecil@committers.tld")
  714. commit = repo.create_commit(
  715. "refs/heads/%s" % branch, # the name of the reference to update
  716. author,
  717. committer,
  718. "Add some directory and a file for more testing",
  719. # binary string representing the tree object ID
  720. tree,
  721. # list of binary strings representing parents of the new commit
  722. parents,
  723. )
  724. # Push to origin
  725. ori_remote = repo.remotes[0]
  726. master_ref = repo.lookup_reference(
  727. "HEAD" if branch == "master" else "refs/heads/%s" % branch
  728. ).resolve()
  729. refname = "%s:%s" % (master_ref.name, master_ref.name)
  730. PagureRepo.push(ori_remote, refname)
  731. shutil.rmtree(newfolder)
  732. def add_readme_git_repo(folder, readme_name="README.rst", branch="master"):
  733. """ Create a README file for the specified git repo. """
  734. repo, newfolder, parents = _clone_and_top_commits(folder, branch)
  735. if readme_name == "README.rst":
  736. content = """Pagure
  737. ======
  738. :Author: Pierre-Yves Chibon <pingou@pingoured.fr>
  739. Pagure is a light-weight git-centered forge based on pygit2.
  740. Currently, Pagure offers a web-interface for git repositories, a ticket
  741. system and possibilities to create new projects, fork existing ones and
  742. create/merge pull-requests across or within projects.
  743. Homepage: https://github.com/pypingou/pagure
  744. Dev instance: http://209.132.184.222/ (/!\\ May change unexpectedly, it's a dev instance ;-))
  745. """
  746. else:
  747. content = (
  748. """Pagure
  749. ======
  750. This is a placeholder """
  751. + readme_name
  752. + """
  753. that should never get displayed on the website if there is a README.rst in the repo.
  754. """
  755. )
  756. # Create a file in that git repo
  757. with open(os.path.join(newfolder, readme_name), "w") as stream:
  758. stream.write(content)
  759. repo.index.add(readme_name)
  760. repo.index.write()
  761. # Commits the files added
  762. tree = repo.index.write_tree()
  763. author = pygit2.Signature("Alice Author", "alice@authors.tld")
  764. committer = pygit2.Signature("Cecil Committer", "cecil@committers.tld")
  765. branch_ref = "refs/heads/%s" % branch
  766. repo.create_commit(
  767. branch_ref, # the name of the reference to update
  768. author,
  769. committer,
  770. "Add a README file",
  771. # binary string representing the tree object ID
  772. tree,
  773. # list of binary strings representing parents of the new commit
  774. parents,
  775. )
  776. # Push to origin
  777. ori_remote = repo.remotes[0]
  778. PagureRepo.push(ori_remote, "%s:%s" % (branch_ref, branch_ref))
  779. shutil.rmtree(newfolder)
  780. def add_commit_git_repo(
  781. folder, ncommits=10, filename="sources", branch="master", symlink_to=None
  782. ):
  783. """ Create some more commits for the specified git repo. """
  784. repo, newfolder, branch_ref_obj = _clone_and_top_commits(
  785. folder, branch, branch_ref=True
  786. )
  787. for index in range(ncommits):
  788. # Create a file in that git repo
  789. if symlink_to:
  790. os.symlink(symlink_to, os.path.join(newfolder, filename))
  791. else:
  792. with open(os.path.join(newfolder, filename), "a") as stream:
  793. stream.write("Row %s\n" % index)
  794. repo.index.add(filename)
  795. repo.index.write()
  796. parents = []
  797. commit = None
  798. try:
  799. if branch_ref_obj:
  800. commit = repo[branch_ref_obj.peel().hex]
  801. else:
  802. commit = repo.revparse_single("HEAD")
  803. except (KeyError, AttributeError):
  804. pass
  805. if commit:
  806. parents = [commit.oid.hex]
  807. # Commits the files added
  808. tree = repo.index.write_tree()
  809. author = pygit2.Signature("Alice Author", "alice@authors.tld")
  810. committer = pygit2.Signature("Cecil Committer", "cecil@committers.tld")
  811. branch_ref = "refs/heads/%s" % branch
  812. repo.create_commit(
  813. branch_ref,
  814. author,
  815. committer,
  816. "Add row %s to %s file" % (index, filename),
  817. # binary string representing the tree object ID
  818. tree,
  819. # list of binary strings representing parents of the new commit
  820. parents,
  821. )
  822. branch_ref_obj = pagure.lib.git.get_branch_ref(repo, branch)
  823. # Push to origin
  824. ori_remote = repo.remotes[0]
  825. PagureRepo.push(ori_remote, "%s:%s" % (branch_ref, branch_ref))
  826. shutil.rmtree(newfolder)
  827. def add_tag_git_repo(folder, tagname, obj_hash, message):
  828. """ Add a tag to the given object of the given repo annotated by given message. """
  829. repo, newfolder, branch_ref_obj = _clone_and_top_commits(
  830. folder, "master", branch_ref=True
  831. )
  832. tag_sha = repo.create_tag(
  833. tagname,
  834. obj_hash,
  835. repo.get(obj_hash).type,
  836. pygit2.Signature("Alice Author", "alice@authors.tld"),
  837. message,
  838. )
  839. # Push to origin
  840. ori_remote = repo.remotes[0]
  841. PagureRepo.push(
  842. ori_remote, "refs/tags/%s:refs/tags/%s" % (tagname, tagname)
  843. )
  844. shutil.rmtree(newfolder)
  845. return tag_sha
  846. def add_content_to_git(
  847. folder,
  848. branch="master",
  849. folders=None,
  850. filename="sources",
  851. content="foo",
  852. message=None,
  853. author=("Alice Author", "alice@authors.tld"),
  854. commiter=("Cecil Committer", "cecil@committers.tld"),
  855. ):
  856. """ Create some more commits for the specified git repo. """
  857. repo, newfolder, branch_ref_obj = _clone_and_top_commits(
  858. folder, branch, branch_ref=True
  859. )
  860. # Create a file in that git repo
  861. if folders:
  862. if not os.path.exists(os.path.join(newfolder, folders)):
  863. os.makedirs(os.path.join(newfolder, folders))
  864. filename = os.path.join(folders, filename)
  865. filepath = os.path.join(newfolder, filename)
  866. with open(filepath, "a", encoding="utf-8") as stream:
  867. stream.write("%s\n" % content)
  868. repo.index.add(filename)
  869. repo.index.write()
  870. parents = []
  871. commit = None
  872. try:
  873. if branch_ref_obj:
  874. commit = repo[branch_ref_obj.peel().hex]
  875. else:
  876. commit = repo.revparse_single("HEAD")
  877. except (KeyError, AttributeError):
  878. pass
  879. if commit:
  880. parents = [commit.oid.hex]
  881. # Commits the files added
  882. tree = repo.index.write_tree()
  883. author = pygit2.Signature(*author)
  884. committer = pygit2.Signature(*commiter)
  885. branch_ref = "refs/heads/%s" % branch
  886. message = message or "Add content to file %s" % (filename)
  887. repo.create_commit(
  888. branch_ref, # the name of the reference to update
  889. author,
  890. committer,
  891. message,
  892. # binary string representing the tree object ID
  893. tree,
  894. # list of binary strings representing parents of the new commit
  895. parents,
  896. )
  897. # Push to origin
  898. ori_remote = repo.remotes[0]
  899. PagureRepo.push(ori_remote, "%s:%s" % (branch_ref, branch_ref))
  900. shutil.rmtree(newfolder)
  901. def add_binary_git_repo(folder, filename):
  902. """ Create a fake image file for the specified git repo. """
  903. repo, newfolder, parents = _clone_and_top_commits(folder, "master")
  904. content = b"""\x00\x00\x01\x00\x01\x00\x18\x18\x00\x00\x01\x00 \x00\x88
  905. \t\x00\x00\x16\x00\x00\x00(\x00\x00\x00\x18\x00x00\x00\x01\x00 \x00\x00\x00
  906. \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
  907. 00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa7lM\x01\xa6kM\t\xa6kM\x01
  908. \xa4fF\x04\xa2dE\x95\xa2cD8\xa1a
  909. """
  910. # Create a file in that git repo
  911. with open(os.path.join(newfolder, filename), "wb") as stream:
  912. stream.write(content)
  913. repo.index.add(filename)
  914. repo.index.write()
  915. # Commits the files added
  916. tree = repo.index.write_tree()
  917. author = pygit2.Signature("Alice Author", "alice@authors.tld")
  918. committer = pygit2.Signature("Cecil Committer", "cecil@committers.tld")
  919. repo.create_commit(
  920. "refs/heads/master", # the name of the reference to update
  921. author,
  922. committer,
  923. "Add a fake image file",
  924. # binary string representing the tree object ID
  925. tree,
  926. # list of binary strings representing parents of the new commit
  927. parents,
  928. )
  929. # Push to origin
  930. ori_remote = repo.remotes[0]
  931. master_ref = repo.lookup_reference("HEAD").resolve()
  932. refname = "%s:%s" % (master_ref.name, master_ref.name)
  933. PagureRepo.push(ori_remote, refname)
  934. shutil.rmtree(newfolder)
  935. def remove_file_git_repo(folder, filename, branch="master"):
  936. """ Delete the specified file on the give git repo and branch. """
  937. repo, newfolder, parents = _clone_and_top_commits(folder, branch)
  938. # Remove file
  939. repo.index.remove(filename)
  940. # Write the change and commit it
  941. tree = repo.index.write_tree()
  942. author = pygit2.Signature("Alice Author", "alice@authors.tld")
  943. committer = pygit2.Signature("Cecil Committer", "cecil@committers.tld")
  944. branch_ref = "refs/heads/%s" % branch
  945. repo.create_commit(
  946. branch_ref, # the name of the reference to update
  947. author,
  948. committer,
  949. "Remove file %s" % filename,
  950. # binary string representing the tree object ID
  951. tree,
  952. # list of binary strings representing parents of the new commit
  953. parents,
  954. )
  955. # Push to origin
  956. ori_remote = repo.remotes[0]
  957. PagureRepo.push(ori_remote, "%s:%s" % (branch_ref, branch_ref))
  958. shutil.rmtree(newfolder)
  959. def add_pull_request_git_repo(
  960. folder,
  961. session,
  962. repo,
  963. fork,
  964. branch_from="feature",
  965. user="pingou",
  966. allow_rebase=False,
  967. ):
  968. """Set up the git repo and create the corresponding PullRequest
  969. object.
  970. """
  971. # Clone the main repo
  972. gitrepo = os.path.join(folder, "repos", repo.path)
  973. newpath = tempfile.mkdtemp(prefix="pagure-fork-test")
  974. repopath = os.path.join(newpath, "test")
  975. clone_repo = pygit2.clone_repository(gitrepo, repopath)
  976. # Create a file in that git repo
  977. with open(os.path.join(repopath, "sources"), "w") as stream:
  978. stream.write("foo\n bar")
  979. clone_repo.index.add("sources")
  980. clone_repo.index.write()
  981. # Commits the files added
  982. tree = clone_repo.index.write_tree()
  983. author = pygit2.Signature("Alice Author", "alice@authors.tld")
  984. committer = pygit2.Signature("Cecil Committer", "cecil@committers.tld")
  985. clone_repo.create_commit(
  986. "refs/heads/master", # the name of the reference to update
  987. author,
  988. committer,
  989. "Add sources file for testing",
  990. # binary string representing the tree object ID
  991. tree,
  992. # list of binary strings representing parents of the new commit
  993. [],
  994. )
  995. refname = "refs/heads/master:refs/heads/master"
  996. ori_remote = clone_repo.remotes[0]
  997. PagureRepo.push(ori_remote, refname)
  998. first_commit = clone_repo.revparse_single("HEAD")
  999. # Set the second repo
  1000. repopath = os.path.join(folder, "repos", fork.path)
  1001. new_gitrepo = os.path.join(newpath, "fork_test")
  1002. clone_repo = pygit2.clone_repository(repopath, new_gitrepo)
  1003. # Add the main project as remote repo
  1004. upstream_path = os.path.join(folder, "repos", repo.path)
  1005. remote = clone_repo.create_remote("upstream", upstream_path)
  1006. remote.fetch()
  1007. # Edit the sources file again
  1008. with open(os.path.join(new_gitrepo, "sources"), "w") as stream:
  1009. stream.write("foo\n bar\nbaz\n boose")
  1010. clone_repo.index.add("sources")
  1011. clone_repo.index.write()
  1012. # Commits the files added
  1013. tree = clone_repo.index.write_tree()
  1014. author = pygit2.Signature("Alice Author", "alice@authors.tld")
  1015. committer = pygit2.Signature("Cecil Committer", "cecil@committers.tld")
  1016. clone_repo.create_commit(
  1017. "refs/heads/%s" % branch_from,
  1018. author,
  1019. committer,
  1020. "A commit on branch %s" % branch_from,
  1021. tree,
  1022. [first_commit.oid.hex],
  1023. )
  1024. refname = "refs/heads/%s" % (branch_from)
  1025. ori_remote = clone_repo.remotes[0]
  1026. PagureRepo.push(ori_remote, refname)
  1027. # Create a PR for these changes
  1028. project = pagure.lib.query.get_authorized_project(session, "test")
  1029. req = pagure.lib.query.new_pull_request(
  1030. session=session,
  1031. repo_from=fork,
  1032. branch_from=branch_from,
  1033. repo_to=project,
  1034. branch_to="master",
  1035. title="PR from the %s branch" % branch_from,
  1036. allow_rebase=allow_rebase,
  1037. user=user,
  1038. )
  1039. session.commit()
  1040. return req
  1041. def clean_pull_requests_path():
  1042. newpath = tempfile.mkdtemp(prefix="pagure-fork-test")
  1043. shutil.rmtree(newpath)
  1044. @contextmanager
  1045. def capture_output(merge_stderr=True):
  1046. oldout, olderr = sys.stdout, sys.stderr
  1047. try:
  1048. out = StringIO()
  1049. err = StringIO()
  1050. if merge_stderr:
  1051. sys.stdout = sys.stderr = out
  1052. yield out
  1053. else:
  1054. sys.stdout, sys.stderr = out, err
  1055. yield out, err
  1056. finally:
  1057. sys.stdout, sys.stderr = oldout, olderr
  1058. def get_alerts(html):
  1059. soup = BeautifulSoup(html, "html.parser")
  1060. alerts = []
  1061. for element in soup.find_all("div", class_="alert"):
  1062. severity = None
  1063. for class_ in element["class"]:
  1064. if not class_.startswith("alert-"):
  1065. continue
  1066. if class_ == "alert-dismissible":
  1067. continue
  1068. severity = class_[len("alert-") :]
  1069. break
  1070. element.find("button").decompose() # close button
  1071. alerts.append(
  1072. dict(severity=severity, text="".join(element.stripped_strings))
  1073. )
  1074. return alerts
  1075. def definitely_wait(result):
  1076. """ Helper function for definitely waiting in _maybe_wait. """
  1077. result.wait()
  1078. if __name__ == "__main__":
  1079. SUITE = unittest.TestLoader().loadTestsFromTestCase(Modeltests)
  1080. unittest.TextTestRunner(verbosity=2).run(SUITE)