From 72420358be128640dd09a0914a8d88b50c152545 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Thu, 24 Jun 2021 04:01:15 +0100 Subject: Tests: chroot test with permissions skipped under root. --- test/test_share_chroot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_share_chroot.py b/test/test_share_chroot.py index 7e53d3f7..02b3657d 100644 --- a/test/test_share_chroot.py +++ b/test/test_share_chroot.py @@ -44,7 +44,10 @@ class TestShareChroot(TestApplicationProto): assert self.get(url='/index.html')['status'] == 403, 'chroot 403 2' assert self.get(url='/file')['status'] == 403, 'chroot 403' - def test_share_chroot_permission(self, temp_dir): + def test_share_chroot_permission(self, is_su, temp_dir): + if is_su: + pytest.skip('does\'t work under root') + os.chmod(temp_dir + '/assets/dir', 0o100) assert 'success' in self.conf( -- cgit From b86891c4ef848a2da05abd1350af5f0b8e4335fa Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Mon, 28 Jun 2021 22:05:40 +0100 Subject: Tests: renamed share to static. Also minor style changes. --- test/conftest.py | 2 +- test/test_configuration.py | 3 +- test/test_node_es_modules.py | 3 +- test/test_share_chroot.py | 111 ---------------------------- test/test_share_fallback.py | 148 ------------------------------------- test/test_share_mount.py | 142 ------------------------------------ test/test_share_symlink.py | 96 ------------------------ test/test_share_types.py | 170 ------------------------------------------- test/test_static_chroot.py | 108 +++++++++++++++++++++++++++ test/test_static_fallback.py | 149 +++++++++++++++++++++++++++++++++++++ test/test_static_mount.py | 140 +++++++++++++++++++++++++++++++++++ test/test_static_symlink.py | 94 ++++++++++++++++++++++++ test/test_static_types.py | 169 ++++++++++++++++++++++++++++++++++++++++++ 13 files changed, 665 insertions(+), 670 deletions(-) delete mode 100644 test/test_share_chroot.py delete mode 100644 test/test_share_fallback.py delete mode 100644 test/test_share_mount.py delete mode 100644 test/test_share_symlink.py delete mode 100644 test/test_share_types.py create mode 100644 test/test_static_chroot.py create mode 100644 test/test_static_fallback.py create mode 100644 test/test_static_mount.py create mode 100644 test/test_static_symlink.py create mode 100644 test/test_static_types.py (limited to 'test') diff --git a/test/conftest.py b/test/conftest.py index 5ea4e49d..db34984f 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -22,8 +22,8 @@ from unit.check.node import check_node from unit.check.regex import check_regex from unit.check.tls import check_openssl from unit.http import TestHTTP -from unit.option import option from unit.log import Log +from unit.option import option from unit.utils import public_dir from unit.utils import waitforfiles diff --git a/test/test_configuration.py b/test/test_configuration.py index 880aef6c..c149658c 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -1,6 +1,7 @@ +import socket + import pytest -import socket from unit.control import TestControl diff --git a/test/test_node_es_modules.py b/test/test_node_es_modules.py index 0945a967..5464d4a6 100644 --- a/test/test_node_es_modules.py +++ b/test/test_node_es_modules.py @@ -1,6 +1,7 @@ +from distutils.version import LooseVersion + import pytest -from distutils.version import LooseVersion from unit.applications.lang.node import TestApplicationNode from unit.applications.websockets import TestApplicationWebsocket diff --git a/test/test_share_chroot.py b/test/test_share_chroot.py deleted file mode 100644 index 02b3657d..00000000 --- a/test/test_share_chroot.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from pathlib import Path - -import pytest - -from unit.applications.proto import TestApplicationProto - - -class TestShareChroot(TestApplicationProto): - prerequisites = {'features': ['chroot']} - - @pytest.fixture(autouse=True) - def setup_method_fixture(self, temp_dir): - os.makedirs(temp_dir + '/assets/dir') - with open(temp_dir + '/assets/index.html', 'w') as index, open( - temp_dir + '/assets/dir/file', 'w' - ) as file: - index.write('0123456789') - file.write('blah') - - test = Path(__file__) - self.test_path = '/' + test.parent.name + '/' + test.name - - self._load_conf( - { - "listeners": {"*:7080": {"pass": "routes"}}, - "routes": [{"action": {"share": temp_dir + "/assets"}}], - } - ) - - def test_share_chroot(self, temp_dir): - assert self.get(url='/dir/file')['status'] == 200, 'default chroot' - assert self.get(url='/index.html')['status'] == 200, 'default chroot 2' - - assert 'success' in self.conf( - { - "share": temp_dir + "/assets", - "chroot": temp_dir + "/assets/dir", - }, - 'routes/0/action', - ), 'configure chroot' - - assert self.get(url='/dir/file')['status'] == 200, 'chroot' - assert self.get(url='/index.html')['status'] == 403, 'chroot 403 2' - assert self.get(url='/file')['status'] == 403, 'chroot 403' - - def test_share_chroot_permission(self, is_su, temp_dir): - if is_su: - pytest.skip('does\'t work under root') - - os.chmod(temp_dir + '/assets/dir', 0o100) - - assert 'success' in self.conf( - { - "share": temp_dir + "/assets", - "chroot": temp_dir + "/assets/dir", - }, - 'routes/0/action', - ), 'configure chroot' - - assert self.get(url='/dir/file')['status'] == 200, 'chroot' - - def test_share_chroot_empty(self, temp_dir): - assert 'success' in self.conf( - {"share": temp_dir + "/assets", "chroot": ""}, 'routes/0/action', - ), 'configure chroot empty absolute' - - assert ( - self.get(url='/dir/file')['status'] == 200 - ), 'chroot empty absolute' - - assert 'success' in self.conf( - {"share": ".", "chroot": ""}, 'routes/0/action', - ), 'configure chroot empty relative' - - assert ( - self.get(url=self.test_path)['status'] == 200 - ), 'chroot empty relative' - - def test_share_chroot_relative(self, is_su, temp_dir): - if is_su: - pytest.skip('does\'t work under root') - - assert 'success' in self.conf( - {"share": temp_dir + "/assets", "chroot": "."}, 'routes/0/action', - ), 'configure relative chroot' - - assert self.get(url='/dir/file')['status'] == 403, 'relative chroot' - - assert 'success' in self.conf( - {"share": "."}, 'routes/0/action', - ), 'configure relative share' - - assert self.get(url=self.test_path)['status'] == 200, 'relative share' - - assert 'success' in self.conf( - {"share": ".", "chroot": "."}, 'routes/0/action', - ), 'configure relative' - - assert self.get(url=self.test_path)['status'] == 200, 'relative' - - def test_share_chroot_invalid(self, temp_dir): - assert 'error' in self.conf( - {"share": temp_dir, "chroot": True}, 'routes/0/action', - ), 'configure chroot error' - assert 'error' in self.conf( - {"share": temp_dir, "symlinks": "True"}, 'routes/0/action', - ), 'configure symlink error' - assert 'error' in self.conf( - {"share": temp_dir, "mount": "True"}, 'routes/0/action', - ), 'configure mount error' diff --git a/test/test_share_fallback.py b/test/test_share_fallback.py deleted file mode 100644 index 0b1c270e..00000000 --- a/test/test_share_fallback.py +++ /dev/null @@ -1,148 +0,0 @@ -import os - -import pytest - -from unit.applications.proto import TestApplicationProto -from unit.option import option - - -class TestStatic(TestApplicationProto): - prerequisites = {} - - def setup_method(self): - os.makedirs(option.temp_dir + '/assets/dir') - with open(option.temp_dir + '/assets/index.html', 'w') as index: - index.write('0123456789') - - os.makedirs(option.temp_dir + '/assets/403') - os.chmod(option.temp_dir + '/assets/403', 0o000) - - self._load_conf( - { - "listeners": { - "*:7080": {"pass": "routes"}, - "*:7081": {"pass": "routes"}, - }, - "routes": [{"action": {"share": option.temp_dir + "/assets"}}], - "applications": {}, - } - ) - - def teardown_method(self): - try: - os.chmod(option.temp_dir + '/assets/403', 0o777) - except FileNotFoundError: - pass - - def action_update(self, conf): - assert 'success' in self.conf(conf, 'routes/0/action') - - def test_fallback(self): - self.action_update({"share": "/blah"}) - assert self.get()['status'] == 404, 'bad path no fallback' - - self.action_update({"share": "/blah", "fallback": {"return": 200}}) - - resp = self.get() - assert resp['status'] == 200, 'bad path fallback status' - assert resp['body'] == '', 'bad path fallback' - - def test_fallback_valid_path(self, temp_dir): - self.action_update( - {"share": temp_dir + "/assets", "fallback": {"return": 200}} - ) - resp = self.get() - assert resp['status'] == 200, 'fallback status' - assert resp['body'] == '0123456789', 'fallback' - - resp = self.get(url='/403/') - assert resp['status'] == 200, 'fallback status 403' - assert resp['body'] == '', 'fallback 403' - - resp = self.post() - assert resp['status'] == 200, 'fallback status 405' - assert resp['body'] == '', 'fallback 405' - - assert self.get(url='/dir')['status'] == 301, 'fallback status 301' - - def test_fallback_nested(self): - self.action_update( - { - "share": "/blah", - "fallback": { - "share": "/blah/blah", - "fallback": {"return": 200}, - }, - } - ) - - resp = self.get() - assert resp['status'] == 200, 'fallback nested status' - assert resp['body'] == '', 'fallback nested' - - def test_fallback_share(self, temp_dir): - self.action_update( - {"share": "/blah", "fallback": {"share": temp_dir + "/assets"},} - ) - - resp = self.get() - assert resp['status'] == 200, 'fallback share status' - assert resp['body'] == '0123456789', 'fallback share' - - resp = self.head() - assert resp['status'] == 200, 'fallback share status HEAD' - assert resp['body'] == '', 'fallback share HEAD' - - assert ( - self.get(url='/dir')['status'] == 301 - ), 'fallback share status 301' - - def test_fallback_proxy(self): - assert 'success' in self.conf( - [ - { - "match": {"destination": "*:7081"}, - "action": {"return": 200}, - }, - { - "action": { - "share": "/blah", - "fallback": {"proxy": "http://127.0.0.1:7081"}, - } - }, - ], - 'routes', - ), 'configure fallback proxy route' - - resp = self.get() - assert resp['status'] == 200, 'fallback proxy status' - assert resp['body'] == '', 'fallback proxy' - - @pytest.mark.skip('not yet') - def test_fallback_proxy_loop(self, skip_alert): - skip_alert( - r'open.*/blah/index.html.*failed', - r'accept.*failed', - r'socket.*failed', - r'new connections are not accepted', - ) - - self.action_update( - {"share": "/blah", "fallback": {"proxy": "http://127.0.0.1:7080"}} - ) - self.get(no_recv=True) - - assert 'success' in self.conf_delete('listeners/*:7081') - self.get(read_timeout=1) - - def test_fallback_invalid(self): - def check_error(conf): - assert 'error' in self.conf(conf, 'routes/0/action') - - check_error({"share": "/blah", "fallback": {}}) - check_error({"share": "/blah", "fallback": ""}) - check_error({"return": 200, "fallback": {"share": "/blah"}}) - check_error( - {"proxy": "http://127.0.0.1:7081", "fallback": {"share": "/blah"}} - ) - check_error({"fallback": {"share": "/blah"}}) diff --git a/test/test_share_mount.py b/test/test_share_mount.py deleted file mode 100644 index f22fbe75..00000000 --- a/test/test_share_mount.py +++ /dev/null @@ -1,142 +0,0 @@ -import os -import subprocess - -import pytest - -from unit.applications.proto import TestApplicationProto - - -class TestShareMount(TestApplicationProto): - prerequisites = {'features': ['chroot']} - - @pytest.fixture(autouse=True) - def setup_method_fixture(self, is_su, temp_dir): - if not is_su: - pytest.skip('requires root') - - os.makedirs(temp_dir + '/assets/dir/mount') - os.makedirs(temp_dir + '/assets/dir/dir') - os.makedirs(temp_dir + '/assets/mount') - with open(temp_dir + '/assets/index.html', 'w') as index, open( - temp_dir + '/assets/dir/dir/file', 'w' - ) as file, open(temp_dir + '/assets/mount/index.html', 'w') as mount: - index.write('index') - file.write('file') - mount.write('mount') - - try: - process = subprocess.Popen( - [ - "mount", - "--bind", - temp_dir + "/assets/mount", - temp_dir + "/assets/dir/mount", - ], - stderr=subprocess.STDOUT, - ) - - process.communicate() - - except KeyboardInterrupt: - raise - - except: - pytest.fail('Can\'t run mount process.') - - self._load_conf( - { - "listeners": {"*:7080": {"pass": "routes"}}, - "routes": [{"action": {"share": temp_dir + "/assets/dir"}}], - } - ) - - yield - - try: - process = subprocess.Popen( - ["umount", "--lazy", temp_dir + "/assets/dir/mount"], - stderr=subprocess.STDOUT, - ) - - process.communicate() - - except KeyboardInterrupt: - raise - - except: - pytest.fail('Can\'t run umount process.') - - def test_share_mount(self, temp_dir, skip_alert): - skip_alert(r'opening.*failed') - - resp = self.get(url='/mount/') - assert resp['status'] == 200 - assert resp['body'] == 'mount' - - assert 'success' in self.conf( - {"share": temp_dir + "/assets/dir", "traverse_mounts": False}, - 'routes/0/action', - ), 'configure mount disable' - - assert self.get(url='/mount/')['status'] == 403 - - assert 'success' in self.conf( - {"share": temp_dir + "/assets/dir", "traverse_mounts": True}, - 'routes/0/action', - ), 'configure mount enable' - - resp = self.get(url='/mount/') - assert resp['status'] == 200 - assert resp['body'] == 'mount' - - def test_share_mount_two_blocks(self, temp_dir, skip_alert): - skip_alert(r'opening.*failed') - - os.symlink(temp_dir + '/assets/dir', temp_dir + '/assets/link') - - assert 'success' in self.conf( - [ - { - "match": {"method": "HEAD"}, - "action": { - "share": temp_dir + "/assets/dir", - "traverse_mounts": False, - }, - }, - { - "match": {"method": "GET"}, - "action": { - "share": temp_dir + "/assets/dir", - "traverse_mounts": True, - }, - }, - ], - 'routes', - ), 'configure two options' - - assert self.get(url='/mount/')['status'] == 200, 'block enabled' - assert self.head(url='/mount/')['status'] == 403, 'block disabled' - - def test_share_mount_chroot(self, temp_dir, skip_alert): - skip_alert(r'opening.*failed') - - assert 'success' in self.conf( - { - "share": temp_dir + "/assets/dir", - "chroot": temp_dir + "/assets", - }, - 'routes/0/action', - ), 'configure chroot mount default' - - assert self.get(url='/mount/')['status'] == 200, 'chroot' - - assert 'success' in self.conf( - { - "share": temp_dir + "/assets/dir", - "chroot": temp_dir + "/assets", - "traverse_mounts": False, - }, - 'routes/0/action', - ), 'configure chroot mount disable' - - assert self.get(url='/mount/')['status'] == 403, 'chroot mount' diff --git a/test/test_share_symlink.py b/test/test_share_symlink.py deleted file mode 100644 index 3970b605..00000000 --- a/test/test_share_symlink.py +++ /dev/null @@ -1,96 +0,0 @@ -import os - -import pytest - -from unit.applications.proto import TestApplicationProto - - -class TestShareSymlink(TestApplicationProto): - prerequisites = {'features': ['chroot']} - - @pytest.fixture(autouse=True) - def setup_method_fixture(self, temp_dir): - os.makedirs(temp_dir + '/assets/dir/dir') - with open(temp_dir + '/assets/index.html', 'w') as index, open( - temp_dir + '/assets/dir/file', 'w' - ) as file: - index.write('0123456789') - file.write('blah') - - self._load_conf( - { - "listeners": {"*:7080": {"pass": "routes"}}, - "routes": [{"action": {"share": temp_dir + "/assets"}}], - } - ) - - def test_share_symlink(self, temp_dir, skip_alert): - skip_alert(r'opening.*failed') - - os.symlink(temp_dir + '/assets/dir', temp_dir + '/assets/link') - - assert self.get(url='/dir')['status'] == 301, 'dir' - assert self.get(url='/dir/file')['status'] == 200, 'file' - assert self.get(url='/link')['status'] == 301, 'symlink dir' - assert self.get(url='/link/file')['status'] == 200, 'symlink file' - - assert 'success' in self.conf( - {"share": temp_dir + "/assets", "follow_symlinks": False}, - 'routes/0/action', - ), 'configure symlink disable' - - assert self.get(url='/link/file')['status'] == 403, 'symlink disabled' - - assert 'success' in self.conf( - {"share": temp_dir + "/assets", "follow_symlinks": True}, - 'routes/0/action', - ), 'configure symlink enable' - - assert self.get(url='/link/file')['status'] == 200, 'symlink enabled' - - def test_share_symlink_two_blocks(self, temp_dir, skip_alert): - skip_alert(r'opening.*failed') - - os.symlink(temp_dir + '/assets/dir', temp_dir + '/assets/link') - - assert 'success' in self.conf( - [ - { - "match": {"method": "HEAD"}, - "action": { - "share": temp_dir + "/assets", - "follow_symlinks": False, - }, - }, - { - "match": {"method": "GET"}, - "action": { - "share": temp_dir + "/assets", - "follow_symlinks": True, - }, - }, - ], - 'routes', - ), 'configure two options' - - assert self.get(url='/link/file')['status'] == 200, 'block enabled' - assert self.head(url='/link/file')['status'] == 403, 'block disabled' - - def test_share_symlink_chroot(self, temp_dir, skip_alert): - skip_alert(r'opening.*failed') - - os.symlink( - temp_dir + '/assets/dir/file', temp_dir + '/assets/dir/dir/link' - ) - - assert self.get(url='/dir/dir/link')['status'] == 200, 'default chroot' - - assert 'success' in self.conf( - { - "share": temp_dir + "/assets", - "chroot": temp_dir + "/assets/dir/dir", - }, - 'routes/0/action', - ), 'configure chroot' - - assert self.get(url='/dir/dir/link')['status'] == 404, 'chroot' diff --git a/test/test_share_types.py b/test/test_share_types.py deleted file mode 100644 index b5ed97a0..00000000 --- a/test/test_share_types.py +++ /dev/null @@ -1,170 +0,0 @@ -import os -from pathlib import Path - -import pytest -from unit.applications.proto import TestApplicationProto -from unit.option import option - - -class TestShareTypes(TestApplicationProto): - prerequisites = {} - - @pytest.fixture(autouse=True) - def setup_method_fixture(self, temp_dir): - Path(temp_dir + '/assets').mkdir() - for ext in ['.xml', '.mp4', '.php', '', '.txt', '.html', '.png']: - Path(temp_dir + '/assets/file' + ext).write_text(ext) - - Path(temp_dir + '/assets/index.html').write_text('index') - - self._load_conf( - { - "listeners": { - "*:7080": {"pass": "routes"}, - "*:7081": {"pass": "routes"}, - }, - "routes": [{"action": {"share": temp_dir + "/assets"}}], - "applications": {}, - } - ) - - def action_update(self, conf): - assert 'success' in self.conf(conf, 'routes/0/action') - - def check_body(self, http_url, body): - resp = self.get(url=http_url) - assert resp['status'] == 200, 'status' - assert resp['body'] == body, 'body' - - def test_share_types_basic(self, temp_dir): - self.action_update({"share": temp_dir + "/assets"}) - self.check_body('/index.html', 'index') - self.check_body('/file.xml', '.xml') - - self.action_update( - {"share": temp_dir + "/assets", "types": "application/xml"} - ) - self.check_body('/file.xml', '.xml') - - self.action_update( - {"share": temp_dir + "/assets", "types": ["application/xml"]} - ) - self.check_body('/file.xml', '.xml') - - self.action_update({"share": temp_dir + "/assets", "types": [""]}) - assert self.get(url='/file.xml')['status'] == 403, 'no mtype' - - def test_share_types_wildcard(self, temp_dir): - self.action_update( - {"share": temp_dir + "/assets", "types": ["application/*"]} - ) - self.check_body('/file.xml', '.xml') - assert self.get(url='/file.mp4')['status'] == 403, 'app * mtype mp4' - - self.action_update( - {"share": temp_dir + "/assets", "types": ["video/*"]} - ) - assert self.get(url='/file.xml')['status'] == 403, 'video * mtype xml' - self.check_body('/file.mp4', '.mp4') - - def test_share_types_negation(self, temp_dir): - self.action_update( - {"share": temp_dir + "/assets", "types": ["!application/xml"]} - ) - assert self.get(url='/file.xml')['status'] == 403, 'forbidden negation' - self.check_body('/file.mp4', '.mp4') - - # sorting negation - self.action_update( - { - "share": temp_dir + "/assets", - "types": ["!video/*", "image/png", "!image/jpg"], - } - ) - assert self.get(url='/file.mp4')['status'] == 403, 'negation sort mp4' - self.check_body('/file.png', '.png') - assert self.get(url='/file.jpg')['status'] == 403, 'negation sort jpg' - - def test_share_types_regex(self, temp_dir): - self.action_update( - {"share": temp_dir + "/assets", "types": ["~text/(html|plain)"]} - ) - assert self.get(url='/file.php')['status'] == 403, 'regex fail' - self.check_body('/file.html', '.html') - self.check_body('/file.txt', '.txt') - - def test_share_types_case(self, temp_dir): - self.action_update( - {"share": temp_dir + "/assets", "types": ["!APpliCaTiOn/xMl"]} - ) - self.check_body('/file.mp4', '.mp4') - assert ( - self.get(url='/file.xml')['status'] == 403 - ), 'mixed case xml negation' - - self.action_update( - {"share": temp_dir + "/assets", "types": ["vIdEo/mp4"]} - ) - assert self.get(url='/file.mp4')['status'] == 200, 'mixed case' - assert ( - self.get(url='/file.xml')['status'] == 403 - ), 'mixed case video negation' - - self.action_update( - {"share": temp_dir + "/assets", "types": ["vIdEo/*"]} - ) - self.check_body('/file.mp4', '.mp4') - assert ( - self.get(url='/file.xml')['status'] == 403 - ), 'mixed case video * negation' - - def test_share_types_fallback(self, temp_dir): - assert 'success' in self.conf( - [ - { - "match": {"destination": "*:7081"}, - "action": {"return": 200}, - }, - { - "action": { - "share": temp_dir + "/assets", - "types": ["!application/x-httpd-php"], - "fallback": {"proxy": "http://127.0.0.1:7081"}, - } - }, - ], - 'routes', - ), 'configure fallback proxy route' - - self.check_body('/file.php', '') - self.check_body('/file.mp4', '.mp4') - - def test_share_types_index(self, temp_dir): - self.action_update( - {"share": temp_dir + "/assets", "types": "application/xml"} - ) - self.check_body('/', 'index') - self.check_body('/file.xml', '.xml') - assert self.get(url='/file.mp4')['status'] == 403, 'forbidden mtype' - - def test_share_types_custom_mime(self, temp_dir): - self._load_conf( - { - "listeners": {"*:7080": {"pass": "routes"}}, - "routes": [{"action": {"share": temp_dir + "/assets"}}], - "applications": {}, - "settings": { - "http": { - "static": {"mime_types": {"test/mime-type": ["file"]}} - } - }, - } - ) - - self.action_update({"share": temp_dir + "/assets", "types": [""]}) - assert self.get(url='/file')['status'] == 403, 'forbidden custom mime' - - self.action_update( - {"share": temp_dir + "/assets", "types": ["test/mime-type"]} - ) - self.check_body('/file', '') diff --git a/test/test_static_chroot.py b/test/test_static_chroot.py new file mode 100644 index 00000000..f9bc93a8 --- /dev/null +++ b/test/test_static_chroot.py @@ -0,0 +1,108 @@ +import os +from pathlib import Path + +import pytest + +from unit.applications.proto import TestApplicationProto + + +class TestStaticChroot(TestApplicationProto): + prerequisites = {'features': ['chroot']} + + @pytest.fixture(autouse=True) + def setup_method_fixture(self, temp_dir): + os.makedirs(temp_dir + '/assets/dir') + Path(temp_dir + '/assets/index.html').write_text('0123456789') + Path(temp_dir + '/assets/dir/file').write_text('blah') + + test = Path(__file__) + self.test_path = '/' + test.parent.name + '/' + test.name + + self._load_conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [{"action": {"share": temp_dir + "/assets"}}], + } + ) + + def test_static_chroot(self, temp_dir): + assert self.get(url='/dir/file')['status'] == 200, 'default chroot' + assert self.get(url='/index.html')['status'] == 200, 'default chroot 2' + + assert 'success' in self.conf( + { + "share": temp_dir + "/assets", + "chroot": temp_dir + "/assets/dir", + }, + 'routes/0/action', + ), 'configure chroot' + + assert self.get(url='/dir/file')['status'] == 200, 'chroot' + assert self.get(url='/index.html')['status'] == 403, 'chroot 403 2' + assert self.get(url='/file')['status'] == 403, 'chroot 403' + + def test_static_chroot_permission(self, is_su, temp_dir): + if is_su: + pytest.skip('does\'t work under root') + + os.chmod(temp_dir + '/assets/dir', 0o100) + + assert 'success' in self.conf( + { + "share": temp_dir + "/assets", + "chroot": temp_dir + "/assets/dir", + }, + 'routes/0/action', + ), 'configure chroot' + + assert self.get(url='/dir/file')['status'] == 200, 'chroot' + + def test_static_chroot_empty(self, temp_dir): + assert 'success' in self.conf( + {"share": temp_dir + "/assets", "chroot": ""}, 'routes/0/action', + ), 'configure chroot empty absolute' + + assert ( + self.get(url='/dir/file')['status'] == 200 + ), 'chroot empty absolute' + + assert 'success' in self.conf( + {"share": ".", "chroot": ""}, 'routes/0/action', + ), 'configure chroot empty relative' + + assert ( + self.get(url=self.test_path)['status'] == 200 + ), 'chroot empty relative' + + def test_static_chroot_relative(self, is_su, temp_dir): + if is_su: + pytest.skip('does\'t work under root') + + assert 'success' in self.conf( + {"share": temp_dir + "/assets", "chroot": "."}, 'routes/0/action', + ), 'configure relative chroot' + + assert self.get(url='/dir/file')['status'] == 403, 'relative chroot' + + assert 'success' in self.conf( + {"share": "."}, 'routes/0/action', + ), 'configure relative share' + + assert self.get(url=self.test_path)['status'] == 200, 'relative share' + + assert 'success' in self.conf( + {"share": ".", "chroot": "."}, 'routes/0/action', + ), 'configure relative' + + assert self.get(url=self.test_path)['status'] == 200, 'relative' + + def test_static_chroot_invalid(self, temp_dir): + assert 'error' in self.conf( + {"share": temp_dir, "chroot": True}, 'routes/0/action', + ), 'configure chroot error' + assert 'error' in self.conf( + {"share": temp_dir, "symlinks": "True"}, 'routes/0/action', + ), 'configure symlink error' + assert 'error' in self.conf( + {"share": temp_dir, "mount": "True"}, 'routes/0/action', + ), 'configure mount error' diff --git a/test/test_static_fallback.py b/test/test_static_fallback.py new file mode 100644 index 00000000..dc9056b9 --- /dev/null +++ b/test/test_static_fallback.py @@ -0,0 +1,149 @@ +import os +from pathlib import Path + +import pytest + +from unit.applications.proto import TestApplicationProto + + +class TestStaticFallback(TestApplicationProto): + prerequisites = {} + + @pytest.fixture(autouse=True) + def setup_method_fixture(self, temp_dir): + os.makedirs(temp_dir + '/assets/dir') + Path(temp_dir + '/assets/index.html').write_text('0123456789') + + os.makedirs(temp_dir + '/assets/403') + os.chmod(temp_dir + '/assets/403', 0o000) + + self._load_conf( + { + "listeners": { + "*:7080": {"pass": "routes"}, + "*:7081": {"pass": "routes"}, + }, + "routes": [{"action": {"share": temp_dir + "/assets"}}], + "applications": {}, + } + ) + + yield + + try: + os.chmod(temp_dir + '/assets/403', 0o777) + except FileNotFoundError: + pass + + def action_update(self, conf): + assert 'success' in self.conf(conf, 'routes/0/action') + + def test_static_fallback(self): + self.action_update({"share": "/blah"}) + assert self.get()['status'] == 404, 'bad path no fallback' + + self.action_update({"share": "/blah", "fallback": {"return": 200}}) + + resp = self.get() + assert resp['status'] == 200, 'bad path fallback status' + assert resp['body'] == '', 'bad path fallback' + + def test_static_fallback_valid_path(self, temp_dir): + self.action_update( + {"share": temp_dir + "/assets", "fallback": {"return": 200}} + ) + resp = self.get() + assert resp['status'] == 200, 'fallback status' + assert resp['body'] == '0123456789', 'fallback' + + resp = self.get(url='/403/') + assert resp['status'] == 200, 'fallback status 403' + assert resp['body'] == '', 'fallback 403' + + resp = self.post() + assert resp['status'] == 200, 'fallback status 405' + assert resp['body'] == '', 'fallback 405' + + assert self.get(url='/dir')['status'] == 301, 'fallback status 301' + + def test_static_fallback_nested(self): + self.action_update( + { + "share": "/blah", + "fallback": { + "share": "/blah/blah", + "fallback": {"return": 200}, + }, + } + ) + + resp = self.get() + assert resp['status'] == 200, 'fallback nested status' + assert resp['body'] == '', 'fallback nested' + + def test_static_fallback_share(self, temp_dir): + self.action_update( + {"share": "/blah", "fallback": {"share": temp_dir + "/assets"},} + ) + + resp = self.get() + assert resp['status'] == 200, 'fallback share status' + assert resp['body'] == '0123456789', 'fallback share' + + resp = self.head() + assert resp['status'] == 200, 'fallback share status HEAD' + assert resp['body'] == '', 'fallback share HEAD' + + assert ( + self.get(url='/dir')['status'] == 301 + ), 'fallback share status 301' + + def test_static_fallback_proxy(self): + assert 'success' in self.conf( + [ + { + "match": {"destination": "*:7081"}, + "action": {"return": 200}, + }, + { + "action": { + "share": "/blah", + "fallback": {"proxy": "http://127.0.0.1:7081"}, + } + }, + ], + 'routes', + ), 'configure fallback proxy route' + + resp = self.get() + assert resp['status'] == 200, 'fallback proxy status' + assert resp['body'] == '', 'fallback proxy' + + @pytest.mark.skip('not yet') + def test_static_fallback_proxy_loop(self, skip_alert): + skip_alert( + r'open.*/blah/index.html.*failed', + r'accept.*failed', + r'socket.*failed', + r'new connections are not accepted', + ) + + self.action_update( + {"share": "/blah", "fallback": {"proxy": "http://127.0.0.1:7080"}} + ) + self.get(no_recv=True) + + assert 'success' in self.conf_delete('listeners/*:7081') + self.get(read_timeout=1) + + def test_static_fallback_invalid(self): + def check_error(conf): + assert 'error' in self.conf(conf, 'routes/0/action') + + check_error({"share": "/blah", "fallback": {}}) + check_error({"share": "/blah", "fallback": ""}) + check_error({"return": 200, "fallback": {"share": "/blah"}}) + check_error( + {"proxy": "http://127.0.0.1:7081", "fallback": {"share": "/blah"}} + ) + check_error({"fallback": {"share": "/blah"}}) diff --git a/test/test_static_mount.py b/test/test_static_mount.py new file mode 100644 index 00000000..570f6439 --- /dev/null +++ b/test/test_static_mount.py @@ -0,0 +1,140 @@ +import os +import subprocess +from pathlib import Path + +import pytest + +from unit.applications.proto import TestApplicationProto + + +class TestStaticMount(TestApplicationProto): + prerequisites = {'features': ['chroot']} + + @pytest.fixture(autouse=True) + def setup_method_fixture(self, is_su, temp_dir): + if not is_su: + pytest.skip('requires root') + + os.makedirs(temp_dir + '/assets/dir/mount') + os.makedirs(temp_dir + '/assets/dir/dir') + os.makedirs(temp_dir + '/assets/mount') + Path(temp_dir + '/assets/index.html').write_text('index') + Path(temp_dir + '/assets/dir/dir/file').write_text('file') + Path(temp_dir + '/assets/mount/index.html').write_text('mount') + + try: + process = subprocess.Popen( + [ + "mount", + "--bind", + temp_dir + "/assets/mount", + temp_dir + "/assets/dir/mount", + ], + stderr=subprocess.STDOUT, + ) + + process.communicate() + + except KeyboardInterrupt: + raise + + except: + pytest.fail('Can\'t run mount process.') + + self._load_conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [{"action": {"share": temp_dir + "/assets/dir"}}], + } + ) + + yield + + try: + process = subprocess.Popen( + ["umount", "--lazy", temp_dir + "/assets/dir/mount"], + stderr=subprocess.STDOUT, + ) + + process.communicate() + + except KeyboardInterrupt: + raise + + except: + pytest.fail('Can\'t run umount process.') + + def test_static_mount(self, temp_dir, skip_alert): + skip_alert(r'opening.*failed') + + resp = self.get(url='/mount/') + assert resp['status'] == 200 + assert resp['body'] == 'mount' + + assert 'success' in self.conf( + {"share": temp_dir + "/assets/dir", "traverse_mounts": False}, + 'routes/0/action', + ), 'configure mount disable' + + assert self.get(url='/mount/')['status'] == 403 + + assert 'success' in self.conf( + {"share": temp_dir + "/assets/dir", "traverse_mounts": True}, + 'routes/0/action', + ), 'configure mount enable' + + resp = self.get(url='/mount/') + assert resp['status'] == 200 + assert resp['body'] == 'mount' + + def test_static_mount_two_blocks(self, temp_dir, skip_alert): + skip_alert(r'opening.*failed') + + os.symlink(temp_dir + '/assets/dir', temp_dir + '/assets/link') + + assert 'success' in self.conf( + [ + { + "match": {"method": "HEAD"}, + "action": { + "share": temp_dir + "/assets/dir", + "traverse_mounts": False, + }, + }, + { + "match": {"method": "GET"}, + "action": { + "share": temp_dir + "/assets/dir", + "traverse_mounts": True, + }, + }, + ], + 'routes', + ), 'configure two options' + + assert self.get(url='/mount/')['status'] == 200, 'block enabled' + assert self.head(url='/mount/')['status'] == 403, 'block disabled' + + def test_static_mount_chroot(self, temp_dir, skip_alert): + skip_alert(r'opening.*failed') + + assert 'success' in self.conf( + { + "share": temp_dir + "/assets/dir", + "chroot": temp_dir + "/assets", + }, + 'routes/0/action', + ), 'configure chroot mount default' + + assert self.get(url='/mount/')['status'] == 200, 'chroot' + + assert 'success' in self.conf( + { + "share": temp_dir + "/assets/dir", + "chroot": temp_dir + "/assets", + "traverse_mounts": False, + }, + 'routes/0/action', + ), 'configure chroot mount disable' + + assert self.get(url='/mount/')['status'] == 403, 'chroot mount' diff --git a/test/test_static_symlink.py b/test/test_static_symlink.py new file mode 100644 index 00000000..35eb402a --- /dev/null +++ b/test/test_static_symlink.py @@ -0,0 +1,94 @@ +import os +from pathlib import Path + +import pytest + +from unit.applications.proto import TestApplicationProto + + +class TestStaticSymlink(TestApplicationProto): + prerequisites = {'features': ['chroot']} + + @pytest.fixture(autouse=True) + def setup_method_fixture(self, temp_dir): + os.makedirs(temp_dir + '/assets/dir/dir') + Path(temp_dir + '/assets/index.html').write_text('0123456789') + Path(temp_dir + '/assets/dir/file').write_text('blah') + + self._load_conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [{"action": {"share": temp_dir + "/assets"}}], + } + ) + + def test_static_symlink(self, temp_dir, skip_alert): + skip_alert(r'opening.*failed') + + os.symlink(temp_dir + '/assets/dir', temp_dir + '/assets/link') + + assert self.get(url='/dir')['status'] == 301, 'dir' + assert self.get(url='/dir/file')['status'] == 200, 'file' + assert self.get(url='/link')['status'] == 301, 'symlink dir' + assert self.get(url='/link/file')['status'] == 200, 'symlink file' + + assert 'success' in self.conf( + {"share": temp_dir + "/assets", "follow_symlinks": False}, + 'routes/0/action', + ), 'configure symlink disable' + + assert self.get(url='/link/file')['status'] == 403, 'symlink disabled' + + assert 'success' in self.conf( + {"share": temp_dir + "/assets", "follow_symlinks": True}, + 'routes/0/action', + ), 'configure symlink enable' + + assert self.get(url='/link/file')['status'] == 200, 'symlink enabled' + + def test_static_symlink_two_blocks(self, temp_dir, skip_alert): + skip_alert(r'opening.*failed') + + os.symlink(temp_dir + '/assets/dir', temp_dir + '/assets/link') + + assert 'success' in self.conf( + [ + { + "match": {"method": "HEAD"}, + "action": { + "share": temp_dir + "/assets", + "follow_symlinks": False, + }, + }, + { + "match": {"method": "GET"}, + "action": { + "share": temp_dir + "/assets", + "follow_symlinks": True, + }, + }, + ], + 'routes', + ), 'configure two options' + + assert self.get(url='/link/file')['status'] == 200, 'block enabled' + assert self.head(url='/link/file')['status'] == 403, 'block disabled' + + def test_static_symlink_chroot(self, temp_dir, skip_alert): + skip_alert(r'opening.*failed') + + os.symlink( + temp_dir + '/assets/dir/file', temp_dir + '/assets/dir/dir/link' + ) + + assert self.get(url='/dir/dir/link')['status'] == 200, 'default chroot' + + assert 'success' in self.conf( + { + "share": temp_dir + "/assets", + "chroot": temp_dir + "/assets/dir/dir", + }, + 'routes/0/action', + ), 'configure chroot' + + assert self.get(url='/dir/dir/link')['status'] == 404, 'chroot' diff --git a/test/test_static_types.py b/test/test_static_types.py new file mode 100644 index 00000000..20defddf --- /dev/null +++ b/test/test_static_types.py @@ -0,0 +1,169 @@ +from pathlib import Path + +import pytest + +from unit.applications.proto import TestApplicationProto + + +class TestStaticTypes(TestApplicationProto): + prerequisites = {} + + @pytest.fixture(autouse=True) + def setup_method_fixture(self, temp_dir): + Path(temp_dir + '/assets').mkdir() + for ext in ['.xml', '.mp4', '.php', '', '.txt', '.html', '.png']: + Path(temp_dir + '/assets/file' + ext).write_text(ext) + + Path(temp_dir + '/assets/index.html').write_text('index') + + self._load_conf( + { + "listeners": { + "*:7080": {"pass": "routes"}, + "*:7081": {"pass": "routes"}, + }, + "routes": [{"action": {"share": temp_dir + "/assets"}}], + "applications": {}, + } + ) + + def action_update(self, conf): + assert 'success' in self.conf(conf, 'routes/0/action') + + def check_body(self, http_url, body): + resp = self.get(url=http_url) + assert resp['status'] == 200, 'status' + assert resp['body'] == body, 'body' + + def test_static_types_basic(self, temp_dir): + self.action_update({"share": temp_dir + "/assets"}) + self.check_body('/index.html', 'index') + self.check_body('/file.xml', '.xml') + + self.action_update( + {"share": temp_dir + "/assets", "types": "application/xml"} + ) + self.check_body('/file.xml', '.xml') + + self.action_update( + {"share": temp_dir + "/assets", "types": ["application/xml"]} + ) + self.check_body('/file.xml', '.xml') + + self.action_update({"share": temp_dir + "/assets", "types": [""]}) + assert self.get(url='/file.xml')['status'] == 403, 'no mtype' + + def test_static_types_wildcard(self, temp_dir): + self.action_update( + {"share": temp_dir + "/assets", "types": ["application/*"]} + ) + self.check_body('/file.xml', '.xml') + assert self.get(url='/file.mp4')['status'] == 403, 'app * mtype mp4' + + self.action_update( + {"share": temp_dir + "/assets", "types": ["video/*"]} + ) + assert self.get(url='/file.xml')['status'] == 403, 'video * mtype xml' + self.check_body('/file.mp4', '.mp4') + + def test_static_types_negation(self, temp_dir): + self.action_update( + {"share": temp_dir + "/assets", "types": ["!application/xml"]} + ) + assert self.get(url='/file.xml')['status'] == 403, 'forbidden negation' + self.check_body('/file.mp4', '.mp4') + + # sorting negation + self.action_update( + { + "share": temp_dir + "/assets", + "types": ["!video/*", "image/png", "!image/jpg"], + } + ) + assert self.get(url='/file.mp4')['status'] == 403, 'negation sort mp4' + self.check_body('/file.png', '.png') + assert self.get(url='/file.jpg')['status'] == 403, 'negation sort jpg' + + def test_static_types_regex(self, temp_dir): + self.action_update( + {"share": temp_dir + "/assets", "types": ["~text/(html|plain)"]} + ) + assert self.get(url='/file.php')['status'] == 403, 'regex fail' + self.check_body('/file.html', '.html') + self.check_body('/file.txt', '.txt') + + def test_static_types_case(self, temp_dir): + self.action_update( + {"share": temp_dir + "/assets", "types": ["!APpliCaTiOn/xMl"]} + ) + self.check_body('/file.mp4', '.mp4') + assert ( + self.get(url='/file.xml')['status'] == 403 + ), 'mixed case xml negation' + + self.action_update( + {"share": temp_dir + "/assets", "types": ["vIdEo/mp4"]} + ) + assert self.get(url='/file.mp4')['status'] == 200, 'mixed case' + assert ( + self.get(url='/file.xml')['status'] == 403 + ), 'mixed case video negation' + + self.action_update( + {"share": temp_dir + "/assets", "types": ["vIdEo/*"]} + ) + self.check_body('/file.mp4', '.mp4') + assert ( + self.get(url='/file.xml')['status'] == 403 + ), 'mixed case video * negation' + + def test_static_types_fallback(self, temp_dir): + assert 'success' in self.conf( + [ + { + "match": {"destination": "*:7081"}, + "action": {"return": 200}, + }, + { + "action": { + "share": temp_dir + "/assets", + "types": ["!application/x-httpd-php"], + "fallback": {"proxy": "http://127.0.0.1:7081"}, + } + }, + ], + 'routes', + ), 'configure fallback proxy route' + + self.check_body('/file.php', '') + self.check_body('/file.mp4', '.mp4') + + def test_static_types_index(self, temp_dir): + self.action_update( + {"share": temp_dir + "/assets", "types": "application/xml"} + ) + self.check_body('/', 'index') + self.check_body('/file.xml', '.xml') + assert self.get(url='/file.mp4')['status'] == 403, 'forbidden mtype' + + def test_static_types_custom_mime(self, temp_dir): + self._load_conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [{"action": {"share": temp_dir + "/assets"}}], + "applications": {}, + "settings": { + "http": { + "static": {"mime_types": {"test/mime-type": ["file"]}} + } + }, + } + ) + + self.action_update({"share": temp_dir + "/assets", "types": [""]}) + assert self.get(url='/file')['status'] == 403, 'forbidden custom mime' + + self.action_update( + {"share": temp_dir + "/assets", "types": ["test/mime-type"]} + ) + self.check_body('/file', '') -- cgit From cfba69781a18407d5c2020c4e3f3d4fc175a6127 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Thu, 1 Jul 2021 13:56:40 +0300 Subject: Fixing multiple TLS-enabled listeners initialization. Because of the incorrect 'last' field assignment, multiple listeners with a TLS certificate did not initialize properly, which caused a router crash while establishing a connection. Test with multiple TLS listeners added. The issue was introduced in the c548e46fe516 commit. This closes #561 issue on GitHub. --- test/test_tls.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'test') diff --git a/test/test_tls.py b/test/test_tls.py index 0cfeaded..546f0f89 100644 --- a/test/test_tls.py +++ b/test/test_tls.py @@ -665,3 +665,16 @@ basicConstraints = critical,CA:TRUE""" ) assert res['status'] == 200, 'status ok' assert res['body'] == filename + data + + def test_tls_multi_listener(self): + self.load('empty') + + self.certificate() + + self.add_tls() + self.add_tls(port=7081) + + assert self.get_ssl()['status'] == 200, 'listener #1' + + assert self.get_ssl(port=7081)['status'] == 200, 'listener #2' + -- cgit From 210c8bbd81a8836bffee221215effa00018868cd Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Thu, 1 Jul 2021 16:22:08 +0300 Subject: Tests: fixing racing condition in respawn tests. A race may occur between the router process restart and the main process sending a notification to the running controller. For example, a test script detects the new process and starts performing a smoke test, but the controller has not yet received the 'remove PID' notification, so the connection to the router is broken and any attempt to update the configuration will cause an error. The solution is to perform several attempts to reconfigure Unit with a short delay between failures. --- test/test_respawn.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'test') diff --git a/test/test_respawn.py b/test/test_respawn.py index edbfa2a8..5a5d6126 100644 --- a/test/test_respawn.py +++ b/test/test_respawn.py @@ -44,11 +44,16 @@ class TestRespawn(TestApplicationPython): 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' - ) - assert self.get()['status'] == 200 + for _ in range(10): + r = self.conf('1', 'applications/' + self.app_name + '/processes') + + if 'success' in r: + break + + time.sleep(0.1) + + assert 'success' in r + assert self.get()['status'] == 200 # Check if the only one router, controller, # and application processes running. -- cgit From 6c14d5d7b1921bd78f2d1b7458eae7d97eee0fcd Mon Sep 17 00:00:00 2001 From: Oisin Canty Date: Fri, 2 Jul 2021 13:00:04 +0000 Subject: Tests: run Ruby applications inside temporary directory. --- test/test_ruby_isolation.py | 7 ------- test/unit/applications/lang/ruby.py | 16 +++++++++++++++- test/unit/check/isolation.py | 7 +++++-- 3 files changed, 20 insertions(+), 10 deletions(-) (limited to 'test') diff --git a/test/test_ruby_isolation.py b/test/test_ruby_isolation.py index 8443d857..f414d610 100644 --- a/test/test_ruby_isolation.py +++ b/test/test_ruby_isolation.py @@ -35,13 +35,6 @@ class TestRubyIsolation(TestApplicationRuby): 'pid': True, } - os.mkdir(option.temp_dir + '/ruby') - - shutil.copytree( - option.test_dir + '/ruby/status_int', - option.temp_dir + '/ruby/status_int', - ) - self.load('status_int', isolation=isolation) assert 'success' in self.conf( diff --git a/test/unit/applications/lang/ruby.py b/test/unit/applications/lang/ruby.py index 02644584..d95d62b4 100644 --- a/test/unit/applications/lang/ruby.py +++ b/test/unit/applications/lang/ruby.py @@ -1,12 +1,26 @@ +import os +import shutil + from unit.applications.proto import TestApplicationProto from unit.option import option +from unit.utils import public_dir class TestApplicationRuby(TestApplicationProto): application_type = "ruby" + def prepare_env(self, script): + shutil.copytree( + option.test_dir + '/ruby/' + script, + option.temp_dir + '/ruby/' + script + ) + + public_dir(option.temp_dir + '/ruby/' + script) + def load(self, script, name='config.ru', **kwargs): - script_path = option.test_dir + '/ruby/' + script + self.prepare_env(script) + + script_path = option.temp_dir + '/ruby/' + script self._load_conf( { diff --git a/test/unit/check/isolation.py b/test/unit/check/isolation.py index 7c83ae35..43c8842f 100644 --- a/test/unit/check/isolation.py +++ b/test/unit/check/isolation.py @@ -3,6 +3,7 @@ import os from unit.applications.lang.go import TestApplicationGo from unit.applications.lang.java import TestApplicationJava +from unit.applications.lang.ruby import TestApplicationRuby from unit.applications.lang.node import TestApplicationNode from unit.applications.proto import TestApplicationProto from unit.http import TestHTTP @@ -65,14 +66,16 @@ def check_isolation(): } elif 'ruby' in available['modules']: + TestApplicationRuby().prepare_env('empty') + 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", + "working_directory": option.temp_dir + "/ruby/empty", + "script": option.temp_dir + "/ruby/empty/config.ru", "isolation": {"namespaces": {"credential": True}}, } }, -- cgit From 8c83652c2a0ad7386e27a9ea595c996d3dce018c Mon Sep 17 00:00:00 2001 From: Oisin Canty Date: Fri, 2 Jul 2021 13:00:57 +0000 Subject: Tests: Ruby hooks. --- test/ruby/hooks/config.ru | 7 +++ test/ruby/hooks/eval.rb | 3 ++ test/ruby/hooks/multiple.rb | 9 ++++ test/ruby/hooks/on_thread_boot.rb | 5 ++ test/ruby/hooks/on_thread_shutdown.rb | 5 ++ test/ruby/hooks/on_worker_boot.rb | 5 ++ test/ruby/hooks/on_worker_shutdown.rb | 5 ++ test/test_ruby_hooks.py | 98 +++++++++++++++++++++++++++++++++++ test/unit/applications/lang/ruby.py | 24 +++++---- test/unit/applications/proto.py | 16 +++--- test/unit/utils.py | 28 +++++++--- 11 files changed, 183 insertions(+), 22 deletions(-) create mode 100644 test/ruby/hooks/config.ru create mode 100644 test/ruby/hooks/eval.rb create mode 100644 test/ruby/hooks/multiple.rb create mode 100644 test/ruby/hooks/on_thread_boot.rb create mode 100644 test/ruby/hooks/on_thread_shutdown.rb create mode 100644 test/ruby/hooks/on_worker_boot.rb create mode 100644 test/ruby/hooks/on_worker_shutdown.rb create mode 100644 test/test_ruby_hooks.py (limited to 'test') diff --git a/test/ruby/hooks/config.ru b/test/ruby/hooks/config.ru new file mode 100644 index 00000000..f3069558 --- /dev/null +++ b/test/ruby/hooks/config.ru @@ -0,0 +1,7 @@ +app = Proc.new do |env| + ['200', { + 'Content-Length' => '0' + }, ['']] +end + +run app diff --git a/test/ruby/hooks/eval.rb b/test/ruby/hooks/eval.rb new file mode 100644 index 00000000..ce7329c1 --- /dev/null +++ b/test/ruby/hooks/eval.rb @@ -0,0 +1,3 @@ +require 'securerandom' + +File.write("./cookie_eval.#{SecureRandom.hex}", "evaluated") diff --git a/test/ruby/hooks/multiple.rb b/test/ruby/hooks/multiple.rb new file mode 100644 index 00000000..295b46a9 --- /dev/null +++ b/test/ruby/hooks/multiple.rb @@ -0,0 +1,9 @@ +require 'securerandom' + +on_worker_boot do + File.write("./cookie_worker_boot.#{SecureRandom.hex}", "worker booted") +end + +on_thread_boot do + File.write("./cookie_thread_boot.#{SecureRandom.hex}", "thread booted") +end diff --git a/test/ruby/hooks/on_thread_boot.rb b/test/ruby/hooks/on_thread_boot.rb new file mode 100644 index 00000000..1e05e248 --- /dev/null +++ b/test/ruby/hooks/on_thread_boot.rb @@ -0,0 +1,5 @@ +require 'securerandom' + +on_thread_boot do + File.write("./cookie_thread_boot.#{SecureRandom.hex}", "booted") +end diff --git a/test/ruby/hooks/on_thread_shutdown.rb b/test/ruby/hooks/on_thread_shutdown.rb new file mode 100644 index 00000000..e65c1b42 --- /dev/null +++ b/test/ruby/hooks/on_thread_shutdown.rb @@ -0,0 +1,5 @@ +require 'securerandom' + +on_thread_shutdown do + File.write("./cookie_thread_shutdown.#{SecureRandom.hex}", "shutdown") +end diff --git a/test/ruby/hooks/on_worker_boot.rb b/test/ruby/hooks/on_worker_boot.rb new file mode 100644 index 00000000..b6529f60 --- /dev/null +++ b/test/ruby/hooks/on_worker_boot.rb @@ -0,0 +1,5 @@ +require 'securerandom' + +on_worker_boot do + File.write("./cookie_worker_boot.#{SecureRandom.hex}", "booted") +end diff --git a/test/ruby/hooks/on_worker_shutdown.rb b/test/ruby/hooks/on_worker_shutdown.rb new file mode 100644 index 00000000..9ffaad93 --- /dev/null +++ b/test/ruby/hooks/on_worker_shutdown.rb @@ -0,0 +1,5 @@ +require 'securerandom' + +on_worker_shutdown do + File.write("./cookie_worker_shutdown.#{SecureRandom.hex}", "shutdown") +end diff --git a/test/test_ruby_hooks.py b/test/test_ruby_hooks.py new file mode 100644 index 00000000..af8ce337 --- /dev/null +++ b/test/test_ruby_hooks.py @@ -0,0 +1,98 @@ +import os +import time +from pathlib import Path + +import pytest + +from conftest import unit_stop +from unit.applications.lang.ruby import TestApplicationRuby +from unit.option import option +from unit.utils import waitforglob + + +class TestRubyHooks(TestApplicationRuby): + prerequisites = {'modules': {'ruby': 'all'}} + + def _wait_cookie(self, pattern, count): + return waitforglob( + option.temp_dir + '/ruby/hooks/cookie_' + pattern, count + ) + + def test_ruby_hooks_eval(self): + processes = 2 + + self.load('hooks', processes=processes, hooks='eval.rb') + + hooked = self._wait_cookie('eval.*', processes) + + assert hooked, 'hooks evaluated' + + def test_ruby_hooks_on_worker_boot(self): + processes = 2 + + self.load('hooks', processes=processes, hooks='on_worker_boot.rb') + + hooked = self._wait_cookie('worker_boot.*', processes) + + assert hooked, 'on_worker_boot called' + + def test_ruby_hooks_on_worker_shutdown(self): + processes = 2 + + self.load('hooks', processes=processes, hooks='on_worker_shutdown.rb') + + assert self.get()['status'] == 200, 'app response' + + self.load('empty') + + hooked = self._wait_cookie('worker_shutdown.*', processes) + + assert hooked, 'on_worker_shutdown called' + + def test_ruby_hooks_on_thread_boot(self): + processes = 1 + threads = 2 + + self.load( + 'hooks', + processes=processes, + threads=threads, + hooks='on_thread_boot.rb', + ) + + hooked = self._wait_cookie('thread_boot.*', processes * threads) + + assert hooked, 'on_thread_boot called' + + def test_ruby_hooks_on_thread_shutdown(self): + processes = 1 + threads = 2 + + self.load( + 'hooks', + processes=processes, + threads=threads, + hooks='on_thread_shutdown.rb', + ) + + assert self.get()['status'] == 200, 'app response' + + self.load('empty') + + hooked = self._wait_cookie('thread_shutdown.*', processes * threads) + + assert hooked, 'on_thread_shutdown called' + + def test_ruby_hooks_multiple(self): + processes = 1 + threads = 1 + + self.load( + 'hooks', processes=processes, threads=threads, hooks='multiple.rb', + ) + + hooked = self._wait_cookie('worker_boot.*', processes) + assert hooked, 'on_worker_boot called' + + hooked = self._wait_cookie('thread_boot.*', threads) + assert hooked, 'on_thread_boot called' diff --git a/test/unit/applications/lang/ruby.py b/test/unit/applications/lang/ruby.py index d95d62b4..61d50558 100644 --- a/test/unit/applications/lang/ruby.py +++ b/test/unit/applications/lang/ruby.py @@ -12,7 +12,7 @@ class TestApplicationRuby(TestApplicationProto): def prepare_env(self, script): shutil.copytree( option.test_dir + '/ruby/' + script, - option.temp_dir + '/ruby/' + script + option.temp_dir + '/ruby/' + script, ) public_dir(option.temp_dir + '/ruby/' + script) @@ -22,17 +22,23 @@ class TestApplicationRuby(TestApplicationProto): script_path = option.temp_dir + '/ruby/' + script + app = { + "type": self.get_application_type(), + "processes": {"spare": 0}, + "working_directory": script_path, + "script": script_path + '/' + name, + } + + for key in [ + 'hooks', + ]: + if key in kwargs: + app[key] = kwargs[key] + self._load_conf( { "listeners": {"*:7080": {"pass": "applications/" + script}}, - "applications": { - script: { - "type": self.get_application_type(), - "processes": {"spare": 0}, - "working_directory": script_path, - "script": script_path + '/' + name, - } - }, + "applications": {script: app}, }, **kwargs ) diff --git a/test/unit/applications/proto.py b/test/unit/applications/proto.py index 92754c03..e30d21ff 100644 --- a/test/unit/applications/proto.py +++ b/test/unit/applications/proto.py @@ -47,13 +47,15 @@ class TestApplicationProto(TestControl): if 'applications' in conf: for app in conf['applications'].keys(): app_conf = conf['applications'][app] - if 'user' in kwargs: - app_conf['user'] = kwargs['user'] - if 'group' in kwargs: - app_conf['group'] = kwargs['group'] - - if 'isolation' in kwargs: - app_conf['isolation'] = kwargs['isolation'] + for key in [ + 'user', + 'group', + 'isolation', + 'processes', + 'threads', + ]: + if key in kwargs: + app_conf[key] = kwargs[key] assert 'success' in self.conf(conf), 'load application configuration' diff --git a/test/unit/utils.py b/test/unit/utils.py index a627e9f5..43aaa81b 100644 --- a/test/unit/utils.py +++ b/test/unit/utils.py @@ -1,3 +1,4 @@ +import glob import os import socket import subprocess @@ -16,8 +17,8 @@ def public_dir(path): os.chmod(os.path.join(root, f), 0o777) -def waitforfiles(*files): - for i in range(50): +def waitforfiles(*files, timeout=50): + for i in range(timeout): wait = False for f in files: @@ -33,6 +34,21 @@ def waitforfiles(*files): return False +def waitforglob(pattern, count=1, timeout=50): + for i in range(timeout): + n = 0 + + for f in glob.glob(pattern): + n += 1 + + if n == count: + 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: @@ -72,8 +88,8 @@ def sysctl(): return out -def waitformount(template, wait=50): - for i in range(wait): +def waitformount(template, timeout=50): + for i in range(timeout): if findmnt().find(template) != -1: return True @@ -82,8 +98,8 @@ def waitformount(template, wait=50): return False -def waitforunmount(template, wait=50): - for i in range(wait): +def waitforunmount(template, timeout=50): + for i in range(timeout): if findmnt().find(template) == -1: return True -- cgit From bc849920754b7b84a34ea46de4d42fa33b507c93 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Sat, 3 Jul 2021 19:15:04 +0100 Subject: Tests: address configuration tests reworked. --- test/test_configuration.py | 79 ++++++++++++---------------------------------- 1 file changed, 21 insertions(+), 58 deletions(-) (limited to 'test') diff --git a/test/test_configuration.py b/test/test_configuration.py index c149658c..8655968f 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -8,6 +8,15 @@ from unit.control import TestControl class TestConfiguration(TestControl): prerequisites = {'modules': {'python': 'any'}} + def try_addr(self, addr): + return self.conf( + { + "listeners": {addr: {"pass": "routes"}}, + "routes": [{"action": {"return": 200}}], + "applications": {}, + } + ) + def test_json_empty(self): assert 'error' in self.conf(''), 'empty' @@ -218,50 +227,20 @@ class TestConfiguration(TestControl): {"*:7080": {"pass": "applications/app"}}, 'listeners' ), 'listeners no app' - def test_listeners_wildcard(self): - assert 'success' in self.conf( - { - "listeners": {"*:7080": {"pass": "applications/app"}}, - "applications": { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), 'listeners wildcard' + def test_listeners_addr(self): + assert 'success' in self.try_addr("*:7080"), 'wildcard' + assert 'success' in self.try_addr("127.0.0.1:7081"), 'explicit' + assert 'success' in self.try_addr("[::1]:7082"), 'explicit ipv6' - def test_listeners_explicit(self): - assert 'success' in self.conf( - { - "listeners": {"127.0.0.1:7080": {"pass": "applications/app"}}, - "applications": { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), 'explicit' + def test_listeners_addr_error(self): + assert 'error' in self.try_addr("127.0.0.1"), 'no port' - def test_listeners_explicit_ipv6(self): - assert 'success' in self.conf( - { - "listeners": {"[::1]:7080": {"pass": "applications/app"}}, - "applications": { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), 'explicit ipv6' + def test_listeners_addr_error_2(self, skip_alert): + skip_alert(r'bind.*failed', r'failed to apply new conf') + + assert 'error' in self.try_addr( + "[f607:7403:1e4b:6c66:33b2:843f:2517:da27]:7080" + ) def test_listeners_port_release(self): for i in range(10): @@ -290,22 +269,6 @@ class TestConfiguration(TestControl): assert 'success' in resp, 'port release' - @pytest.mark.skip('not yet, unsafe') - def test_listeners_no_port(self): - assert 'error' in self.conf( - { - "listeners": {"127.0.0.1": {"pass": "applications/app"}}, - "applications": { - "app": { - "type": "python", - "processes": {"spare": 0}, - "path": "/app", - "module": "wsgi", - } - }, - } - ), 'no port' - def test_json_application_name_large(self): name = "X" * 1024 * 1024 -- cgit From 4f94df6a71d60e3930d3d5f1dd1301ba43778618 Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Tue, 6 Jul 2021 12:22:10 +0100 Subject: Tests: print_log_on_assert() decorator introduced. --- test/conftest.py | 155 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 82 insertions(+), 73 deletions(-) (limited to 'test') diff --git a/test/conftest.py b/test/conftest.py index db34984f..87471287 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -74,7 +74,7 @@ def pytest_addoption(parser): unit_instance = {} _processes = [] -_fds_check = { +_fds_info = { 'main': {'fds': 0, 'skip': False}, 'router': {'name': 'unit: router', 'pid': -1, 'fds': 0, 'skip': False}, 'controller': { @@ -115,6 +115,17 @@ def pytest_configure(config): fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, 0) +def print_log_on_assert(func): + def inner_function(*args, **kwargs): + try: + func(*args, **kwargs) + except AssertionError as e: + _print_log(kwargs.get('log', None)) + raise e + + return inner_function + + def pytest_generate_tests(metafunc): cls = metafunc.cls if ( @@ -275,9 +286,9 @@ def run(request): ] option.skip_sanitizer = False - _fds_check['main']['skip'] = False - _fds_check['router']['skip'] = False - _fds_check['controller']['skip'] = False + _fds_info['main']['skip'] = False + _fds_info['router']['skip'] = False + _fds_info['controller']['skip'] = False yield @@ -299,7 +310,7 @@ def run(request): # clean temp_dir before the next test if not option.restart: - _clear_conf(unit['temp_dir'] + '/control.unit.sock', log) + _clear_conf(unit['temp_dir'] + '/control.unit.sock', log=log) for item in os.listdir(unit['temp_dir']): if item not in [ @@ -319,51 +330,9 @@ def run(request): else: shutil.rmtree(path) - # check descriptors (wait for some time before check) - - def waitforfds(diff): - for i in range(600): - fds_diff = diff() - - if fds_diff <= option.fds_threshold: - break - - time.sleep(0.1) + # check descriptors - return fds_diff - - ps = _fds_check['main'] - if not ps['skip']: - fds_diff = waitforfds( - lambda: _count_fds(unit_instance['pid']) - ps['fds'] - ) - ps['fds'] += fds_diff - - assert ( - fds_diff <= option.fds_threshold - ), 'descriptors leak main process' - - else: - ps['fds'] = _count_fds(unit_instance['pid']) - - for name in ['controller', 'router']: - ps = _fds_check[name] - ps_pid = ps['pid'] - ps['pid'] = pid_by_name(ps['name']) - - if not ps['skip']: - fds_diff = waitforfds(lambda: _count_fds(ps['pid']) - ps['fds']) - ps['fds'] += fds_diff - - if not option.restart: - assert ps['pid'] == ps_pid, 'same pid %s' % name - - assert fds_diff <= option.fds_threshold, ( - 'descriptors leak %s' % name - ) - - else: - ps['fds'] = _count_fds(ps['pid']) + _check_fds(log=log) # print unit.log in case of error @@ -440,13 +409,13 @@ def unit_run(): _clear_conf(unit_instance['temp_dir'] + '/control.unit.sock') - _fds_check['main']['fds'] = _count_fds(unit_instance['pid']) + _fds_info['main']['fds'] = _count_fds(unit_instance['pid']) - router = _fds_check['router'] + router = _fds_info['router'] router['pid'] = pid_by_name(router['name']) router['fds'] = _count_fds(router['pid']) - controller = _fds_check['controller'] + controller = _fds_info['controller'] controller['pid'] = pid_by_name(controller['name']) controller['fds'] = _count_fds(controller['pid']) @@ -481,7 +450,8 @@ def unit_stop(): return 'Could not terminate unit' -def _check_alerts(log=None): +@print_log_on_assert +def _check_alerts(*, log=None): if log is None: with Log.open(encoding='utf-8') as f: log = f.read() @@ -499,22 +469,18 @@ def _check_alerts(log=None): for skip in option.skip_alerts: alerts = [al for al in alerts if re.search(skip, al) is None] - if alerts: - _print_log(log) - assert not alerts, 'alert(s)' + assert not alerts, 'alert(s)' if not option.skip_sanitizer: sanitizer_errors = re.findall('.+Sanitizer.+', log) - if sanitizer_errors: - _print_log(log) - assert not sanitizer_errors, 'sanitizer error(s)' + assert not sanitizer_errors, 'sanitizer error(s)' if found: print('skipped.') -def _print_log(data=None): +def _print_log(log): path = Log.get_path() print('Path to unit.log:\n' + path + '\n') @@ -523,19 +489,15 @@ def _print_log(data=None): os.set_blocking(sys.stdout.fileno(), True) sys.stdout.flush() - if data is None: + if log is None: with open(path, 'r', encoding='utf-8', errors='ignore') as f: shutil.copyfileobj(f, sys.stdout) else: - sys.stdout.write(data) - + sys.stdout.write(log) -def _clear_conf(sock, log=None): - def check_success(resp): - if 'success' not in resp: - _print_log(log) - assert 'success' in resp +@print_log_on_assert +def _clear_conf(sock, *, log=None): resp = http.put( url='/config', sock_type='unix', @@ -543,7 +505,7 @@ def _clear_conf(sock, log=None): body=json.dumps({"listeners": {}, "applications": {}}), )['body'] - check_success(resp) + assert 'success' in resp, 'clear conf' if 'openssl' not in option.available['modules']: return @@ -561,7 +523,54 @@ def _clear_conf(sock, log=None): url='/certificates/' + cert, sock_type='unix', addr=sock, )['body'] - check_success(resp) + assert 'success' in resp, 'remove certificate' + + +@print_log_on_assert +def _check_fds(*, log=None): + def waitforfds(diff): + for i in range(600): + fds_diff = diff() + + if fds_diff <= option.fds_threshold: + break + + time.sleep(0.1) + + return fds_diff + + ps = _fds_info['main'] + if not ps['skip']: + fds_diff = waitforfds( + lambda: _count_fds(unit_instance['pid']) - ps['fds'] + ) + ps['fds'] += fds_diff + + assert ( + fds_diff <= option.fds_threshold + ), 'descriptors leak main process' + + else: + ps['fds'] = _count_fds(unit_instance['pid']) + + for name in ['controller', 'router']: + ps = _fds_info[name] + ps_pid = ps['pid'] + ps['pid'] = pid_by_name(ps['name']) + + if not ps['skip']: + fds_diff = waitforfds(lambda: _count_fds(ps['pid']) - ps['fds']) + ps['fds'] += fds_diff + + if not option.restart: + assert ps['pid'] == ps_pid, 'same pid %s' % name + + assert fds_diff <= option.fds_threshold, ( + 'descriptors leak %s' % name + ) + + else: + ps['fds'] = _count_fds(ps['pid']) def _count_fds(pid): @@ -639,9 +648,9 @@ def skip_alert(): @pytest.fixture() def skip_fds_check(): def _skip(main=False, router=False, controller=False): - _fds_check['main']['skip'] = main - _fds_check['router']['skip'] = router - _fds_check['controller']['skip'] = controller + _fds_info['main']['skip'] = main + _fds_info['router']['skip'] = router + _fds_info['controller']['skip'] = controller return _skip -- cgit From 1f2ba4dca8c67442e19367ac7f1f96dbff6457ff Mon Sep 17 00:00:00 2001 From: Oisin Canty Date: Wed, 21 Jul 2021 14:53:33 +0000 Subject: Tests: use mutex with multitthreaded Ruby hooks. This commit fixes a rare crash that can occur when File.write is called by many threads. --- test/ruby/hooks/multiple.rb | 6 +++++- test/ruby/hooks/on_thread_boot.rb | 6 +++++- test/ruby/hooks/on_thread_shutdown.rb | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) (limited to 'test') diff --git a/test/ruby/hooks/multiple.rb b/test/ruby/hooks/multiple.rb index 295b46a9..b1b659a5 100644 --- a/test/ruby/hooks/multiple.rb +++ b/test/ruby/hooks/multiple.rb @@ -1,9 +1,13 @@ require 'securerandom' +@mutex = Mutex.new + on_worker_boot do File.write("./cookie_worker_boot.#{SecureRandom.hex}", "worker booted") end on_thread_boot do - File.write("./cookie_thread_boot.#{SecureRandom.hex}", "thread booted") + @mutex.synchronize do + File.write("./cookie_thread_boot.#{SecureRandom.hex}", "thread booted") + end end diff --git a/test/ruby/hooks/on_thread_boot.rb b/test/ruby/hooks/on_thread_boot.rb index 1e05e248..4f88424e 100644 --- a/test/ruby/hooks/on_thread_boot.rb +++ b/test/ruby/hooks/on_thread_boot.rb @@ -1,5 +1,9 @@ require 'securerandom' +@mutex = Mutex.new + on_thread_boot do - File.write("./cookie_thread_boot.#{SecureRandom.hex}", "booted") + @mutex.synchronize do + File.write("./cookie_thread_boot.#{SecureRandom.hex}", "booted") + end end diff --git a/test/ruby/hooks/on_thread_shutdown.rb b/test/ruby/hooks/on_thread_shutdown.rb index e65c1b42..d953b8b7 100644 --- a/test/ruby/hooks/on_thread_shutdown.rb +++ b/test/ruby/hooks/on_thread_shutdown.rb @@ -1,5 +1,9 @@ require 'securerandom' +@mutex = Mutex.new + on_thread_shutdown do - File.write("./cookie_thread_shutdown.#{SecureRandom.hex}", "shutdown") + @mutex.synchronize do + File.write("./cookie_thread_shutdown.#{SecureRandom.hex}", "shutdown") + end end -- cgit From 960ffc99677b8451a3cc4ea7bc053b3c123e4e1b Mon Sep 17 00:00:00 2001 From: Andrei Zeliankou Date: Fri, 23 Jul 2021 15:37:03 +0100 Subject: Tests: added SNI test without hostname in request. --- test/test_tls_sni.py | 20 ++++++++++++++++++++ test/unit/http.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_tls_sni.py b/test/test_tls_sni.py index 2e5424e2..eba6140a 100644 --- a/test/test_tls_sni.py +++ b/test/test_tls_sni.py @@ -168,6 +168,26 @@ basicConstraints = critical,CA:TRUE""" self.check_cert('alt2.example.com', bundles['example.com']['subj']) self.check_cert('blah', bundles['default']['subj']) + def test_tls_sni_no_hostname(self): + bundles = { + "localhost.com": {"subj": "localhost.com", "alt_names": []}, + "example.com": { + "subj": "example.com", + "alt_names": ["example.com"], + }, + } + self.config_bundles(bundles) + self.add_tls(["localhost.com", "example.com"]) + + resp, sock = self.get_ssl( + headers={'Content-Length': '0', 'Connection': 'close'}, start=True, + ) + assert resp['status'] == 200 + assert ( + sock.getpeercert()['subject'][0][0][1] + == bundles['localhost.com']['subj'] + ) + def test_tls_sni_upper_case(self): bundles = { "localhost.com": {"subj": "LOCALHOST.COM", "alt_names": []}, diff --git a/test/unit/http.py b/test/unit/http.py index 797b7681..dcfcd232 100644 --- a/test/unit/http.py +++ b/test/unit/http.py @@ -45,7 +45,7 @@ class TestHTTP: sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) if 'wrapper' in kwargs: - server_hostname = headers.get('Host', 'localhost') + server_hostname = headers.get('Host', None) sock = kwargs['wrapper'](sock, server_hostname=server_hostname) connect_args = addr if sock_type == 'unix' else (addr, port) -- cgit From fa9fb29be221e0393562831a9e3bcba416652f60 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Thu, 29 Jul 2021 19:50:39 +0300 Subject: Application restart introduced. When processing a restart request, the router sends a QUIT message to all existing processes of the application. Then, a new shared application port is created to ensure that new requests won't be handled by the old processes of the application. --- test/python/restart/longstart.py | 10 +++++ test/python/restart/v1.py | 7 ++++ test/python/restart/v2.py | 7 ++++ test/test_python_procman.py | 79 +++++++++++++++++++++++++++++++++++ test/unit/applications/lang/python.py | 1 + 5 files changed, 104 insertions(+) create mode 100644 test/python/restart/longstart.py create mode 100644 test/python/restart/v1.py create mode 100644 test/python/restart/v2.py (limited to 'test') diff --git a/test/python/restart/longstart.py b/test/python/restart/longstart.py new file mode 100644 index 00000000..777398ac --- /dev/null +++ b/test/python/restart/longstart.py @@ -0,0 +1,10 @@ +import os +import time + +time.sleep(2) + +def application(environ, start_response): + body = str(os.getpid()).encode() + + start_response('200', [('Content-Length', str(len(body)))]) + return [body] diff --git a/test/python/restart/v1.py b/test/python/restart/v1.py new file mode 100644 index 00000000..2e45b269 --- /dev/null +++ b/test/python/restart/v1.py @@ -0,0 +1,7 @@ +import os + +def application(environ, start_response): + body = "v1".encode() + + start_response('200', [('Content-Length', str(len(body)))]) + return [body] diff --git a/test/python/restart/v2.py b/test/python/restart/v2.py new file mode 100644 index 00000000..59e3d30f --- /dev/null +++ b/test/python/restart/v2.py @@ -0,0 +1,7 @@ +import os + +def application(environ, start_response): + body = "v2".encode() + + start_response('200', [('Content-Length', str(len(body)))]) + return [body] diff --git a/test/test_python_procman.py b/test/test_python_procman.py index b0d0f5af..a95c5680 100644 --- a/test/test_python_procman.py +++ b/test/test_python_procman.py @@ -1,4 +1,5 @@ import re +import shutil import subprocess import time @@ -201,3 +202,81 @@ class TestPythonProcman(TestApplicationPython): assert 'success' in self.conf({"listeners": {}, "applications": {}}) assert len(self.pids_for_process()) == 0, 'stop all' + + def test_python_restart(self, temp_dir): + shutil.copyfile( + option.test_dir + '/python/restart/v1.py', temp_dir + '/wsgi.py' + ) + + self.load( + temp_dir, + name=self.app_name, + processes=1, + environment={'PYTHONDONTWRITEBYTECODE': '1'}, + ) + + b = self.get()['body'] + assert b == "v1", 'process started' + + shutil.copyfile( + option.test_dir + '/python/restart/v2.py', temp_dir + '/wsgi.py' + ) + + b = self.get()['body'] + assert b == "v1", 'still old process' + + assert 'success' in self.conf_get( + '/control/applications/' + self.app_name + '/restart' + ), 'restart processes' + + b = self.get()['body'] + assert b == "v2", 'new process started' + + assert 'error' in self.conf_get( + '/control/applications/blah/restart' + ), 'application incorrect' + + assert 'error' in self.conf_delete( + '/control/applications/' + self.app_name + '/restart' + ), 'method incorrect' + + def test_python_restart_multi(self): + self.conf_proc('2') + + pids = self.pids_for_process() + assert len(pids) == 2, 'restart 2 started' + + assert 'success' in self.conf_get( + '/control/applications/' + self.app_name + '/restart' + ), 'restart processes' + + new_pids = self.pids_for_process() + assert len(new_pids) == 2, 'restart still 2' + + assert len(new_pids.intersection(pids)) == 0, 'restart all new' + + def test_python_restart_longstart(self): + self.load( + 'restart', + name=self.app_name, + module="longstart", + processes={"spare": 1, "max": 2, "idle_timeout": 5}, + ) + + assert len(self.pids_for_process()) == 1, 'longstarts == 1' + + pid = self.get()['body'] + pids = self.pids_for_process() + assert len(pids) == 2, 'longstarts == 2' + + assert 'success' in self.conf_get( + '/control/applications/' + self.app_name + '/restart' + ), 'restart processes' + + # wait for longstarted app + time.sleep(2) + + new_pids = self.pids_for_process() + assert len(new_pids) == 1, 'restart 1' + + assert len(new_pids.intersection(pids)) == 0, 'restart all new' diff --git a/test/unit/applications/lang/python.py b/test/unit/applications/lang/python.py index b399dffd..215aa332 100644 --- a/test/unit/applications/lang/python.py +++ b/test/unit/applications/lang/python.py @@ -44,6 +44,7 @@ class TestApplicationPython(TestApplicationProto): for attr in ( 'callable', + 'environment', 'home', 'limits', 'path', -- cgit From d16cf0416784db82147ee5aaad1054840d028e7d Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Mon, 2 Aug 2021 12:30:38 +0800 Subject: Router: fixed segmentation fault. In the case that routes or upstreams is empty and the pass option is a variable. If the resolved pass is routes or upstreams, a segment error occurred. --- test/test_variables.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'test') diff --git a/test/test_variables.py b/test/test_variables.py index 139d867e..d8547b7b 100644 --- a/test/test_variables.py +++ b/test/test_variables.py @@ -100,6 +100,25 @@ class TestVariables(TestApplicationProto): assert self.get(url='/1')['status'] == 200 assert self.get(url='/2')['status'] == 404 + def test_variables_empty(self): + def update_pass(prefix): + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": prefix + "/$method"}, + }, + }, + ), 'variables empty' + + update_pass("routes"); + assert self.get(url='/1')['status'] == 404 + + update_pass("upstreams"); + assert self.get(url='/2')['status'] == 404 + + update_pass("applications"); + assert self.get(url='/3')['status'] == 404 + def test_variables_invalid(self): def check_variables(routes): assert 'error' in self.conf( -- cgit From 60cf1399611ae1b2728492c94ff57a4a044774b4 Mon Sep 17 00:00:00 2001 From: Oisin Canty Date: Thu, 5 Aug 2021 16:00:01 +0000 Subject: Router: fixed crash when matching an empty address pattern array. A crash would occur when the router tried to match an against an empty address pattern array. The following configuration was used to reproduce the issue: { "listeners": { "127.0.0.1:8082": { "pass": "routes" } }, "routes": [ { "match": { "source": [] }, "action": { "return": 200 } } ] } --- test/test_routing.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'test') diff --git a/test/test_routing.py b/test/test_routing.py index eaa0a134..ef5622c2 100644 --- a/test/test_routing.py +++ b/test/test_routing.py @@ -1751,6 +1751,10 @@ class TestRouting(TestApplicationProto): self.route_match_invalid({"source": "*:1-a"}) self.route_match_invalid({"source": "*:65536"}) + def test_routes_match_source_none(self): + self.route_match({"source": []}) + assert self.get()['status'] == 404, 'source none' + def test_routes_match_destination(self): assert 'success' in self.conf( {"*:7080": {"pass": "routes"}, "*:7081": {"pass": "routes"}}, -- cgit From 1a85ad378f3374b9c83b59ae9c12277288aa7c30 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Mon, 9 Aug 2021 10:14:57 +0300 Subject: Java: upgrading third-party components. --- test/unit/applications/lang/java.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test') diff --git a/test/unit/applications/lang/java.py b/test/unit/applications/lang/java.py index c9c2095e..3a620aa0 100644 --- a/test/unit/applications/lang/java.py +++ b/test/unit/applications/lang/java.py @@ -52,7 +52,7 @@ class TestApplicationJava(TestApplicationProto): os.makedirs(classes_path) classpath = ( - option.current_dir + '/build/tomcat-servlet-api-9.0.44.jar' + option.current_dir + '/build/tomcat-servlet-api-9.0.50.jar' ) ws_jars = glob.glob( -- cgit From 5f67d30ec709ea101870d0a91db8953fc49c7810 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Wed, 11 Aug 2021 19:01:04 +0300 Subject: Tests: retrying directory remove if resource is busy. --- test/conftest.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'test') diff --git a/test/conftest.py b/test/conftest.py index 87471287..ea3aaf72 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -328,7 +328,14 @@ def run(request): ): os.remove(path) else: - shutil.rmtree(path) + for attempt in range(10): + try: + shutil.rmtree(path) + break + except OSError as err: + if err.errno != 16: + raise + time.sleep(1) # check descriptors -- cgit From 9988569beafce7f587f17a9ef710b76e9bfa3165 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Wed, 11 Aug 2021 19:01:16 +0300 Subject: Tests: initialising log params before first _print_log(). --- test/conftest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'test') diff --git a/test/conftest.py b/test/conftest.py index ea3aaf72..4d46e2fc 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -400,6 +400,8 @@ def unit_run(): with open(temp_dir + '/unit.log', 'w') as log: unit_instance['process'] = subprocess.Popen(unitd_args, stderr=log) + Log.temp_dir = temp_dir + if not waitforfiles(temp_dir + '/control.unit.sock'): _print_log() exit('Could not start unit') @@ -409,7 +411,6 @@ def unit_run(): unit_instance['unitd'] = unitd option.temp_dir = temp_dir - Log.temp_dir = temp_dir with open(temp_dir + '/unit.pid', 'r') as f: unit_instance['pid'] = f.read().rstrip() @@ -487,7 +488,7 @@ def _check_alerts(*, log=None): print('skipped.') -def _print_log(log): +def _print_log(log=None): path = Log.get_path() print('Path to unit.log:\n' + path + '\n') -- cgit From 039d032dd6f4d9720ea4e009925a45f4160df55c Mon Sep 17 00:00:00 2001 From: Oisin Canty Date: Thu, 12 Aug 2021 08:23:23 +0000 Subject: Tests: client IP address replacement. --- test/python/client_ip/wsgi.py | 4 ++ test/test_client_ip.py | 129 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 test/python/client_ip/wsgi.py create mode 100644 test/test_client_ip.py (limited to 'test') diff --git a/test/python/client_ip/wsgi.py b/test/python/client_ip/wsgi.py new file mode 100644 index 00000000..0e12db0a --- /dev/null +++ b/test/python/client_ip/wsgi.py @@ -0,0 +1,4 @@ +def application(env, start_response): + ip = env['REMOTE_ADDR'].encode() + start_response('200', [('Content-Length', str(len(ip)))]) + return ip diff --git a/test/test_client_ip.py b/test/test_client_ip.py new file mode 100644 index 00000000..0084574e --- /dev/null +++ b/test/test_client_ip.py @@ -0,0 +1,129 @@ +import pytest + +from unit.applications.lang.python import TestApplicationPython + + +class TestClientIP(TestApplicationPython): + prerequisites = {'modules': {'python': 'any'}} + + def client_ip(self, options): + assert 'success' in self.conf( + { + "127.0.0.1:7081": + {"client_ip": options, "pass": "applications/client_ip"}, + "[::1]:7082": + {"client_ip": options, "pass": "applications/client_ip"}, + }, + 'listeners', + ), 'listeners configure' + + def get_xff(self, xff, sock_type='ipv4'): + port = 7081 if sock_type == 'ipv4' else 7082 + + return self.get( + sock_type=sock_type, + port=port, + headers={'Connection': 'close', 'X-Forwarded-For': xff}, + )['body'] + + def setup_method(self): + self.load('client_ip') + + def test_settings_client_ip_single_ip(self): + self.client_ip( + {'header': 'X-Forwarded-For', 'source': '123.123.123.123'} + ) + + assert self.get(port=7081)['body'] == '127.0.0.1', 'ipv4 default' + assert ( + self.get(sock_type='ipv6', port=7082)['body'] == '::1' + ), 'ipv6 default' + assert self.get_xff('1.1.1.1') == '127.0.0.1', 'bad source' + assert self.get_xff('blah') == '127.0.0.1', 'bad header' + assert self.get_xff('1.1.1.1', 'ipv6') == '::1', 'bad source ipv6' + + self.client_ip({'header': 'X-Forwarded-For', 'source': '127.0.0.1'}) + + assert self.get(port=7081)['body'] == '127.0.0.1', 'ipv4 default 2' + assert ( + self.get(sock_type='ipv6', port=7082)['body'] == '::1' + ), 'ipv6 default 2' + assert self.get_xff('1.1.1.1') == '1.1.1.1', 'replace' + assert self.get_xff('blah') == '127.0.0.1', 'bad header 2' + assert ( + self.get_xff('1.1.1.1', 'ipv6') == '::1' + ), 'bad source ipv6 2' + + self.client_ip({'header': 'X-Forwarded-For', 'source': '!127.0.0.1'}) + + assert self.get_xff('1.1.1.1') == '127.0.0.1', 'bad source 3' + assert self.get_xff('1.1.1.1', 'ipv6') == '1.1.1.1', 'replace 2' + + def test_settings_client_ip_ipv4(self): + self.client_ip({'header': 'X-Forwarded-For', 'source': '127.0.0.1'}) + + assert ( + self.get_xff('8.8.8.8, 84.23.23.11') == '84.23.23.11' + ), 'xff replace' + assert ( + self.get_xff('8.8.8.8, 84.23.23.11, 127.0.0.1') == '127.0.0.1' + ), 'xff replace 2' + assert ( + self.get_xff(['8.8.8.8', '127.0.0.1, 10.0.1.1']) == '10.0.1.1' + ), 'xff replace multi' + + def test_settings_client_ip_ipv6(self): + self.client_ip({'header': 'X-Forwarded-For', 'source': '::1'}) + + assert self.get_xff('1.1.1.1') == '127.0.0.1', 'bad source ipv4' + + for ip in [ + 'f607:7403:1e4b:6c66:33b2:843f:2517:da27', + '2001:db8:3c4d:15::1a2f:1a2b', + '2001::3c4d:15:1a2f:1a2b', + '::11.22.33.44', + ]: + assert self.get_xff(ip, 'ipv6') == ip, 'replace' + + def test_settings_client_ip_recursive(self): + self.client_ip( + { + 'header': 'X-Forwarded-For', + 'recursive': True, + 'source': ['127.0.0.1', '10.50.0.17', '10.5.2.1'], + } + ) + + assert self.get_xff('1.1.1.1') == '1.1.1.1', 'xff chain' + assert self.get_xff('1.1.1.1, 10.5.2.1') == '1.1.1.1', 'xff chain 2' + assert ( + self.get_xff('8.8.8.8, 1.1.1.1, 10.5.2.1') == '1.1.1.1' + ), 'xff chain 3' + assert ( + self.get_xff('10.50.0.17, 10.5.2.1, 10.5.2.1') == '10.50.0.17' + ), 'xff chain 4' + assert ( + self.get_xff(['8.8.8.8', '1.1.1.1, 127.0.0.1']) == '1.1.1.1' + ), 'xff replace multi' + assert ( + self.get_xff(['8.8.8.8', '1.1.1.1, 127.0.0.1', '10.5.2.1']) + == '1.1.1.1' + ), 'xff replace multi 2' + assert ( + self.get_xff(['10.5.2.1', '10.50.0.17, 1.1.1.1', '10.5.2.1']) + == '1.1.1.1' + ), 'xff replace multi 3' + assert ( + self.get_xff('8.8.8.8, 2001:db8:3c4d:15::1a2f:1a2b, 127.0.0.1') + == '2001:db8:3c4d:15::1a2f:1a2b' + ), 'xff chain ipv6' + + def test_settings_client_ip_invalid(self): + assert 'error' in self.conf( + {"http": {"client_ip": {'header': 'X-Forwarded-For', 'source': []}}}, + 'settings', + ), 'empty array source' + assert 'error' in self.conf( + {"http":{"client_ip": {'header': 'X-Forwarded-For', 'source': 'a'}}}, + 'settings', + ), 'empty source invalid' -- cgit From b586707c861448cfdd6d8783864c4a1ce7112a77 Mon Sep 17 00:00:00 2001 From: Max Romanov Date: Thu, 12 Aug 2021 14:55:51 +0300 Subject: Java: upgrading third-party components. --- test/unit/applications/lang/java.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test') diff --git a/test/unit/applications/lang/java.py b/test/unit/applications/lang/java.py index 3a620aa0..53b27b07 100644 --- a/test/unit/applications/lang/java.py +++ b/test/unit/applications/lang/java.py @@ -52,7 +52,7 @@ class TestApplicationJava(TestApplicationProto): os.makedirs(classes_path) classpath = ( - option.current_dir + '/build/tomcat-servlet-api-9.0.50.jar' + option.current_dir + '/build/tomcat-servlet-api-9.0.52.jar' ) ws_jars = glob.glob( -- cgit