diff --git a/0001-Replace-python3-flask-with-httpserver-in-python-tests.patch b/0001-Replace-python3-flask-with-httpserver-in-python-tests.patch new file mode 100644 index 0000000..2cae8ca --- /dev/null +++ b/0001-Replace-python3-flask-with-httpserver-in-python-tests.patch @@ -0,0 +1,551 @@ +From 8bba792da37142028f6b6e61137ec2988b5578ee Mon Sep 17 00:00:00 2001 +From: Pavla Kratochvilova +Date: Mon, 26 Apr 2021 09:42:15 +0200 +Subject: [PATCH] Replace python3-flask with http.server in python tests + +--- + README.md | 1 - + librepo.spec | 1 - + tests/README.rst | 36 ++++++++++-------------------------- + tests/python/tests/base.py | 24 ++++-------------------- + tests/python/tests/servermock/server.py | 31 +++++++++---------------------- + tests/python/tests/servermock/yum_mock/yum_mock.py | 279 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------ + tests/python/tests/test_yum_package_downloading.py | 9 +++------ + tests/python/tests/test_yum_repo_downloading.py | 5 ++--- + 8 files changed, 175 insertions(+), 211 deletions(-) + +diff --git a/README.md b/README.md +index ad6bc23..d9fb062 100644 +--- a/README.md ++++ b/README.md +@@ -19,7 +19,6 @@ Fedora/Ubuntu name + * openssl (http://www.openssl.org/) - openssl-devel/libssl-dev + * python (http://python.org/) - python3-devel/libpython3-dev + * **Test requires:** pygpgme (https://pypi.python.org/pypi/pygpgme/0.1) - python3-pygpgme/python3-gpgme +-* **Test requires:** python3-flask (http://flask.pocoo.org/) - python-flask/python-flask + * **Test requires:** python3-pyxattr (https://github.com/xattr/xattr) - python3-pyxattr/python3-pyxattr + + ### Build from your checkout dir: +diff --git a/librepo.spec b/librepo.spec +index 6db1fc2..ed90a84 100644 +--- a/librepo.spec ++++ b/librepo.spec +@@ -51,7 +51,6 @@ Summary: Python 3 bindings for the librepo library + %{?python_provide:%python_provide python3-%{name}} + BuildRequires: python3-devel + BuildRequires: python3-gpg +-BuildRequires: python3-flask + BuildRequires: python3-pyxattr + BuildRequires: python3-requests + BuildRequires: python3-sphinx +diff --git a/tests/README.rst b/tests/README.rst +index ea55c87..f7e6d03 100644 +--- a/tests/README.rst ++++ b/tests/README.rst +@@ -33,15 +33,13 @@ Files of C tests + * Test suites + + +-Python tests with Flask +-======================= ++Python tests http.server ++======================== + +-Some python tests use **Flask** python framework (http://flask.pocoo.org/) ++Some python tests use **http.server** python standard library + to simulate web server. The server is started automatically during that tests. + +-*TestCases with Flask inherit from TestCaseWithApp class.* +- +-The Flask is then set as the app to the test case by 'application' class attribute. ++*TestCases with http.server inherit from TestCaseWithServer class.* + + If you want to start server manually:: + +@@ -57,27 +55,13 @@ http://127.0.0.1:5000/yum/badgpg/static/01/repodata/repomd.xml.asc + + etc.. + +-Modularity of tests with Flask +------------------------------- +- +-Flask tests are intended to be modular. +- +-Modularity is provided by use of http://flask.pocoo.org/docs/blueprints/ +-Currently there is only one module (blueprint) for yum repo mocking +-in servermock/yum_mock. +- +-Repos for test with Flask +-------------------------- +- +-Currently each module (blueprint) uses its own data (repositories, +-packages, ..). ++Repos for test with http.server ++------------------------------- + +-E.g. for yum mocking module: servermock/yum_mock/static/ ++All data (repositories, packages, ..) are in servermock/yum_mock/static/ + +-Configuration and globals for tests with Flask +----------------------------------------------- ++Configuration and globals for tests with http.server ++---------------------------------------------------- + +-Each module (blueprint) has its own configuration and globals which uses +-and provides to tests which use the module. ++Configuration and globals used by these tests are in servermock/yum_mock/config.py + +-E.g. for yum mocking module: servermock/yum_mock/config.py +diff --git a/tests/python/tests/base.py b/tests/python/tests/base.py +index 1d01c9d..ecabbb5 100644 +--- a/tests/python/tests/base.py ++++ b/tests/python/tests/base.py +@@ -5,7 +5,7 @@ import ctypes + import os.path + import requests + from multiprocessing import Process, Value +-from tests.servermock.server import app ++from tests.servermock.server import start_server + try: + import unittest2 as unittest + except ImportError: +@@ -53,39 +53,23 @@ class TestCase(unittest.TestCase): + pass + + +-class TestCaseWithApp(TestCase): +- application = NotImplemented +- +- @classmethod +- def setUpClass(cls): +- cls.server = Process(target=cls.application.run) +- cls.server.start() +- time.sleep(0.5) +- +- @classmethod +- def tearDownClass(cls): +- cls.server.terminate() +- cls.server.join() +- +- + def application(port): + """Sometimes, the port is used, in that case, use different port""" + + while True: + try: + port_val = port.value +- app._librepo_port = port_val # Store used port into Flask app +- app.run(port=port_val) +- except socket.error as e: ++ start_server(port=port_val) ++ except OSError as e: + if e.errno == 98: + # Address already in use + port.value += 1 + continue + raise + break + + +-class TestCaseWithFlask(TestCase): ++class TestCaseWithServer(TestCase): + _TS_PORT = Value(ctypes.c_int, 5000) + MOCKURL = None + PORT = -1 +diff --git a/tests/python/tests/servermock/server.py b/tests/python/tests/servermock/server.py +index 6d17a0a..37ddb64 100644 +--- a/tests/python/tests/servermock/server.py ++++ b/tests/python/tests/servermock/server.py +@@ -1,42 +1,29 @@ +-from flask import Flask ++from http.server import BaseHTTPRequestHandler, HTTPServer + from optparse import OptionParser + try: +- from yum_mock.yum_mock import yum_mock ++ from yum_mock.yum_mock import yum_mock_handler + except (ValueError, ImportError): +- from .yum_mock.yum_mock import yum_mock ++ from .yum_mock.yum_mock import yum_mock_handler + +-app = Flask(__name__) +-#app.register_blueprint(working_repo) +-app.register_blueprint(yum_mock, url_prefix='/yum') + ++def start_server(port, host="127.0.0.1", handler=None): ++ if handler is None: ++ handler = yum_mock_handler(port) ++ with HTTPServer((host, port), handler) as server: ++ server.serve_forever() + + if __name__ == '__main__': + parser = OptionParser("%prog [options]") + parser.add_option( +- "-d", "--debug", +- action="store_true", +- ) +- parser.add_option( + "-p", "--port", + default=5000, + type="int", + ) + parser.add_option( + "-n", "--host", + default="127.0.0.1", + ) +- parser.add_option( +- "--passthrough_errors", +- action="store_true", +- ) + options, args = parser.parse_args() + +- kwargs = { +- "threaded": True, +- "debug": options.debug, +- "port": options.port, +- "host": options.host, +- "passthrough_errors": options.passthrough_errors, +- } ++ start_server(options.port, options.host) + +- app.run(**kwargs) +diff --git a/tests/python/tests/servermock/yum_mock/yum_mock.py b/tests/python/tests/servermock/yum_mock/yum_mock.py +index 826f7c8..dd5bda6 100644 +--- a/tests/python/tests/servermock/yum_mock/yum_mock.py ++++ b/tests/python/tests/servermock/yum_mock/yum_mock.py +@@ -1,137 +1,152 @@ ++import base64 ++from http.server import BaseHTTPRequestHandler, HTTPServer + import os +-from flask import Blueprint, render_template, abort, send_file, request, Response +-from flask import current_app +-from functools import wraps ++import sys + + from .config import AUTH_USER, AUTH_PASS + +-yum_mock = Blueprint('yum_mock', __name__, +- template_folder='templates', +- static_folder='static') +- +-@yum_mock.route('/static/mirrorlist/') +-def serve_mirrorlists_with_right_port(path): +- try: +- with yum_mock.open_resource('static/mirrorlist/'+path) as f: +- data = f.read() +- data = data.decode('utf-8') +- data = data.replace(":{PORT_PLACEHOLDER}", ":%d" % current_app._librepo_port) +- return data +- except IOError: +- # File probably doesn't exist or we can't read it +- abort(404) +- +-@yum_mock.route('/static/metalink/') +-def serve_metalinks_with_right_port(path): +- try: +- with yum_mock.open_resource('static/metalink/'+path) as f: +- data = f.read() +- data = data.decode('utf-8') +- data = data.replace(":{PORT_PLACEHOLDER}", ":%d" % current_app._librepo_port) +- return data +- except IOError: +- # File probably doesn't exist or we can't read it +- abort(404) +- +-@yum_mock.route('/harm_checksum//') +-def harm_checksum(keyword, path): +- """Append two newlines to content of a file (from the static dir) with +- specified keyword in the filename. If the filename doesn't contain +- the keyword, content of the file is returnen unchanged.""" +- +- if "static/" not in path: +- # Support changing only files from static directory +- abort(400) +- path = path[path.find("static/"):] +- +- try: +- with yum_mock.open_resource(path) as f: +- data = f.read() +- if keyword in os.path.basename(path): +- return "%s\n\n" %data +- return data +- except IOError: +- # File probably doesn't exist or we can't read it +- abort(404) +- +-@yum_mock.route("/not_found//") +-def not_found(keyword, path): +- """For each file containing keyword in the filename, http status +- code 404 will be returned""" +- +- if "static/" not in path: +- abort(400) +- path = path[path.find("static/"):] +- +- try: +- with yum_mock.open_resource(path) as f: +- data = f.read() +- if keyword in os.path.basename(path): +- abort(404) +- return data +- except IOError: +- # File probably doesn't exist or we can't read it +- abort(404) +- +-@yum_mock.route("/badurl/") +-def badurl(path): +- """Just return 404 for each url with this prefix""" +- abort(404) +- +-@yum_mock.route("/badgpg/") +-def badgpg(path): +- """Instead of /repomd.xml.asc returns +- content of /repomd.xml.asc.bad""" +- if "static/" not in path: +- abort(400) +- path = path[path.find("static/"):] +- if path.endswith("repomd.xml.asc"): +- path = path + ".bad" +- +- try: +- with yum_mock.open_resource(path) as f: +- return f.read() +- except IOError: +- # File probably doesn't exist or we can't read it +- abort(404) +- +-# Basic Auth +- +-def check_auth(username, password): +- """This function is called to check if a username / +- password combination is valid. +- """ +- return username == AUTH_USER and password == AUTH_PASS +- +-def authenticate(): +- """Sends a 401 response that enables basic auth""" +- return Response( +- 'Could not verify your access level for that URL.\n' +- 'You have to login with proper credentials', 401, +- {'WWW-Authenticate': 'Basic realm="Login Required"'}) +- +-def requires_auth(f): +- @wraps(f) +- def decorated(*args, **kwargs): +- auth = request.authorization +- if not auth or not check_auth(auth.username, auth.password): +- return authenticate() +- return f(*args, **kwargs) +- return decorated +- +-@yum_mock.route("/auth_basic/") +-@requires_auth +-def secret_repo_basic_auth(path): +- """Page secured with basic HTTP auth +- User: admin Password: secret""" +- if "static/" not in path: +- abort(400) +- path = path[path.find("static/"):] +- +- try: +- with yum_mock.open_resource(path) as f: +- data = f.read() +- return data +- except IOError: +- abort(404) ++ ++def file_path(path): ++ return(os.path.join(os.path.dirname(os.path.abspath(__file__)), path)) ++ ++ ++def yum_mock_handler(port): ++ ++ class YumMockHandler(BaseHTTPRequestHandler): ++ _port = port ++ ++ def return_bad_request(self): ++ self.send_response(400) ++ self.end_headers() ++ ++ def return_not_found(self): ++ self.send_response(404) ++ self.end_headers() ++ ++ def return_ok_with_message(self, message, content_type='text/html'): ++ if content_type == 'text/html': ++ message = bytes(message, 'utf8') ++ self.send_response(200) ++ self.send_header('Content-type', content_type) ++ self.send_header('Content-Length', str(len(message))) ++ self.end_headers() ++ self.wfile.write(message) ++ ++ def parse_path(self, test_prefix='', keyword_expected=False): ++ path = self.path[len(test_prefix):] ++ if keyword_expected: ++ keyword, path = path.split('/', 1) ++ # Strip arguments ++ if '?' in path: ++ path = path[:path.find('?')] ++ if keyword_expected: ++ return keyword, path ++ return path ++ ++ def serve_file(self, path, harm_keyword=None): ++ if "static/" not in path: ++ # Support changing only files from static directory ++ return self.return_bad_request() ++ path = path[path.find("static/"):] ++ try: ++ with open(file_path(path), 'rb') as f: ++ data = f.read() ++ if harm_keyword is not None and harm_keyword in os.path.basename(file_path(path)): ++ data += b"\n\n" ++ return self.return_ok_with_message(data, 'application/octet-stream') ++ except IOError: ++ # File probably doesn't exist or we can't read it ++ return self.return_not_found() ++ ++ def authenticate(self): ++ """Sends a 401 response that enables basic auth""" ++ self.send_response(401) ++ self.send_header('Content-type', 'text/html') ++ self.send_header('WWW-Authenticate', 'Basic realm="Login Required') ++ self.end_headers() ++ message = ( ++ 'Could not verify your access level for that URL.\n' ++ 'You have to login with proper credentials' ++ ) ++ self.wfile.write(bytes(message, "utf8")) ++ ++ def check_auth(self): ++ if self.headers.get('Authorization') is None: ++ return False ++ expected_authorization = 'Basic {}'.format( ++ base64.b64encode('{}:{}'.format(AUTH_USER, AUTH_PASS).encode()).decode() ++ ) ++ if self.headers.get('Authorization') != expected_authorization: ++ return False ++ return True ++ ++ def serve_mirrorlist_or_metalink_with_right_port(self): ++ path = self.parse_path() ++ if "static/" not in path: ++ return self.return_bad_request() ++ path = path[path.find("static/"):] ++ try: ++ with open(file_path(path), 'r') as f: ++ data = f.read() ++ data = data.replace(":{PORT_PLACEHOLDER}", ":%d" % self._port) ++ return self.return_ok_with_message(data) ++ except IOError: ++ # File probably doesn't exist or we can't read it ++ return self.return_not_found() ++ ++ def serve_harm_checksum(self): ++ """Append two newlines to content of a file (from the static dir) with ++ specified keyword in the filename. If the filename doesn't contain ++ the keyword, content of the file is returnen unchanged.""" ++ keyword, path = self.parse_path('/yum/harm_checksum/', keyword_expected=True) ++ self.serve_file(path, harm_keyword=keyword) ++ ++ def serve_not_found(self): ++ """For each file containing keyword in the filename, http status ++ code 404 will be returned""" ++ keyword, path = self.parse_path('/yum/not_found/', keyword_expected=True) ++ if keyword in os.path.basename(file_path(path)): ++ return self.return_not_found() ++ self.serve_file(path) ++ ++ def serve_badurl(self): ++ """Just return 404 for each url with this prefix""" ++ return self.return_not_found() ++ ++ def serve_badgpg(self): ++ """Instead of /repomd.xml.asc returns content of /repomd.xml.asc.bad""" ++ path = self.parse_path('/yum/badgpg/') ++ if path.endswith("repomd.xml.asc"): ++ path += ".bad" ++ self.serve_file(path) ++ ++ def serve_auth_basic(self): ++ """Page secured with basic HTTP auth; User: admin Password: secret""" ++ if not self.check_auth(): ++ return self.authenticate() ++ path = self.parse_path('/yum/auth_basic/') ++ self.serve_file(path) ++ ++ def serve_static(self): ++ path = self.parse_path() ++ self.serve_file(path) ++ ++ def do_GET(self): ++ if self.path.startswith('/yum/static/mirrorlist/'): ++ return self.serve_mirrorlist_or_metalink_with_right_port() ++ if self.path.startswith('/yum/static/metalink/'): ++ return self.serve_mirrorlist_or_metalink_with_right_port() ++ if self.path.startswith('/yum/harm_checksum/'): ++ return self.serve_harm_checksum() ++ if self.path.startswith('/yum/not_found/'): ++ return self.serve_not_found() ++ if self.path.startswith('/badurl/'): ++ return self.serve_badurl() ++ if self.path.startswith('/yum/badgpg/'): ++ return self.serve_badgpg() ++ if self.path.startswith('/yum/auth_basic/'): ++ return self.serve_auth_basic() ++ return self.serve_static() ++ ++ return YumMockHandler + +diff --git a/tests/python/tests/test_yum_package_downloading.py b/tests/python/tests/test_yum_package_downloading.py +index 577a8ce..0364be0 100644 +--- a/tests/python/tests/test_yum_package_downloading.py ++++ b/tests/python/tests/test_yum_package_downloading.py +@@ -9,11 +9,10 @@ import xattr + + import tests.servermock.yum_mock.config as config + +-from tests.base import TestCaseWithFlask +-from tests.servermock.server import app ++from tests.base import TestCaseWithServer + + +-class TestCaseYumPackageDownloading(TestCaseWithFlask): ++class TestCaseYumPackageDownloading(TestCaseWithServer): + + def setUp(self): + self.tmpdir = tempfile.mkdtemp(prefix="librepotest-", dir="./") +@@ -163,9 +162,7 @@ class TestCaseYumPackageDownloading(TestCaseWithFlask): + pkg = os.path.join(self.tmpdir, config.PACKAGE_01_01) + self.assertTrue(os.path.isfile(pkg)) + +-class TestCaseYumPackagesDownloading(TestCaseWithFlask): +- application = app +- ++class TestCaseYumPackagesDownloading(TestCaseWithServer): + # @classmethod + # def setUpClass(cls): + # super(TestCaseYumPackageDownloading, cls).setUpClass() +diff --git a/tests/python/tests/test_yum_repo_downloading.py b/tests/python/tests/test_yum_repo_downloading.py +index 76b067c..4d56d1c 100644 +--- a/tests/python/tests/test_yum_repo_downloading.py ++++ b/tests/python/tests/test_yum_repo_downloading.py +@@ -7,13 +7,12 @@ import unittest + + import librepo + +-from tests.base import Context, TestCaseWithFlask, TEST_DATA +-from tests.servermock.server import app ++from tests.base import Context, TestCaseWithServer, TEST_DATA + import tests.servermock.yum_mock.config as config + + PUB_KEY = TEST_DATA+"/key.pub" + +-class TestCaseYumRepoDownloading(TestCaseWithFlask): ++class TestCaseYumRepoDownloading(TestCaseWithServer): + + def setUp(self): + self.tmpdir = tempfile.mkdtemp(prefix="librepotest-") +-- +libgit2 1.0.1 + diff --git a/librepo.spec b/librepo.spec index 30d7552..1e3b7f8 100644 --- a/librepo.spec +++ b/librepo.spec @@ -12,12 +12,13 @@ Name: librepo Version: 1.14.0 -Release: 1%{?dist} +Release: 2%{?dist} Summary: Repodata downloading library License: LGPLv2+ URL: https://github.com/rpm-software-management/librepo Source0: %{url}/archive/%{version}/%{name}-%{version}.tar.gz +Patch1: 0001-Replace-python3-flask-with-httpserver-in-python-tests.patch BuildRequires: cmake BuildRequires: gcc @@ -51,7 +52,6 @@ Summary: Python 3 bindings for the librepo library %{?python_provide:%python_provide python3-%{name}} BuildRequires: python3-devel BuildRequires: python3-gpg -BuildRequires: python3-flask BuildRequires: python3-pyxattr BuildRequires: python3-requests BuildRequires: python3-sphinx @@ -97,6 +97,9 @@ Python 3 bindings for the librepo library. %{python3_sitearch}/%{name}/ %changelog +* Fri Apr 30 2021 Pavla Kratochvilova - 1.14.0-2 +- Remove build dependency on python3-flask + * Mon Apr 26 2021 Pavla Kratochvilova - 1.14.0-1 - Update to 1.14.0 - Reposync does not re-download unchanged packages (RhBug:1931904)