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