diff options
Diffstat (limited to 'test')
56 files changed, 724 insertions, 699 deletions
diff --git a/test/conftest.py b/test/conftest.py index 3edc471d..b36aabad 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -4,7 +4,6 @@ import platform import re import shutil import signal -import socket import stat import subprocess import sys @@ -13,10 +12,13 @@ import time from multiprocessing import Process import pytest - from unit.check.go import check_go +from unit.check.isolation import check_isolation from unit.check.node import check_node from unit.check.tls import check_openssl +from unit.option import option +from unit.utils import public_dir +from unit.utils import waitforfiles def pytest_addoption(parser): @@ -27,13 +29,13 @@ def pytest_addoption(parser): help="Detailed output for tests", ) parser.addoption( - "--print_log", + "--print-log", default=False, action="store_true", help="Print unit.log to stdout in case of errors", ) parser.addoption( - "--save_log", + "--save-log", default=False, action="store_true", help="Save unit.log after the test execution", @@ -44,16 +46,24 @@ def pytest_addoption(parser): action="store_true", help="Run unsafe tests", ) + parser.addoption( + "--user", + type=str, + help="Default user for non-privileged processes of unitd", + ) unit_instance = {} _processes = [] -option = None - def pytest_configure(config): - global option - option = config.option + option.config = config.option + + option.detailed = config.option.detailed + option.print_log = config.option.print_log + option.save_log = config.option.save_log + option.unsafe = config.option.unsafe + option.user = config.option.user option.generated_tests = {} option.current_dir = os.path.abspath( @@ -63,16 +73,15 @@ def pytest_configure(config): option.architecture = platform.architecture()[0] option.system = platform.system() + option.cache_dir = tempfile.mkdtemp(prefix='unit-test-cache-') + public_dir(option.cache_dir) + # set stdout to non-blocking if option.detailed or option.print_log: fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, 0) -def skip_alert(*alerts): - option.skip_alerts.extend(alerts) - - def pytest_generate_tests(metafunc): cls = metafunc.cls if (not hasattr(cls, 'application_type') @@ -122,6 +131,7 @@ def pytest_sessionstart(session): option.available = {'modules': {}, 'features': {}} unit = unit_run() + option.temp_dir = unit['temp_dir'] # read unit.log @@ -160,8 +170,12 @@ def pytest_sessionstart(session): k: v for k, v in option.available['modules'].items() if v is not None } + check_isolation() + unit_stop() + _check_alerts() + shutil.rmtree(unit_instance['temp_dir']) @@ -177,6 +191,40 @@ def pytest_runtest_makereport(item, call): setattr(item, "rep_" + rep.when, rep) +@pytest.fixture(scope='class', autouse=True) +def check_prerequisites(request): + cls = request.cls + missed = [] + + # check modules + + if 'modules' in cls.prerequisites: + available_modules = list(option.available['modules'].keys()) + + for module in cls.prerequisites['modules']: + if module in available_modules: + continue + + missed.append(module) + + if missed: + pytest.skip('Unit has no ' + ', '.join(missed) + ' module(s)') + + # check features + + if 'features' in cls.prerequisites: + available_features = list(option.available['features'].keys()) + + for feature in cls.prerequisites['features']: + if feature in available_features: + continue + + missed.append(feature) + + if missed: + pytest.skip(', '.join(missed) + ' feature(s) not supported') + + @pytest.fixture(autouse=True) def run(request): unit = unit_run() @@ -239,26 +287,28 @@ def unit_run(): os.mkdir(temp_dir + '/state') + unitd_args = [ + unitd, + '--no-daemon', + '--modules', + build_dir, + '--state', + temp_dir + '/state', + '--pid', + temp_dir + '/unit.pid', + '--log', + temp_dir + '/unit.log', + '--control', + 'unix:' + temp_dir + '/control.unit.sock', + '--tmp', + temp_dir, + ] + + if option.user: + unitd_args.extend(['--user', option.user]) + with open(temp_dir + '/unit.log', 'w') as log: - unit_instance['process'] = subprocess.Popen( - [ - unitd, - '--no-daemon', - '--modules', - build_dir, - '--state', - temp_dir + '/state', - '--pid', - temp_dir + '/unit.pid', - '--log', - temp_dir + '/unit.log', - '--control', - 'unix:' + temp_dir + '/control.unit.sock', - '--tmp', - temp_dir, - ], - stderr=log, - ) + unit_instance['process'] = subprocess.Popen(unitd_args, stderr=log) if not waitforfiles(temp_dir + '/control.unit.sock'): _print_log() @@ -293,34 +343,6 @@ def unit_stop(): p.kill() return 'Could not terminate unit' -def public_dir(path): - os.chmod(path, 0o777) - - for root, dirs, files in os.walk(path): - for d in dirs: - os.chmod(os.path.join(root, d), 0o777) - for f in files: - os.chmod(os.path.join(root, f), 0o777) - -def waitforfiles(*files): - for i in range(50): - wait = False - ret = False - - for f in files: - if not os.path.exists(f): - wait = True - break - - if wait: - time.sleep(0.1) - - else: - ret = True - break - - return ret - def _check_alerts(path=None): @@ -335,7 +357,7 @@ def _check_alerts(path=None): alerts = re.findall(r'.+\[alert\].+', log) if alerts: - print('All alerts/sanitizer errors found in log:') + print('\nAll alerts/sanitizer errors found in log:') [print(alert) for alert in alerts] found = True @@ -399,26 +421,13 @@ def stop_processes(): return 'Fail to stop process(es)' -def waitforsocket(port): - ret = False - - for i in range(50): - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect(('127.0.0.1', port)) - ret = True - break - - except KeyboardInterrupt: - raise - - except: - sock.close() - time.sleep(0.1) +@pytest.fixture() +def skip_alert(): + def _skip(*alerts): + option.skip_alerts.extend(alerts) - sock.close() + return _skip - assert ret, 'socket connected' @pytest.fixture def temp_dir(request): @@ -432,5 +441,10 @@ def is_unsafe(request): def is_su(request): return os.geteuid() == 0 +@pytest.fixture +def unit_pid(request): + return unit_instance['process'].pid + def pytest_sessionfinish(session): unit_stop() + shutil.rmtree(option.cache_dir) diff --git a/test/php/auth/index.php b/test/php/auth/index.php new file mode 100644 index 00000000..d77076d8 --- /dev/null +++ b/test/php/auth/index.php @@ -0,0 +1,7 @@ +<?php + +header('X-Digest: ' . (isset($_SERVER['PHP_AUTH_DIGEST']) ? $_SERVER['PHP_AUTH_DIGEST'] : 'not set')); +header('X-User: ' . (isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : 'not set')); +header('X-Password: ' . (isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : 'not set')); + +?> diff --git a/test/pytest.ini b/test/pytest.ini index fe86cef2..8952e8c6 100644 --- a/test/pytest.ini +++ b/test/pytest.ini @@ -1,3 +1,3 @@ [pytest] -addopts = -vvv -s --print_log +addopts = -vvv -s --print-log python_functions = test_* diff --git a/test/python/path/wsgi.py b/test/python/path/wsgi.py new file mode 100644 index 00000000..2807f6ef --- /dev/null +++ b/test/python/path/wsgi.py @@ -0,0 +1,8 @@ +import os +import sys + +def application(environ, start_response): + body = os.pathsep.join(sys.path).encode() + + start_response('200', [('Content-Length', str(len(body)))]) + return [body] diff --git a/test/test_access_log.py b/test/test_access_log.py index 511ce6c5..65d5e50a 100644 --- a/test/test_access_log.py +++ b/test/test_access_log.py @@ -1,10 +1,8 @@ import time import pytest - -from conftest import option -from conftest import unit_stop from unit.applications.lang.python import TestApplicationPython +from unit.option import option class TestAccessLog(TestApplicationPython): @@ -50,8 +48,6 @@ class TestAccessLog(TestApplicationPython): body='0123456789', ) - unit_stop() - assert ( self.wait_for_record(r'"POST / HTTP/1.1" 200 10') is not None ), 'keepalive 2' @@ -78,8 +74,6 @@ Connection: close raw=True, ) - unit_stop() - assert ( self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-1" "-"') is not None @@ -96,12 +90,12 @@ Connection: close def test_access_log_ipv6(self): self.load('empty') - self.conf({"[::1]:7080": {"pass": "applications/empty"}}, 'listeners') + assert 'success' in self.conf( + {"[::1]:7080": {"pass": "applications/empty"}}, 'listeners' + ) self.get(sock_type='ipv6') - unit_stop() - assert ( self.wait_for_record( r'::1 - - \[.+\] "GET / HTTP/1.1" 200 0 "-" "-"' @@ -114,14 +108,12 @@ Connection: close addr = option.temp_dir + '/sock' - self.conf( + assert 'success' in self.conf( {"unix:" + addr: {"pass": "applications/empty"}}, 'listeners' ) self.get(sock_type='unix', addr=addr) - unit_stop() - assert ( self.wait_for_record( r'unix: - - \[.+\] "GET / HTTP/1.1" 200 0 "-" "-"' @@ -140,8 +132,6 @@ Connection: close } ) - unit_stop() - assert ( self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "referer-value" "-"') is not None @@ -158,8 +148,6 @@ Connection: close } ) - unit_stop() - assert ( self.wait_for_record( r'"GET / HTTP/1.1" 200 0 "-" "user-agent-value"' @@ -172,8 +160,6 @@ Connection: close self.get(http_10=True) - unit_stop() - assert ( self.wait_for_record(r'"GET / HTTP/1.0" 200 0 "-" "-"') is not None ), 'http 1.0' @@ -187,8 +173,6 @@ Connection: close time.sleep(1) - unit_stop() - assert ( self.wait_for_record(r'"GE" 400 0 "-" "-"') is not None ), 'partial' @@ -200,8 +184,6 @@ Connection: close self.http(b"""GET /\n""", raw=True) - unit_stop() - assert ( self.wait_for_record(r'"GET /" 400 \d+ "-" "-"') is not None ), 'partial 2' @@ -215,8 +197,6 @@ Connection: close time.sleep(1) - unit_stop() - assert ( self.wait_for_record(r'"GET /" 400 0 "-" "-"') is not None ), 'partial 3' @@ -230,8 +210,6 @@ Connection: close time.sleep(1) - unit_stop() - assert ( self.wait_for_record(r'"GET / HTTP/1.1" 400 0 "-" "-"') is not None ), 'partial 4' @@ -244,8 +222,6 @@ Connection: close self.get(headers={'Connection': 'close'}) - unit_stop() - assert ( self.wait_for_record(r'"GET / HTTP/1.1" 400 \d+ "-" "-"') is not None @@ -256,8 +232,6 @@ Connection: close self.get(url='/?blah&var=val') - unit_stop() - assert ( self.wait_for_record( r'"GET /\?blah&var=val HTTP/1.1" 200 0 "-" "-"' @@ -268,12 +242,10 @@ Connection: close def test_access_log_delete(self): self.load('empty') - self.conf_delete('access_log') + assert 'success' in self.conf_delete('access_log') self.get(url='/delete') - unit_stop() - assert self.search_in_log(r'/delete', 'access.log') is None, 'delete' def test_access_log_change(self, temp_dir): @@ -281,12 +253,12 @@ Connection: close self.get() - self.conf('"' + option.temp_dir + '/new.log"', 'access_log') + assert 'success' in self.conf( + '"' + option.temp_dir + '/new.log"', 'access_log' + ) self.get() - unit_stop() - assert ( self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', 'new.log') is not None diff --git a/test/test_asgi_application.py b/test/test_asgi_application.py index e90d78bc..5770265d 100644 --- a/test/test_asgi_application.py +++ b/test/test_asgi_application.py @@ -3,10 +3,8 @@ import time from distutils.version import LooseVersion import pytest - -from conftest import option -from conftest import skip_alert from unit.applications.lang.python import TestApplicationPython +from unit.option import option class TestASGIApplication(TestApplicationPython): @@ -361,7 +359,7 @@ Connection: close self.get(headers=headers_delay_1) - def test_asgi_application_loading_error(self): + def test_asgi_application_loading_error(self, skip_alert): skip_alert(r'Python failed to import module "blah"') self.load('empty', module="blah") diff --git a/test/test_asgi_lifespan.py b/test/test_asgi_lifespan.py index 3f29c7e7..3ecece43 100644 --- a/test/test_asgi_lifespan.py +++ b/test/test_asgi_lifespan.py @@ -2,11 +2,9 @@ import os from distutils.version import LooseVersion import pytest - -from conftest import option -from conftest import public_dir from conftest import unit_stop from unit.applications.lang.python import TestApplicationPython +from unit.option import option class TestASGILifespan(TestApplicationPython): diff --git a/test/test_asgi_websockets.py b/test/test_asgi_websockets.py index 54984526..6121fcc5 100644 --- a/test/test_asgi_websockets.py +++ b/test/test_asgi_websockets.py @@ -3,11 +3,9 @@ import time from distutils.version import LooseVersion import pytest - -from conftest import option -from conftest import skip_alert from unit.applications.lang.python import TestApplicationPython from unit.applications.websockets import TestApplicationWebsocket +from unit.option import option class TestASGIWebsockets(TestApplicationPython): @@ -17,7 +15,8 @@ class TestASGIWebsockets(TestApplicationPython): ws = TestApplicationWebsocket() - def setup_method(self): + @pytest.fixture(autouse=True) + def setup_method_fixture(self, request, skip_alert): assert 'success' in self.conf( {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' ), 'clear keepalive_interval' @@ -64,6 +63,9 @@ class TestASGIWebsockets(TestApplicationPython): key ), 'key' + # remove "mirror" application + self.load('websockets/subprotocol') + def test_asgi_websockets_subprotocol(self): self.load('websockets/subprotocol') @@ -93,6 +95,27 @@ class TestASGIWebsockets(TestApplicationPython): sock.close() + def test_asgi_websockets_mirror_app_change(self): + self.load('websockets/mirror') + + message = 'blah' + + _, sock, _ = self.ws.upgrade() + + self.ws.frame_write(sock, self.ws.OP_TEXT, message) + frame = self.ws.frame_read(sock) + + assert message == frame['data'].decode('utf-8'), 'mirror' + + self.load('websockets/subprotocol') + + self.ws.frame_write(sock, self.ws.OP_TEXT, message) + frame = self.ws.frame_read(sock) + + assert message == frame['data'].decode('utf-8'), 'mirror 2' + + sock.close() + def test_asgi_websockets_no_mask(self): self.load('websockets/mirror') diff --git a/test/test_configuration.py b/test/test_configuration.py index d1e6f000..b7417264 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -1,6 +1,4 @@ import pytest - -from conftest import skip_alert from unit.control import TestControl @@ -337,7 +335,7 @@ class TestConfiguration(TestControl): assert 'success' in self.conf(conf) - def test_unprivileged_user_error(self, is_su): + def test_unprivileged_user_error(self, is_su, skip_alert): skip_alert(r'cannot set user "root"', r'failed to apply new conf') if is_su: pytest.skip('unprivileged tests') diff --git a/test/test_go_application.py b/test/test_go_application.py index 8c77dfc5..e833d190 100644 --- a/test/test_go_application.py +++ b/test/test_go_application.py @@ -149,7 +149,7 @@ class TestGoApplication(TestApplicationGo): arg2 = '--cc-opt=\'-O0 -DNXT_DEBUG_MEMORY=1 -fsanitize=address\'' arg3 = '--debug' - self.conf( + assert 'success' in self.conf( '["' + arg1 + '", "' + arg2 + '", "' + arg3 + '"]', 'applications/command_line_arguments/arguments', ) @@ -163,15 +163,15 @@ class TestGoApplication(TestApplicationGo): args_path = 'applications/command_line_arguments/arguments' - self.conf('["0", "a", "$", ""]', args_path) + assert 'success' in self.conf('["0", "a", "$", ""]', args_path) assert self.get()['body'] == '0,a,$,', 'arguments' - self.conf('["-1", "b", "%"]', args_path) + assert 'success' in self.conf('["-1", "b", "%"]', args_path) assert self.get()['body'] == '-1,b,%', 'arguments change' - self.conf('[]', args_path) + assert 'success' in self.conf('[]', args_path) assert ( self.get()['headers']['Content-Length'] == '0' diff --git a/test/test_go_isolation.py b/test/test_go_isolation.py index 8c4a6b9c..e8fa26c6 100644 --- a/test/test_go_isolation.py +++ b/test/test_go_isolation.py @@ -1,35 +1,15 @@ import grp import os import pwd -import shutil import pytest - -from conftest import option -from conftest import unit_run -from conftest import unit_stop from unit.applications.lang.go import TestApplicationGo -from unit.feature.isolation import TestFeatureIsolation +from unit.option import option +from unit.utils import getns class TestGoIsolation(TestApplicationGo): prerequisites = {'modules': {'go': 'any'}, 'features': ['isolation']} - isolation = TestFeatureIsolation() - - @classmethod - def setup_class(cls, complete_check=True): - check = super().setup_class(complete_check=False) - - unit = unit_run() - option.temp_dir = unit['temp_dir'] - - TestFeatureIsolation().check(option.available, unit['temp_dir']) - - assert unit_stop() is None - shutil.rmtree(unit['temp_dir']) - - return check if not complete_check else check() - def unpriv_creds(self): nobody_uid = pwd.getpwnam('nobody').pw_uid @@ -219,8 +199,8 @@ class TestGoIsolation(TestApplicationGo): == option.available['features']['isolation'][ns] ), ('%s match' % ns) - assert obj['NS']['MNT'] != self.isolation.getns('mnt'), 'mnt set' - assert obj['NS']['USER'] != self.isolation.getns('user'), 'user set' + assert obj['NS']['MNT'] != getns('mnt'), 'mnt set' + assert obj['NS']['USER'] != getns('user'), 'user set' def test_isolation_pid(self, is_su): if not self.isolation_key('pid'): @@ -360,16 +340,20 @@ class TestGoIsolation(TestApplicationGo): 'pid': True } + isolation['automount'] = { + 'tmpfs': False + } + self.load('ns_inspect', isolation=isolation) obj = self.getjson(url='/?mounts=true')['body'] assert ( - "/ /tmp" in obj['Mounts'] and "tmpfs" in obj['Mounts'] - ), 'app has /tmp mounted on /' + "/ /tmp" not in obj['Mounts'] and "tmpfs" not in obj['Mounts'] + ), 'app has no /tmp mounted' isolation['automount'] = { - 'tmpfs': False + 'tmpfs': True } self.load('ns_inspect', isolation=isolation) @@ -377,5 +361,5 @@ class TestGoIsolation(TestApplicationGo): obj = self.getjson(url='/?mounts=true')['body'] assert ( - "/ /tmp" not in obj['Mounts'] and "tmpfs" not in obj['Mounts'] - ), 'app has no /tmp mounted' + "/ /tmp" in obj['Mounts'] and "tmpfs" in obj['Mounts'] + ), 'app has /tmp mounted on /' diff --git a/test/test_go_isolation_rootfs.py b/test/test_go_isolation_rootfs.py index 1cc59c67..2bded5ec 100644 --- a/test/test_go_isolation_rootfs.py +++ b/test/test_go_isolation_rootfs.py @@ -1,7 +1,6 @@ import os import pytest - from unit.applications.lang.go import TestApplicationGo diff --git a/test/test_http_header.py b/test/test_http_header.py index fdb557cf..ca355eb7 100644 --- a/test/test_http_header.py +++ b/test/test_http_header.py @@ -1,5 +1,4 @@ import pytest - from unit.applications.lang.python import TestApplicationPython diff --git a/test/test_java_application.py b/test/test_java_application.py index 41345e87..4a67f291 100644 --- a/test/test_java_application.py +++ b/test/test_java_application.py @@ -3,15 +3,14 @@ import os import re import time -from conftest import option -from conftest import public_dir -from conftest import skip_alert from unit.applications.lang.java import TestApplicationJava +from unit.option import option +from unit.utils import public_dir class TestJavaApplication(TestApplicationJava): prerequisites = {'modules': {'java': 'all'}} - def test_java_conf_error(self, temp_dir): + def test_java_conf_error(self, temp_dir, skip_alert): skip_alert( r'realpath.*failed', r'failed to apply new conf', diff --git a/test/test_java_isolation_rootfs.py b/test/test_java_isolation_rootfs.py index 02d35a62..91773981 100644 --- a/test/test_java_isolation_rootfs.py +++ b/test/test_java_isolation_rootfs.py @@ -2,9 +2,8 @@ import os import subprocess import pytest - -from conftest import option from unit.applications.lang.java import TestApplicationJava +from unit.option import option class TestJavaIsolationRootfs(TestApplicationJava): diff --git a/test/test_java_websockets.py b/test/test_java_websockets.py index 7586d4aa..315c496d 100644 --- a/test/test_java_websockets.py +++ b/test/test_java_websockets.py @@ -2,11 +2,9 @@ import struct import time import pytest - -from conftest import option -from conftest import skip_alert from unit.applications.lang.java import TestApplicationJava from unit.applications.websockets import TestApplicationWebsocket +from unit.option import option class TestJavaWebsockets(TestApplicationJava): @@ -14,7 +12,8 @@ class TestJavaWebsockets(TestApplicationJava): ws = TestApplicationWebsocket() - def setup_method(self): + @pytest.fixture(autouse=True) + def setup_method_fixture(self, request, skip_alert): assert 'success' in self.conf( {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' ), 'clear keepalive_interval' diff --git a/test/test_node_application.py b/test/test_node_application.py index c8c3a444..b277dc3a 100644 --- a/test/test_node_application.py +++ b/test/test_node_application.py @@ -1,9 +1,8 @@ import re import pytest - -from conftest import waitforfiles from unit.applications.lang.node import TestApplicationNode +from unit.utils import waitforfiles class TestNodeApplication(TestApplicationNode): @@ -222,22 +221,6 @@ class TestNodeApplication(TestApplicationNode): assert 'X-Header' not in headers, 'insensitive' assert 'X-header' not in headers, 'insensitive 2' - def test_node_application_promise_handler(self, temp_dir): - self.load('promise_handler') - - assert ( - self.post( - headers={ - 'Host': 'localhost', - 'Content-Type': 'text/html', - 'Connection': 'close', - }, - body='callback', - )['status'] - == 200 - ), 'promise handler request' - assert waitforfiles(temp_dir + '/node/callback'), 'promise handler' - def test_node_application_promise_handler_write_after_end(self): self.load('promise_handler') @@ -270,35 +253,6 @@ class TestNodeApplication(TestApplicationNode): ), 'promise end request' assert waitforfiles(temp_dir + '/node/callback'), 'promise end' - def test_node_application_promise_multiple_calls(self, temp_dir): - self.load('promise_handler') - - self.post( - headers={ - 'Host': 'localhost', - 'Content-Type': 'text/html', - 'Connection': 'close', - }, - body='callback1', - ) - - assert waitforfiles( - temp_dir + '/node/callback1' - ), 'promise first call' - - self.post( - headers={ - 'Host': 'localhost', - 'Content-Type': 'text/html', - 'Connection': 'close', - }, - body='callback2', - ) - - assert waitforfiles( - temp_dir + '/node/callback2' - ), 'promise second call' - @pytest.mark.skip('not yet') def test_node_application_header_name_valid(self): self.load('header_name_valid') diff --git a/test/test_node_websockets.py b/test/test_node_websockets.py index 7b65b5c1..4f1e5906 100644 --- a/test/test_node_websockets.py +++ b/test/test_node_websockets.py @@ -2,11 +2,9 @@ import struct import time import pytest - -from conftest import option -from conftest import skip_alert from unit.applications.lang.node import TestApplicationNode from unit.applications.websockets import TestApplicationWebsocket +from unit.option import option class TestNodeWebsockets(TestApplicationNode): @@ -14,7 +12,8 @@ class TestNodeWebsockets(TestApplicationNode): ws = TestApplicationWebsocket() - def setup_method(self): + @pytest.fixture(autouse=True) + def setup_method_fixture(self, request, skip_alert): assert 'success' in self.conf( {'http': {'websocket': {'keepalive_interval': 0}}}, 'settings' ), 'clear keepalive_interval' diff --git a/test/test_perl_application.py b/test/test_perl_application.py index 78f2dd90..dfd8be6c 100644 --- a/test/test_perl_application.py +++ b/test/test_perl_application.py @@ -1,9 +1,6 @@ import re import pytest - -from conftest import skip_alert -from conftest import unit_stop from unit.applications.lang.perl import TestApplicationPerl @@ -120,8 +117,6 @@ class TestPerlApplication(TestApplicationPerl): assert self.get()['body'] == '1', 'errors result' - unit_stop() - assert ( self.wait_for_record(r'\[error\].+Error in application') is not None @@ -170,7 +165,7 @@ class TestPerlApplication(TestApplicationPerl): assert self.get()['body'] == 'body\n', 'body io file' @pytest.mark.skip('not yet') - def test_perl_application_syntax_error(self): + def test_perl_application_syntax_error(self, skip_alert): skip_alert(r'PSGI: Failed to parse script') self.load('syntax_error') diff --git a/test/test_php_application.py b/test/test_php_application.py index 578de0b7..e73c67ba 100644 --- a/test/test_php_application.py +++ b/test/test_php_application.py @@ -2,12 +2,11 @@ import os import re import shutil import time +from subprocess import call import pytest - -from conftest import option -from conftest import unit_stop from unit.applications.lang.php import TestApplicationPHP +from unit.option import option class TestPHPApplication(TestApplicationPHP): prerequisites = {'modules': {'php': 'all'}} @@ -20,7 +19,7 @@ class TestPHPApplication(TestApplicationPHP): def set_opcache(self, app, val): assert 'success' in self.conf( - {"admin": {"opcache.enable": val, "opcache.enable_cli": val,},}, + {"admin": {"opcache.enable": val, "opcache.enable_cli": val}}, 'applications/' + app + '/options', ) @@ -99,7 +98,10 @@ class TestPHPApplication(TestApplicationPHP): assert self.get()['body'] == '0123' - unit_stop() + with open(temp_dir + '/unit.pid', 'r') as f: + pid = f.read().rstrip() + + call(['kill', '-s', 'USR1', pid]) with open(temp_dir + '/unit.log', 'r', errors='ignore') as f: errs = re.findall(r'Error in fastcgi_finish_request', f.read()) @@ -113,7 +115,10 @@ class TestPHPApplication(TestApplicationPHP): assert resp['status'] == 200 assert resp['body'] == '' - unit_stop() + with open(temp_dir + '/unit.pid', 'r') as f: + pid = f.read().rstrip() + + call(['kill', '-s', 'USR1', pid]) with open(temp_dir + '/unit.log', 'r', errors='ignore') as f: errs = re.findall(r'Error in fastcgi_finish_request', f.read()) @@ -263,7 +268,7 @@ class TestPHPApplication(TestApplicationPHP): assert self.get()['headers']['X-Precision'] != '4', 'ini value default' - self.conf( + assert 'success' in self.conf( {"file": "ini/php.ini"}, 'applications/ini_precision/options' ) @@ -285,7 +290,7 @@ class TestPHPApplication(TestApplicationPHP): def test_php_application_ini_admin(self): self.load('ini_precision') - self.conf( + assert 'success' in self.conf( {"file": "php.ini", "admin": {"precision": "5"}}, 'applications/ini_precision/options', ) @@ -295,7 +300,7 @@ class TestPHPApplication(TestApplicationPHP): def test_php_application_ini_user(self): self.load('ini_precision') - self.conf( + assert 'success' in self.conf( {"file": "php.ini", "user": {"precision": "5"}}, 'applications/ini_precision/options', ) @@ -305,13 +310,13 @@ class TestPHPApplication(TestApplicationPHP): def test_php_application_ini_user_2(self): self.load('ini_precision') - self.conf( + assert 'success' in self.conf( {"file": "ini/php.ini"}, 'applications/ini_precision/options' ) assert self.get()['headers']['X-Precision'] == '4', 'ini user file' - self.conf( + assert 'success' in self.conf( {"precision": "5"}, 'applications/ini_precision/options/user' ) @@ -320,7 +325,7 @@ class TestPHPApplication(TestApplicationPHP): def test_php_application_ini_set_admin(self): self.load('ini_precision') - self.conf( + assert 'success' in self.conf( {"admin": {"precision": "5"}}, 'applications/ini_precision/options' ) @@ -331,7 +336,7 @@ class TestPHPApplication(TestApplicationPHP): def test_php_application_ini_set_user(self): self.load('ini_precision') - self.conf( + assert 'success' in self.conf( {"user": {"precision": "5"}}, 'applications/ini_precision/options' ) @@ -342,7 +347,7 @@ class TestPHPApplication(TestApplicationPHP): def test_php_application_ini_repeat(self): self.load('ini_precision') - self.conf( + assert 'success' in self.conf( {"user": {"precision": "5"}}, 'applications/ini_precision/options' ) @@ -355,7 +360,7 @@ class TestPHPApplication(TestApplicationPHP): self.before_disable_functions() - self.conf( + assert 'success' in self.conf( {"admin": {"disable_functions": "exec"}}, 'applications/time_exec/options', ) @@ -370,7 +375,7 @@ class TestPHPApplication(TestApplicationPHP): self.before_disable_functions() - self.conf( + assert 'success' in self.conf( {"admin": {"disable_functions": "exec,time"}}, 'applications/time_exec/options', ) @@ -384,12 +389,70 @@ class TestPHPApplication(TestApplicationPHP): r'exec: \/\w+', body ), 'disable_functions comma exec' + def test_php_application_auth(self): + self.load('auth') + + resp = self.get() + assert resp['status'] == 200, 'status' + assert resp['headers']['X-Digest'] == 'not set', 'digest' + assert resp['headers']['X-User'] == 'not set', 'user' + assert resp['headers']['X-Password'] == 'not set', 'password' + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Authorization': 'Basic dXNlcjpwYXNzd29yZA==', + 'Connection': 'close', + } + ) + assert resp['status'] == 200, 'basic status' + assert resp['headers']['X-Digest'] == 'not set', 'basic digest' + assert resp['headers']['X-User'] == 'user', 'basic user' + assert resp['headers']['X-Password'] == 'password', 'basic password' + + resp = self.get( + headers={ + 'Host': 'localhost', + 'Authorization': 'Digest username="blah", realm="", uri="/"', + 'Connection': 'close', + } + ) + assert resp['status'] == 200, 'digest status' + assert ( + resp['headers']['X-Digest'] == 'username="blah", realm="", uri="/"' + ), 'digest digest' + assert resp['headers']['X-User'] == 'not set', 'digest user' + assert resp['headers']['X-Password'] == 'not set', 'digest password' + + def test_php_application_auth_invalid(self): + self.load('auth') + + def check_auth(auth): + resp = self.get(headers={ + 'Host': 'localhost', + 'Authorization': auth, + 'Connection': 'close', + }) + + assert resp['status'] == 200, 'status' + assert resp['headers']['X-Digest'] == 'not set', 'Digest' + assert resp['headers']['X-User'] == 'not set', 'User' + assert resp['headers']['X-Password'] == 'not set', 'Password' + + check_auth('Basic dXN%cjpwYXNzd29yZA==') + check_auth('Basic XNlcjpwYXNzd29yZA==') + check_auth('Basic DdXNlcjpwYXNzd29yZA==') + check_auth('Basic blah') + check_auth('Basic') + check_auth('Digest') + check_auth('blah') + def test_php_application_disable_functions_space(self): self.load('time_exec') self.before_disable_functions() - self.conf( + assert 'success' in self.conf( {"admin": {"disable_functions": "exec time"}}, 'applications/time_exec/options', ) @@ -408,7 +471,7 @@ class TestPHPApplication(TestApplicationPHP): self.before_disable_functions() - self.conf( + assert 'success' in self.conf( {"user": {"disable_functions": "exec"}}, 'applications/time_exec/options', ) @@ -425,7 +488,7 @@ class TestPHPApplication(TestApplicationPHP): self.before_disable_functions() - self.conf( + assert 'success' in self.conf( {"admin": {"disable_functions": "blah"}}, 'applications/time_exec/options', ) @@ -446,7 +509,7 @@ class TestPHPApplication(TestApplicationPHP): r'012345', self.get()['body'] ), 'disable_classes before' - self.conf( + assert 'success' in self.conf( {"admin": {"disable_classes": "DateTime"}}, 'applications/date_time/options', ) @@ -462,7 +525,7 @@ class TestPHPApplication(TestApplicationPHP): r'012345', self.get()['body'] ), 'disable_classes before' - self.conf( + assert 'success' in self.conf( {"user": {"disable_classes": "DateTime"}}, 'applications/date_time/options', ) @@ -480,8 +543,6 @@ class TestPHPApplication(TestApplicationPHP): assert self.get()['status'] == 200, 'status 2' - unit_stop() - pattern = r'\d{4}\/\d\d\/\d\d\s\d\d:.+\[notice\].+Error in application' assert self.wait_for_record(pattern) is not None, 'errors print' diff --git a/test/test_php_basic.py b/test/test_php_basic.py index 1420ec21..bcd66173 100644 --- a/test/test_php_basic.py +++ b/test/test_php_basic.py @@ -19,7 +19,7 @@ class TestPHPBasic(TestControl): } def test_php_get_applications(self): - self.conf(self.conf_app, 'applications') + assert 'success' in self.conf(self.conf_app, 'applications') conf = self.conf_get() @@ -55,7 +55,7 @@ class TestPHPBasic(TestControl): ), 'spare processes' def test_php_get_listeners(self): - self.conf(self.conf_basic) + assert 'success' in self.conf(self.conf_basic) assert self.conf_get()['listeners'] == { "*:7080": {"pass": "applications/app"} @@ -70,16 +70,20 @@ class TestPHPBasic(TestControl): }, 'listeners prefix 2' def test_php_change_listener(self): - self.conf(self.conf_basic) - self.conf({"*:7081": {"pass": "applications/app"}}, 'listeners') + assert 'success' in self.conf(self.conf_basic) + assert 'success' in self.conf( + {"*:7081": {"pass": "applications/app"}}, 'listeners' + ) assert self.conf_get('listeners') == { "*:7081": {"pass": "applications/app"} }, 'change listener' def test_php_add_listener(self): - self.conf(self.conf_basic) - self.conf({"pass": "applications/app"}, 'listeners/*:7082') + assert 'success' in self.conf(self.conf_basic) + assert 'success' in self.conf( + {"pass": "applications/app"}, 'listeners/*:7082' + ) assert self.conf_get('listeners') == { "*:7080": {"pass": "applications/app"}, @@ -87,20 +91,20 @@ class TestPHPBasic(TestControl): }, 'add listener' def test_php_change_application(self): - self.conf(self.conf_basic) + assert 'success' in self.conf(self.conf_basic) - self.conf('30', 'applications/app/processes/max') + assert 'success' in self.conf('30', 'applications/app/processes/max') assert ( self.conf_get('applications/app/processes/max') == 30 ), 'change application max' - self.conf('"/www"', 'applications/app/root') + assert 'success' in self.conf('"/www"', 'applications/app/root') assert ( self.conf_get('applications/app/root') == '/www' ), 'change application root' def test_php_delete(self): - self.conf(self.conf_basic) + assert 'success' in self.conf(self.conf_basic) assert 'error' in self.conf_delete('applications/app') assert 'success' in self.conf_delete('listeners/*:7080') @@ -108,7 +112,7 @@ class TestPHPBasic(TestControl): assert 'error' in self.conf_delete('applications/app') def test_php_delete_blocks(self): - self.conf(self.conf_basic) + assert 'success' in self.conf(self.conf_basic) assert 'success' in self.conf_delete('listeners') assert 'success' in self.conf_delete('applications') diff --git a/test/test_php_isolation.py b/test/test_php_isolation.py index cc660e04..2e458023 100644 --- a/test/test_php_isolation.py +++ b/test/test_php_isolation.py @@ -1,31 +1,11 @@ -import shutil - import pytest - -from conftest import option -from conftest import unit_run -from conftest import unit_stop from unit.applications.lang.php import TestApplicationPHP -from unit.feature.isolation import TestFeatureIsolation +from unit.option import option class TestPHPIsolation(TestApplicationPHP): prerequisites = {'modules': {'php': 'any'}, 'features': ['isolation']} - @classmethod - def setup_class(cls, complete_check=True): - check = super().setup_class(complete_check=False) - - unit = unit_run() - option.temp_dir = unit['temp_dir'] - - TestFeatureIsolation().check(option.available, unit['temp_dir']) - - assert unit_stop() is None - shutil.rmtree(unit['temp_dir']) - - return check if not complete_check else check() - def test_php_isolation_rootfs(self, is_su, temp_dir): isolation_features = option.available['features']['isolation'].keys() diff --git a/test/test_php_targets.py b/test/test_php_targets.py index e64cd6b6..76326131 100644 --- a/test/test_php_targets.py +++ b/test/test_php_targets.py @@ -1,5 +1,5 @@ -from conftest import option from unit.applications.lang.php import TestApplicationPHP +from unit.option import option class TestPHPTargets(TestApplicationPHP): diff --git a/test/test_proxy.py b/test/test_proxy.py index be3e93fd..2d305e98 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -3,12 +3,10 @@ import socket import time import pytest - -from conftest import option from conftest import run_process -from conftest import skip_alert -from conftest import waitforsocket from unit.applications.lang.python import TestApplicationPython +from unit.option import option +from unit.utils import waitforsocket class TestProxy(TestApplicationPython): @@ -174,7 +172,9 @@ Content-Length: 10 assert resp['status'] == 200, 'status' assert resp['body'] == payload, 'body' - self.conf({'http': {'max_body_size': 32 * 1024 * 1024}}, 'settings') + assert 'success' in self.conf( + {'http': {'max_body_size': 32 * 1024 * 1024}}, 'settings' + ) payload = '0123456789abcdef' * 32 * 64 * 1024 resp = self.post_http10(body=payload, read_buffer_size=1024 * 1024) @@ -482,13 +482,13 @@ Content-Length: 10 check_proxy('http://[:]:7080') check_proxy('http://[::7080') - def test_proxy_loop(self): + def test_proxy_loop(self, skip_alert): skip_alert( r'socket.*failed', r'accept.*failed', r'new connections are not accepted', ) - self.conf( + assert 'success' in self.conf( { "listeners": { "*:7080": {"pass": "routes"}, diff --git a/test/test_proxy_chunked.py b/test/test_proxy_chunked.py index ae2228fa..73d94332 100644 --- a/test/test_proxy_chunked.py +++ b/test/test_proxy_chunked.py @@ -3,10 +3,10 @@ import select import socket import time -from conftest import option from conftest import run_process -from conftest import waitforsocket from unit.applications.lang.python import TestApplicationPython +from unit.option import option +from unit.utils import waitforsocket class TestProxyChunked(TestApplicationPython): diff --git a/test/test_python_application.py b/test/test_python_application.py index 83b0c8f4..709df3ff 100644 --- a/test/test_python_application.py +++ b/test/test_python_application.py @@ -1,14 +1,12 @@ import grp +import os import pwd import re import time import pytest - -from conftest import option -from conftest import skip_alert -from conftest import unit_stop from unit.applications.lang.python import TestApplicationPython +from unit.option import option class TestPythonApplication(TestApplicationPython): @@ -156,9 +154,7 @@ custom-header: BLAH assert resp['status'] == 200, 'ctx iter status' assert resp['body'] == '0123456789', 'ctx iter body' - self.conf({"listeners": {}, "applications": {}}) - - unit_stop() + assert 'success' in self.conf({"listeners": {}, "applications": {}}) assert ( self.wait_for_record(r'RuntimeError') is not None @@ -336,9 +332,7 @@ Connection: close self.get() - self.conf({"listeners": {}, "applications": {}}) - - unit_stop() + assert 'success' in self.conf({"listeners": {}, "applications": {}}) assert self.wait_for_record(r'At exit called\.') is not None, 'atexit' @@ -497,8 +491,6 @@ last line: 987654321 self.get() - unit_stop() - assert ( self.wait_for_record(r'\[error\].+Error in application\.') is not None @@ -520,13 +512,13 @@ last line: 987654321 assert self.get()['body'] == 'body\n', 'body io file' @pytest.mark.skip('not yet') - def test_python_application_syntax_error(self): + def test_python_application_syntax_error(self, skip_alert): skip_alert(r'Python failed to import module "wsgi"') self.load('syntax_error') assert self.get()['status'] == 500, 'syntax error' - def test_python_application_loading_error(self): + def test_python_application_loading_error(self, skip_alert): skip_alert(r'Python failed to import module "blah"') self.load('empty', module="blah") @@ -538,8 +530,6 @@ last line: 987654321 self.get() - unit_stop() - assert self.wait_for_record(r'Close called\.') is not None, 'close' def test_python_application_close_error(self): @@ -547,8 +537,6 @@ last line: 987654321 self.get() - unit_stop() - assert ( self.wait_for_record(r'Close called\.') is not None ), 'close error' @@ -558,8 +546,6 @@ last line: 987654321 self.get() - unit_stop() - assert ( self.wait_for_record( r'\[error\].+the application returned not an iterable object' @@ -791,7 +777,7 @@ last line: 987654321 assert obj['UID'] == nobody_uid, 'root uid group=root' assert obj['GID'] == 0, 'root gid group=root' - def test_python_application_callable(self): + def test_python_application_callable(self, skip_alert): skip_alert(r'Python failed to get "blah" from module') self.load('callable') @@ -805,6 +791,42 @@ last line: 987654321 assert self.get()['status'] not in [200, 204], 'callable response inv' + def test_python_application_path(self): + self.load('path') + + def set_path(path): + assert 'success' in self.conf(path, 'applications/path/path') + + def get_path(): + return self.get()['body'].split(os.pathsep) + + default_path = self.conf_get('/config/applications/path/path') + assert 'success' in self.conf( + {"PYTHONPATH": default_path}, + '/config/applications/path/environment', + ) + + self.conf_delete('/config/applications/path/path') + sys_path = get_path() + + set_path('"/blah"') + assert ['/blah', *sys_path] == get_path(), 'check path' + + set_path('"/new"') + assert ['/new', *sys_path] == get_path(), 'check path update' + + set_path('["/blah1", "/blah2"]') + assert ['/blah1', '/blah2', *sys_path] == get_path(), 'check path array' + + def test_python_application_path_invalid(self): + self.load('path') + + def check_path(path): + assert 'error' in self.conf(path, 'applications/path/path') + + check_path('{}') + check_path('["/blah", []]') + def test_python_application_threads(self): self.load('threads', threads=4) diff --git a/test/test_python_basic.py b/test/test_python_basic.py index 0cc70e51..e661a89c 100644 --- a/test/test_python_basic.py +++ b/test/test_python_basic.py @@ -58,7 +58,7 @@ class TestPythonBasic(TestControl): assert self.conf_get('applications/app/processes/spare') == 0, 'spare' def test_python_get_listeners(self): - self.conf(self.conf_basic) + assert 'success' in self.conf(self.conf_basic) assert self.conf_get()['listeners'] == { "*:7080": {"pass": "applications/app"} @@ -73,16 +73,20 @@ class TestPythonBasic(TestControl): }, 'listeners prefix 2' def test_python_change_listener(self): - self.conf(self.conf_basic) - self.conf({"*:7081": {"pass": "applications/app"}}, 'listeners') + assert 'success' in self.conf(self.conf_basic) + assert 'success' in self.conf( + {"*:7081": {"pass": "applications/app"}}, 'listeners' + ) assert self.conf_get('listeners') == { "*:7081": {"pass": "applications/app"} }, 'change listener' def test_python_add_listener(self): - self.conf(self.conf_basic) - self.conf({"pass": "applications/app"}, 'listeners/*:7082') + assert 'success' in self.conf(self.conf_basic) + assert 'success' in self.conf( + {"pass": "applications/app"}, 'listeners/*:7082' + ) assert self.conf_get('listeners') == { "*:7080": {"pass": "applications/app"}, @@ -90,20 +94,20 @@ class TestPythonBasic(TestControl): }, 'add listener' def test_python_change_application(self): - self.conf(self.conf_basic) + assert 'success' in self.conf(self.conf_basic) - self.conf('30', 'applications/app/processes/max') + assert 'success' in self.conf('30', 'applications/app/processes/max') assert ( self.conf_get('applications/app/processes/max') == 30 ), 'change application max' - self.conf('"/www"', 'applications/app/path') + assert 'success' in self.conf('"/www"', 'applications/app/path') assert ( self.conf_get('applications/app/path') == '/www' ), 'change application path' def test_python_delete(self): - self.conf(self.conf_basic) + assert 'success' in self.conf(self.conf_basic) assert 'error' in self.conf_delete('applications/app') assert 'success' in self.conf_delete('listeners/*:7080') @@ -111,7 +115,7 @@ class TestPythonBasic(TestControl): assert 'error' in self.conf_delete('applications/app') def test_python_delete_blocks(self): - self.conf(self.conf_basic) + assert 'success' in self.conf(self.conf_basic) assert 'success' in self.conf_delete('listeners') assert 'success' in self.conf_delete('applications') diff --git a/test/test_python_isolation.py b/test/test_python_isolation.py index 1a157528..680f2c03 100644 --- a/test/test_python_isolation.py +++ b/test/test_python_isolation.py @@ -1,31 +1,15 @@ -import shutil import pytest - -from conftest import option -from conftest import unit_run -from conftest import unit_stop from unit.applications.lang.python import TestApplicationPython -from unit.feature.isolation import TestFeatureIsolation +from unit.option import option +from unit.utils import findmnt +from unit.utils import waitformount +from unit.utils import waitforunmount class TestPythonIsolation(TestApplicationPython): prerequisites = {'modules': {'python': 'any'}, 'features': ['isolation']} - @classmethod - def setup_class(cls, complete_check=True): - check = super().setup_class(complete_check=False) - - unit = unit_run() - option.temp_dir = unit['temp_dir'] - - TestFeatureIsolation().check(option.available, unit['temp_dir']) - - assert unit_stop() is None - shutil.rmtree(unit['temp_dir']) - - return check if not complete_check else check() - def test_python_isolation_rootfs(self, is_su, temp_dir): isolation_features = option.available['features']['isolation'].keys() @@ -79,39 +63,51 @@ class TestPythonIsolation(TestApplicationPython): ), 'application exists in rootfs' def test_python_isolation_rootfs_no_language_deps(self, is_su, temp_dir): - isolation_features = option.available['features']['isolation'].keys() - if not is_su: - if not 'unprivileged_userns_clone' in isolation_features: - pytest.skip('requires unprivileged userns or root') - - if 'user' not in isolation_features: - pytest.skip('user namespace is not supported') - - if 'mnt' not in isolation_features: - pytest.skip('mnt namespace is not supported') - - if 'pid' not in isolation_features: - pytest.skip('pid namespace is not supported') + pytest.skip('requires root') isolation = { 'rootfs': temp_dir, 'automount': {'language_deps': False} } - if not is_su: - isolation['namespaces'] = { - 'mount': True, - 'credential': True, - 'pid': True - } - self.load('empty', isolation=isolation) + assert findmnt().find(temp_dir) == -1 assert (self.get()['status'] != 200), 'disabled language_deps' + assert findmnt().find(temp_dir) == -1 isolation['automount']['language_deps'] = True self.load('empty', isolation=isolation) + assert findmnt().find(temp_dir) == -1 assert (self.get()['status'] == 200), 'enabled language_deps' + assert waitformount(temp_dir), 'language_deps mount' + + self.conf({"listeners": {}, "applications": {}}) + + assert waitforunmount(temp_dir), 'language_deps unmount' + + def test_python_isolation_procfs(self, is_su, temp_dir): + isolation_features = option.available['features']['isolation'].keys() + + if not is_su: + pytest.skip('requires root') + + isolation = {'rootfs': temp_dir, 'automount': {'procfs': False}} + + self.load('ns_inspect', isolation=isolation) + + assert ( + self.getjson(url='/?path=/proc/self')['body']['FileExists'] + == False + ), 'no /proc/self' + + isolation['automount']['procfs'] = True + + self.load('ns_inspect', isolation=isolation) + + assert ( + self.getjson(url='/?path=/proc/self')['body']['FileExists'] == True + ), '/proc/self' diff --git a/test/test_python_isolation_chroot.py b/test/test_python_isolation_chroot.py index 8018d5b9..7281f5f6 100644 --- a/test/test_python_isolation_chroot.py +++ b/test/test_python_isolation_chroot.py @@ -1,7 +1,5 @@ import pytest - from unit.applications.lang.python import TestApplicationPython -from unit.feature.isolation import TestFeatureIsolation class TestPythonIsolation(TestApplicationPython): diff --git a/test/test_python_procman.py b/test/test_python_procman.py index ff914fc8..ac403ce4 100644 --- a/test/test_python_procman.py +++ b/test/test_python_procman.py @@ -3,9 +3,8 @@ import subprocess import time import pytest - -from conftest import option from unit.applications.lang.python import TestApplicationPython +from unit.option import option class TestPythonProcman(TestApplicationPython): @@ -198,6 +197,6 @@ class TestPythonProcman(TestApplicationPython): ), 'max zero' def stop_all(self): - self.conf({"listeners": {}, "applications": {}}) + assert 'success' in self.conf({"listeners": {}, "applications": {}}) assert len(self.pids_for_process()) == 0, 'stop all' diff --git a/test/test_respawn.py b/test/test_respawn.py index 09a806d4..ed85ee95 100644 --- a/test/test_respawn.py +++ b/test/test_respawn.py @@ -2,9 +2,8 @@ import re import subprocess import time -from conftest import option -from conftest import skip_alert from unit.applications.lang.python import TestApplicationPython +from unit.option import option class TestRespawn(TestApplicationPython): @@ -22,17 +21,17 @@ class TestRespawn(TestApplicationPython): '1', 'applications/' + self.app_name + '/processes' ) - def pid_by_name(self, name): - output = subprocess.check_output(['ps', 'ax']).decode() - m = re.search(r'\s*(\d+).*' + name, output) - return m if m is None else m.group(1) + def pid_by_name(self, name, ppid): + output = subprocess.check_output(['ps', 'ax', '-O', 'ppid']).decode() + m = re.search(r'\s*(\d+)\s*' + str(ppid) + r'.*' + name, output) + return None if m is None else m.group(1) def kill_pids(self, *pids): subprocess.call(['kill', '-9'] + list(pids)) - def wait_for_process(self, process): + def wait_for_process(self, process, unit_pid): for i in range(50): - found = self.pid_by_name(process) + found = self.pid_by_name(process, unit_pid) if found is not None: break @@ -41,7 +40,10 @@ class TestRespawn(TestApplicationPython): return found - def smoke_test(self): + def find_proc(self, name, ppid, ps_output): + return re.findall(str(ppid) + r'.*' + name, ps_output) + + def smoke_test(self, unit_pid): for _ in range(5): assert 'success' in self.conf( '1', 'applications/' + self.app_name + '/processes' @@ -51,39 +53,41 @@ class TestRespawn(TestApplicationPython): # Check if the only one router, controller, # and application processes running. - output = subprocess.check_output(['ps', 'ax']).decode() - assert len(re.findall(self.PATTERN_ROUTER, output)) == 1 - assert len(re.findall(self.PATTERN_CONTROLLER, output)) == 1 - assert len(re.findall(self.app_name, output)) == 1 + out = subprocess.check_output(['ps', 'ax', '-O', 'ppid']).decode() + assert len(self.find_proc(self.PATTERN_ROUTER, unit_pid, out)) == 1 + assert len(self.find_proc(self.PATTERN_CONTROLLER, unit_pid, out)) == 1 + assert len(self.find_proc(self.app_name, unit_pid, out)) == 1 - def test_respawn_router(self): - pid = self.pid_by_name(self.PATTERN_ROUTER) + def test_respawn_router(self, skip_alert, unit_pid): + pid = self.pid_by_name(self.PATTERN_ROUTER, unit_pid) self.kill_pids(pid) skip_alert(r'process %s exited on signal 9' % pid) - assert self.wait_for_process(self.PATTERN_ROUTER) is not None + assert self.wait_for_process(self.PATTERN_ROUTER, unit_pid) is not None - self.smoke_test() + self.smoke_test(unit_pid) - def test_respawn_controller(self): - pid = self.pid_by_name(self.PATTERN_CONTROLLER) + def test_respawn_controller(self, skip_alert, unit_pid): + pid = self.pid_by_name(self.PATTERN_CONTROLLER, unit_pid) self.kill_pids(pid) skip_alert(r'process %s exited on signal 9' % pid) - assert self.wait_for_process(self.PATTERN_CONTROLLER) is not None + assert self.wait_for_process( + self.PATTERN_CONTROLLER, unit_pid + ) is not None assert self.get()['status'] == 200 - self.smoke_test() + self.smoke_test(unit_pid) - def test_respawn_application(self): - pid = self.pid_by_name(self.app_name) + def test_respawn_application(self, skip_alert, unit_pid): + pid = self.pid_by_name(self.app_name, unit_pid) self.kill_pids(pid) skip_alert(r'process %s exited on signal 9' % pid) - assert self.wait_for_process(self.app_name) is not None + assert self.wait_for_process(self.app_name, unit_pid) is not None - self.smoke_test() + self.smoke_test(unit_pid) diff --git a/test/test_routing.py b/test/test_routing.py index 83852273..4d27cb61 100644 --- a/test/test_routing.py +++ b/test/test_routing.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- import pytest - -from conftest import option -from conftest import skip_alert from unit.applications.proto import TestApplicationProto +from unit.option import option class TestRouting(TestApplicationProto): @@ -318,7 +316,7 @@ class TestRouting(TestApplicationProto): check_pass_error("%1", "%1") def test_routes_absent(self): - self.conf( + assert 'success' in self.conf( { "listeners": {"*:7081": {"pass": "applications/empty"}}, "applications": { @@ -366,7 +364,7 @@ class TestRouting(TestApplicationProto): assert self.get()['status'] == 200, 'route match absent' - def test_routes_route_action_absent(self): + def test_routes_route_action_absent(self, skip_alert): skip_alert(r'failed to apply new conf') assert 'error' in self.conf( @@ -755,7 +753,7 @@ class TestRouting(TestApplicationProto): 'routes/main' ), 'route edit configure 9' - def test_match_edit(self): + def test_match_edit(self, skip_alert): skip_alert(r'failed to apply new conf') self.route_match({"method": ["GET", "POST"]}) @@ -1352,7 +1350,7 @@ class TestRouting(TestApplicationProto): assert self.get(url='/?var2=val2')['status'] == 404, 'arr 7' assert self.get(url='/?var3=foo')['status'] == 200, 'arr 8' - def test_routes_match_arguments_invalid(self): + def test_routes_match_arguments_invalid(self, skip_alert): # TODO remove it after controller fixed skip_alert(r'failed to apply new conf') diff --git a/test/test_ruby_application.py b/test/test_ruby_application.py index e42fb97f..b18a6cee 100644 --- a/test/test_ruby_application.py +++ b/test/test_ruby_application.py @@ -1,9 +1,6 @@ import re import pytest - -from conftest import skip_alert -from conftest import unit_stop from unit.applications.lang.ruby import TestApplicationRuby @@ -160,7 +157,7 @@ class TestRubyApplication(TestApplicationRuby): assert self.post(body=body)['body'] == body, 'input rewind' @pytest.mark.skip('not yet') - def test_ruby_application_syntax_error(self): + def test_ruby_application_syntax_error(self, skip_alert): skip_alert( r'Failed to parse rack script', r'syntax error', @@ -176,8 +173,6 @@ class TestRubyApplication(TestApplicationRuby): self.get() - unit_stop() - assert ( self.wait_for_record(r'\[error\].+Error in application') is not None @@ -188,8 +183,6 @@ class TestRubyApplication(TestApplicationRuby): self.get() - unit_stop() - assert ( self.wait_for_record(r'\[error\].+1234567890') is not None ), 'errors puts int' @@ -199,8 +192,6 @@ class TestRubyApplication(TestApplicationRuby): self.get() - unit_stop() - assert ( self.wait_for_record(r'\[error\].+Error in application') is not None @@ -216,7 +207,6 @@ class TestRubyApplication(TestApplicationRuby): self.get() - unit_stop() assert ( self.wait_for_record(r'\[error\].+1234567890') is not None @@ -227,9 +217,7 @@ class TestRubyApplication(TestApplicationRuby): self.get() - self.conf({"listeners": {}, "applications": {}}) - - unit_stop() + assert 'success' in self.conf({"listeners": {}, "applications": {}}) assert ( self.wait_for_record(r'\[error\].+At exit called\.') is not None @@ -290,8 +278,6 @@ class TestRubyApplication(TestApplicationRuby): assert self.get()['status'] == 500, 'body each error status' - unit_stop() - assert ( self.wait_for_record(r'\[error\].+Failed to run ruby script') is not None diff --git a/test/test_ruby_isolation.py b/test/test_ruby_isolation.py index 69e25de9..c49b6732 100644 --- a/test/test_ruby_isolation.py +++ b/test/test_ruby_isolation.py @@ -1,32 +1,15 @@ +import os import shutil import pytest - -from conftest import option -from conftest import unit_run -from conftest import unit_stop from unit.applications.lang.ruby import TestApplicationRuby -from unit.feature.isolation import TestFeatureIsolation +from unit.option import option class TestRubyIsolation(TestApplicationRuby): prerequisites = {'modules': {'ruby': 'any'}, 'features': ['isolation']} - @classmethod - def setup_class(cls, complete_check=True): - check = super().setup_class(complete_check=False) - - unit = unit_run() - option.temp_dir = unit['temp_dir'] - - TestFeatureIsolation().check(option.available, unit['temp_dir']) - - assert unit_stop() is None - shutil.rmtree(unit['temp_dir']) - - return check if not complete_check else check() - - def test_ruby_isolation_rootfs_mount_namespace(self, is_su): + def test_ruby_isolation_rootfs(self, is_su): isolation_features = option.available['features']['isolation'].keys() if not is_su: @@ -42,34 +25,22 @@ class TestRubyIsolation(TestApplicationRuby): if 'pid' not in isolation_features: pytest.skip('pid namespace is not supported') - isolation = {'rootfs': option.test_dir} + isolation = {'rootfs': option.temp_dir} if not is_su: isolation['namespaces'] = { 'mount': True, 'credential': True, - 'pid': True + 'pid': True, } - self.load('status_int', isolation=isolation) - - assert 'success' in self.conf( - '"/ruby/status_int/config.ru"', 'applications/status_int/script', - ) + os.mkdir(option.temp_dir + '/ruby') - assert 'success' in self.conf( - '"/ruby/status_int"', 'applications/status_int/working_directory', + shutil.copytree( + option.test_dir + '/ruby/status_int', + option.temp_dir + '/ruby/status_int', ) - assert self.get()['status'] == 200, 'status int' - - def test_ruby_isolation_rootfs(self, is_su): - if not is_su: - pytest.skip('requires root') - return - - isolation = {'rootfs': option.test_dir} - self.load('status_int', isolation=isolation) assert 'success' in self.conf( diff --git a/test/test_settings.py b/test/test_settings.py index 22830a3b..c59ca8b9 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -3,7 +3,6 @@ import socket import time import pytest - from unit.applications.lang.python import TestApplicationPython @@ -297,4 +296,3 @@ Connection: close assert bool(resp), 'response from application 4' assert resp['status'] == 200, 'status 4' assert resp['body'] == body, 'body 4' - diff --git a/test/test_share_fallback.py b/test/test_share_fallback.py index 462da9de..a02cb1a3 100644 --- a/test/test_share_fallback.py +++ b/test/test_share_fallback.py @@ -1,8 +1,7 @@ import os -from conftest import option -from conftest import skip_alert from unit.applications.proto import TestApplicationProto +from unit.option import option class TestStatic(TestApplicationProto): @@ -117,7 +116,7 @@ class TestStatic(TestApplicationProto): assert resp['status'] == 200, 'fallback proxy status' assert resp['body'] == '', 'fallback proxy' - def test_fallback_proxy_loop(self): + def test_fallback_proxy_loop(self, skip_alert): skip_alert( r'open.*/blah/index.html.*failed', r'accept.*failed', diff --git a/test/test_static.py b/test/test_static.py index a65928ca..3e85b435 100644 --- a/test/test_static.py +++ b/test/test_static.py @@ -2,10 +2,9 @@ import os import socket import pytest - -from conftest import option -from conftest import waitforfiles from unit.applications.proto import TestApplicationProto +from unit.option import option +from unit.utils import waitforfiles class TestStatic(TestApplicationProto): diff --git a/test/test_tls.py b/test/test_tls.py index 4cf8d22c..89c57d07 100644 --- a/test/test_tls.py +++ b/test/test_tls.py @@ -4,10 +4,8 @@ import ssl import subprocess import pytest - -from conftest import option -from conftest import skip_alert from unit.applications.tls import TestApplicationTLS +from unit.option import option class TestTLS(TestApplicationTLS): @@ -21,7 +19,7 @@ class TestTLS(TestApplicationTLS): return self.date_to_sec_epoch(date, '%b %d %H:%M:%S %Y %Z') def add_tls(self, application='empty', cert='default', port=7080): - self.conf( + assert 'success' in self.conf( { "pass": "applications/" + application, "tls": {"certificate": cert} @@ -30,7 +28,7 @@ class TestTLS(TestApplicationTLS): ) def remove_tls(self, application='empty', port=7080): - self.conf( + assert 'success' in self.conf( {"pass": "applications/" + application}, 'listeners/*:' + str(port) ) @@ -479,8 +477,10 @@ basicConstraints = critical,CA:TRUE""" read_timeout=1, ) - self.conf({"pass": "applications/empty"}, 'listeners/*:7080') - self.conf_delete('/certificates/default') + assert 'success' in self.conf( + {"pass": "applications/empty"}, 'listeners/*:7080' + ) + assert 'success' in self.conf_delete('/certificates/default') try: resp = self.get_ssl( @@ -505,12 +505,12 @@ basicConstraints = critical,CA:TRUE""" '/certificates' ), 'remove all certificates' - def test_tls_application_respawn(self): + def test_tls_application_respawn(self, skip_alert): self.load('mirror') self.certificate() - self.conf('1', 'applications/mirror/processes') + assert 'success' in self.conf('1', 'applications/mirror/processes') self.add_tls(application='mirror') diff --git a/test/test_upstreams_rr.py b/test/test_upstreams_rr.py index c20d6054..163eb646 100644 --- a/test/test_upstreams_rr.py +++ b/test/test_upstreams_rr.py @@ -1,8 +1,8 @@ import os import re -from conftest import option from unit.applications.lang.python import TestApplicationPython +from unit.option import option class TestUpstreamsRR(TestApplicationPython): diff --git a/test/test_usr1.py b/test/test_usr1.py index 3e44e4c5..dbb5265c 100644 --- a/test/test_usr1.py +++ b/test/test_usr1.py @@ -1,9 +1,8 @@ import os from subprocess import call -from conftest import unit_stop -from conftest import waitforfiles from unit.applications.lang.python import TestApplicationPython +from unit.utils import waitforfiles class TestUSR1(TestApplicationPython): @@ -41,8 +40,6 @@ class TestUSR1(TestApplicationPython): assert self.get(url='/usr1')['status'] == 200 - unit_stop() - assert ( self.wait_for_record(r'"GET /usr1 HTTP/1.1" 200 0 "-" "-"', log) is not None @@ -74,8 +71,6 @@ class TestUSR1(TestApplicationPython): body = 'body_for_a_log_unit' assert self.post(body=body)['status'] == 200 - unit_stop() - assert self.wait_for_record(body) is not None, 'rename new' assert self.search_in_log(body, log_new) is None, 'rename new 2' diff --git a/test/unit/applications/lang/go.py b/test/unit/applications/lang/go.py index 866dec47..a17b1af4 100644 --- a/test/unit/applications/lang/go.py +++ b/test/unit/applications/lang/go.py @@ -1,8 +1,8 @@ import os import subprocess -from conftest import option from unit.applications.proto import TestApplicationProto +from unit.option import option class TestApplicationGo(TestApplicationProto): @@ -12,6 +12,7 @@ class TestApplicationGo(TestApplicationProto): env = os.environ.copy() env['GOPATH'] = option.current_dir + '/build/go' + env['GOCACHE'] = option.cache_dir + '/go' if static: args = [ diff --git a/test/unit/applications/lang/java.py b/test/unit/applications/lang/java.py index 0ff85187..b2e17f23 100644 --- a/test/unit/applications/lang/java.py +++ b/test/unit/applications/lang/java.py @@ -4,8 +4,8 @@ import shutil import subprocess import pytest -from conftest import option from unit.applications.proto import TestApplicationProto +from unit.option import option class TestApplicationJava(TestApplicationProto): diff --git a/test/unit/applications/lang/node.py b/test/unit/applications/lang/node.py index 98fd9ffc..cc6d06ef 100644 --- a/test/unit/applications/lang/node.py +++ b/test/unit/applications/lang/node.py @@ -1,9 +1,9 @@ import shutil from urllib.parse import quote -from conftest import option -from conftest import public_dir from unit.applications.proto import TestApplicationProto +from unit.option import option +from unit.utils import public_dir class TestApplicationNode(TestApplicationProto): diff --git a/test/unit/applications/lang/perl.py b/test/unit/applications/lang/perl.py index 9dc24ace..58b867f0 100644 --- a/test/unit/applications/lang/perl.py +++ b/test/unit/applications/lang/perl.py @@ -1,5 +1,5 @@ -from conftest import option from unit.applications.proto import TestApplicationProto +from unit.option import option class TestApplicationPerl(TestApplicationProto): diff --git a/test/unit/applications/lang/php.py b/test/unit/applications/lang/php.py index 3dbb32f5..90c0078c 100644 --- a/test/unit/applications/lang/php.py +++ b/test/unit/applications/lang/php.py @@ -1,8 +1,8 @@ -from conftest import option import os import shutil from unit.applications.proto import TestApplicationProto +from unit.option import option class TestApplicationPHP(TestApplicationProto): diff --git a/test/unit/applications/lang/python.py b/test/unit/applications/lang/python.py index 792a86fa..287d23f0 100644 --- a/test/unit/applications/lang/python.py +++ b/test/unit/applications/lang/python.py @@ -3,8 +3,8 @@ import shutil from urllib.parse import quote import pytest -from conftest import option from unit.applications.proto import TestApplicationProto +from unit.option import option class TestApplicationPython(TestApplicationProto): diff --git a/test/unit/applications/lang/ruby.py b/test/unit/applications/lang/ruby.py index 82d66e65..02644584 100644 --- a/test/unit/applications/lang/ruby.py +++ b/test/unit/applications/lang/ruby.py @@ -1,5 +1,5 @@ -from conftest import option from unit.applications.proto import TestApplicationProto +from unit.option import option class TestApplicationRuby(TestApplicationProto): diff --git a/test/unit/applications/proto.py b/test/unit/applications/proto.py index 6e760c70..af05d071 100644 --- a/test/unit/applications/proto.py +++ b/test/unit/applications/proto.py @@ -2,8 +2,8 @@ import os import re import time -from conftest import option from unit.control import TestControl +from unit.option import option class TestApplicationProto(TestControl): diff --git a/test/unit/applications/tls.py b/test/unit/applications/tls.py index fb1b112c..b0cd5abb 100644 --- a/test/unit/applications/tls.py +++ b/test/unit/applications/tls.py @@ -2,8 +2,8 @@ import os import ssl import subprocess -from conftest import option from unit.applications.proto import TestApplicationProto +from unit.option import option class TestApplicationTLS(TestApplicationProto): diff --git a/test/unit/check/isolation.py b/test/unit/check/isolation.py new file mode 100644 index 00000000..fe5a41f8 --- /dev/null +++ b/test/unit/check/isolation.py @@ -0,0 +1,158 @@ +import json +import os + +from unit.applications.lang.go import TestApplicationGo +from unit.applications.lang.java import TestApplicationJava +from unit.applications.lang.node import TestApplicationNode +from unit.applications.proto import TestApplicationProto +from unit.http import TestHTTP +from unit.option import option +from unit.utils import getns + +allns = ['pid', 'mnt', 'ipc', 'uts', 'cgroup', 'net'] +http = TestHTTP() + +def check_isolation(): + test_conf = {"namespaces": {"credential": True}} + available = option.available + + conf = '' + if 'go' in available['modules']: + TestApplicationGo().prepare_env('empty', 'app') + + conf = { + "listeners": {"*:7080": {"pass": "applications/empty"}}, + "applications": { + "empty": { + "type": "external", + "processes": {"spare": 0}, + "working_directory": option.test_dir + "/go/empty", + "executable": option.temp_dir + "/go/app", + "isolation": {"namespaces": {"credential": True}}, + }, + }, + } + + elif 'python' in available['modules']: + conf = { + "listeners": {"*:7080": {"pass": "applications/empty"}}, + "applications": { + "empty": { + "type": "python", + "processes": {"spare": 0}, + "path": option.test_dir + "/python/empty", + "working_directory": option.test_dir + "/python/empty", + "module": "wsgi", + "isolation": {"namespaces": {"credential": True}}, + } + }, + } + + elif 'php' in available['modules']: + conf = { + "listeners": {"*:7080": {"pass": "applications/phpinfo"}}, + "applications": { + "phpinfo": { + "type": "php", + "processes": {"spare": 0}, + "root": option.test_dir + "/php/phpinfo", + "working_directory": option.test_dir + "/php/phpinfo", + "index": "index.php", + "isolation": {"namespaces": {"credential": True}}, + } + }, + } + + elif 'ruby' in available['modules']: + conf = { + "listeners": {"*:7080": {"pass": "applications/empty"}}, + "applications": { + "empty": { + "type": "ruby", + "processes": {"spare": 0}, + "working_directory": option.test_dir + "/ruby/empty", + "script": option.test_dir + "/ruby/empty/config.ru", + "isolation": {"namespaces": {"credential": True}}, + } + }, + } + + elif 'java' in available['modules']: + TestApplicationJava().prepare_env('empty') + + conf = { + "listeners": {"*:7080": {"pass": "applications/empty"}}, + "applications": { + "empty": { + "unit_jars": option.current_dir + "/build", + "type": "java", + "processes": {"spare": 0}, + "working_directory": option.test_dir + "/java/empty/", + "webapp": option.temp_dir + "/java", + "isolation": {"namespaces": {"credential": True}}, + } + }, + } + + elif 'node' in available['modules']: + TestApplicationNode().prepare_env('basic') + + conf = { + "listeners": {"*:7080": {"pass": "applications/basic"}}, + "applications": { + "basic": { + "type": "external", + "processes": {"spare": 0}, + "working_directory": option.temp_dir + "/node", + "executable": "app.js", + "isolation": {"namespaces": {"credential": True}}, + } + }, + } + + elif 'perl' in available['modules']: + conf = { + "listeners": {"*:7080": {"pass": "applications/body_empty"}}, + "applications": { + "body_empty": { + "type": "perl", + "processes": {"spare": 0}, + "working_directory": option.test_dir + + "/perl/body_empty", + "script": option.test_dir + "/perl/body_empty/psgi.pl", + "isolation": {"namespaces": {"credential": True}}, + } + }, + } + + else: + return + + resp = http.put( + url='/config', + sock_type='unix', + addr=option.temp_dir + '/control.unit.sock', + body=json.dumps(conf), + ) + + if 'success' not in resp['body']: + return + + userns = getns('user') + if not userns: + return + + available['features']['isolation'] = {'user': userns} + + unp_clone_path = '/proc/sys/kernel/unprivileged_userns_clone' + if os.path.exists(unp_clone_path): + with open(unp_clone_path, 'r') as f: + if str(f.read()).rstrip() == '1': + available['features']['isolation'][ + 'unprivileged_userns_clone' + ] = True + + for ns in allns: + ns_value = getns(ns) + if ns_value: + available['features']['isolation'][ns] = ns_value diff --git a/test/unit/control.py b/test/unit/control.py index f05aa827..3008a64b 100644 --- a/test/unit/control.py +++ b/test/unit/control.py @@ -1,7 +1,7 @@ import json -from conftest import option from unit.http import TestHTTP +from unit.option import option def args_handler(conf_func): diff --git a/test/unit/feature/isolation.py b/test/unit/feature/isolation.py deleted file mode 100644 index 7877c03a..00000000 --- a/test/unit/feature/isolation.py +++ /dev/null @@ -1,160 +0,0 @@ -import os - -from unit.applications.lang.go import TestApplicationGo -from unit.applications.lang.java import TestApplicationJava -from unit.applications.lang.node import TestApplicationNode -from unit.applications.proto import TestApplicationProto -from conftest import option - - -class TestFeatureIsolation(TestApplicationProto): - allns = ['pid', 'mnt', 'ipc', 'uts', 'cgroup', 'net'] - - def check(self, available, temp_dir): - test_conf = {"namespaces": {"credential": True}} - - conf = '' - if 'go' in available['modules']: - TestApplicationGo().prepare_env('empty', 'app') - - conf = { - "listeners": {"*:7080": {"pass": "applications/empty"}}, - "applications": { - "empty": { - "type": "external", - "processes": {"spare": 0}, - "working_directory": option.test_dir + "/go/empty", - "executable": option.temp_dir + "/go/app", - "isolation": {"namespaces": {"credential": True}}, - }, - }, - } - - elif 'python' in available['modules']: - conf = { - "listeners": {"*:7080": {"pass": "applications/empty"}}, - "applications": { - "empty": { - "type": "python", - "processes": {"spare": 0}, - "path": option.test_dir + "/python/empty", - "working_directory": option.test_dir + "/python/empty", - "module": "wsgi", - "isolation": {"namespaces": {"credential": True}}, - } - }, - } - - elif 'php' in available['modules']: - conf = { - "listeners": {"*:7080": {"pass": "applications/phpinfo"}}, - "applications": { - "phpinfo": { - "type": "php", - "processes": {"spare": 0}, - "root": option.test_dir + "/php/phpinfo", - "working_directory": option.test_dir + "/php/phpinfo", - "index": "index.php", - "isolation": {"namespaces": {"credential": True}}, - } - }, - } - - elif 'ruby' in available['modules']: - conf = { - "listeners": {"*:7080": {"pass": "applications/empty"}}, - "applications": { - "empty": { - "type": "ruby", - "processes": {"spare": 0}, - "working_directory": option.test_dir + "/ruby/empty", - "script": option.test_dir + "/ruby/empty/config.ru", - "isolation": {"namespaces": {"credential": True}}, - } - }, - } - - elif 'java' in available['modules']: - TestApplicationJava().prepare_env('empty') - - conf = { - "listeners": {"*:7080": {"pass": "applications/empty"}}, - "applications": { - "empty": { - "unit_jars": option.current_dir + "/build", - "type": "java", - "processes": {"spare": 0}, - "working_directory": option.test_dir + "/java/empty/", - "webapp": option.temp_dir + "/java", - "isolation": {"namespaces": {"credential": True}}, - } - }, - } - - elif 'node' in available['modules']: - TestApplicationNode().prepare_env('basic') - - conf = { - "listeners": {"*:7080": {"pass": "applications/basic"}}, - "applications": { - "basic": { - "type": "external", - "processes": {"spare": 0}, - "working_directory": option.temp_dir + "/node", - "executable": "app.js", - "isolation": {"namespaces": {"credential": True}}, - } - }, - } - - elif 'perl' in available['modules']: - conf = { - "listeners": {"*:7080": {"pass": "applications/body_empty"}}, - "applications": { - "body_empty": { - "type": "perl", - "processes": {"spare": 0}, - "working_directory": option.test_dir - + "/perl/body_empty", - "script": option.test_dir + "/perl/body_empty/psgi.pl", - "isolation": {"namespaces": {"credential": True}}, - } - }, - } - - else: - return - - if 'success' not in self.conf(conf): - return - - userns = self.getns('user') - if not userns: - return - - available['features']['isolation'] = {'user': userns} - - unp_clone_path = '/proc/sys/kernel/unprivileged_userns_clone' - if os.path.exists(unp_clone_path): - with open(unp_clone_path, 'r') as f: - if str(f.read()).rstrip() == '1': - available['features']['isolation'][ - 'unprivileged_userns_clone' - ] = True - - for ns in self.allns: - ns_value = self.getns(ns) - if ns_value: - available['features']['isolation'][ns] = ns_value - - def getns(self, nstype): - # read namespace id from symlink file: - # it points to: '<nstype>:[<ns id>]' - # # eg.: 'pid:[4026531836]' - nspath = '/proc/self/ns/' + nstype - data = None - - if os.path.exists(nspath): - data = int(os.readlink(nspath)[len(nstype) + 2 : -1]) - - return data diff --git a/test/unit/http.py b/test/unit/http.py index 8d964978..57e6ed3a 100644 --- a/test/unit/http.py +++ b/test/unit/http.py @@ -7,11 +7,10 @@ import select import socket import pytest -from conftest import option -from unit.main import TestUnit +from unit.option import option -class TestHTTP(TestUnit): +class TestHTTP(): def http(self, start_str, **kwargs): sock_type = kwargs.get('sock_type', 'ipv4') port = kwargs.get('port', 7080) diff --git a/test/unit/main.py b/test/unit/main.py deleted file mode 100644 index 488b3f4d..00000000 --- a/test/unit/main.py +++ /dev/null @@ -1,42 +0,0 @@ -import pytest -from conftest import option - - -class TestUnit(): - @classmethod - def setup_class(cls, complete_check=True): - def check(): - missed = [] - - # check modules - - if 'modules' in cls.prerequisites: - available_modules = list(option.available['modules'].keys()) - - for module in cls.prerequisites['modules']: - if module in available_modules: - continue - - missed.append(module) - - if missed: - pytest.skip('Unit has no ' + ', '.join(missed) + ' module(s)') - - # check features - - if 'features' in cls.prerequisites: - available_features = list(option.available['features'].keys()) - - for feature in cls.prerequisites['features']: - if feature in available_features: - continue - - missed.append(feature) - - if missed: - pytest.skip(', '.join(missed) + ' feature(s) not supported') - - if complete_check: - check() - else: - return check diff --git a/test/unit/option.py b/test/unit/option.py new file mode 100644 index 00000000..677d806e --- /dev/null +++ b/test/unit/option.py @@ -0,0 +1,16 @@ +class Options(): + _options = { + 'skip_alerts': [], + 'skip_sanitizer': False, + } + + def __setattr__(self, name, value): + Options._options[name] = value + + def __getattr__(self, name): + if name in Options._options: + return Options._options[name] + + raise AttributeError + +option = Options() diff --git a/test/unit/utils.py b/test/unit/utils.py new file mode 100644 index 00000000..7a0a3fe5 --- /dev/null +++ b/test/unit/utils.py @@ -0,0 +1,94 @@ +import os +import socket +import subprocess +import time + +import pytest + + +def public_dir(path): + os.chmod(path, 0o777) + + for root, dirs, files in os.walk(path): + for d in dirs: + os.chmod(os.path.join(root, d), 0o777) + for f in files: + os.chmod(os.path.join(root, f), 0o777) + + +def waitforfiles(*files): + for i in range(50): + wait = False + + for f in files: + if not os.path.exists(f): + wait = True + break + + if not wait: + return True + + time.sleep(0.1) + + return False + + +def waitforsocket(port): + for i in range(50): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + try: + sock.settimeout(5) + sock.connect(('127.0.0.1', port)) + return + + except ConnectionRefusedError: + time.sleep(0.1) + + except KeyboardInterrupt: + raise + + pytest.fail('Can\'t connect to the 127.0.0.1:' + port) + + +def findmnt(): + try: + out = subprocess.check_output( + ['findmnt', '--raw'], stderr=subprocess.STDOUT + ).decode() + except FileNotFoundError: + pytest.skip('requires findmnt') + + return out + + +def waitformount(template, wait=50): + for i in range(wait): + if findmnt().find(template) != -1: + return True + + time.sleep(0.1) + + return False + + +def waitforunmount(template, wait=50): + for i in range(wait): + if findmnt().find(template) == -1: + return True + + time.sleep(0.1) + + return False + + +def getns(nstype): + # read namespace id from symlink file: + # it points to: '<nstype>:[<ns id>]' + # # eg.: 'pid:[4026531836]' + nspath = '/proc/self/ns/' + nstype + data = None + + if os.path.exists(nspath): + data = int(os.readlink(nspath)[len(nstype) + 2 : -1]) + + return data |